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

Revision 3184, 100.6 kB (checked in by jerome, 17 years ago)

Added some docstrings.

  • 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                    user.Payments.append((date, float(amount), description))
400            user.Exists = True
401        return user
402       
403    def getGroupFromBackend(self, groupname) :   
404        """Extracts group information given its name."""
405        group = StorageGroup(self, groupname)
406        groupname = self.userCharsetToDatabase(groupname)
407        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % (groupname, self.info["grouprdn"], groupname), ["pykotaGroupName", "pykotaLimitBy", "description"], base=self.info["groupbase"])
408        if result :
409            fields = result[0][1]
410            group.ident = result[0][0]
411            group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
412            group.Description = self.databaseToUserCharset(fields.get("description", [None])[0])
413            group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
414            group.AccountBalance = 0.0
415            group.LifeTimePaid = 0.0
416            for member in self.getGroupMembers(group) :
417                if member.Exists :
418                    group.AccountBalance += member.AccountBalance
419                    group.LifeTimePaid += member.LifeTimePaid
420            group.Exists = True
421        return group
422       
423    def getPrinterFromBackend(self, printername) :       
424        """Extracts printer information given its name : returns first matching printer."""
425        printer = StoragePrinter(self, printername)
426        printername = self.userCharsetToDatabase(printername)
427        result = self.doSearch("(&(objectClass=pykotaPrinter)(|(pykotaPrinterName=%s)(%s=%s)))" \
428                      % (printername, self.info["printerrdn"], printername), \
429                        ["pykotaPrinterName", "pykotaPricePerPage", \
430                         "pykotaPricePerJob", "pykotaMaxJobSize", \
431                         "pykotaPassThrough", "uniqueMember", "description"], \
432                      base=self.info["printerbase"])
433        if result :
434            fields = result[0][1]       # take only first matching printer, ignore the rest
435            printer.ident = result[0][0]
436            printer.Name = fields.get("pykotaPrinterName", [self.databaseToUserCharset(printername)])[0] 
437            printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0])
438            printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0])
439            printer.MaxJobSize = int(fields.get("pykotaMaxJobSize", [0])[0])
440            printer.PassThrough = fields.get("pykotaPassThrough", [None])[0]
441            if printer.PassThrough in (1, "1", "t", "true", "TRUE", "True") :
442                printer.PassThrough = 1
443            else :
444                printer.PassThrough = 0
445            printer.uniqueMember = fields.get("uniqueMember", [])
446            printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
447            printer.Exists = True
448        return printer   
449       
450    def getUserPQuotaFromBackend(self, user, printer) :       
451        """Extracts a user print quota."""
452        userpquota = StorageUserPQuota(self, user, printer)
453        if printer.Exists and user.Exists :
454            if self.info["userquotabase"].lower() == "user" :
455                base = user.ident
456            else :   
457                base = self.info["userquotabase"]
458            result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s)(pykotaPrinterName=%s))" % \
459                                      (self.userCharsetToDatabase(user.Name), self.userCharsetToDatabase(printer.Name)), \
460                                      ["pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount", "pykotaMaxJobSize"], \
461                                      base=base)
462            if result :
463                fields = result[0][1]
464                userpquota.ident = result[0][0]
465                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
466                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
467                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
468                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
469                if userpquota.SoftLimit is not None :
470                    if userpquota.SoftLimit[0].upper() == "NONE" :
471                        userpquota.SoftLimit = None
472                    else :   
473                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
474                userpquota.HardLimit = fields.get("pykotaHardLimit")
475                if userpquota.HardLimit is not None :
476                    if userpquota.HardLimit[0].upper() == "NONE" :
477                        userpquota.HardLimit = None
478                    elif userpquota.HardLimit is not None :   
479                        userpquota.HardLimit = int(userpquota.HardLimit[0])
480                userpquota.DateLimit = fields.get("pykotaDateLimit")
481                if userpquota.DateLimit is not None :
482                    if userpquota.DateLimit[0].upper() == "NONE" : 
483                        userpquota.DateLimit = None
484                    else :   
485                        userpquota.DateLimit = userpquota.DateLimit[0]
486                userpquota.MaxJobSize = fields.get("pykotaMaxJobSize")
487                if userpquota.MaxJobSize is not None :
488                    if userpquota.MaxJobSize[0].upper() == "NONE" :
489                        userpquota.MaxJobSize = None
490                    else :   
491                        userpquota.MaxJobSize = int(userpquota.MaxJobSize[0])
492                userpquota.Exists = True
493        return userpquota
494       
495    def getGroupPQuotaFromBackend(self, group, printer) :       
496        """Extracts a group print quota."""
497        grouppquota = StorageGroupPQuota(self, group, printer)
498        if group.Exists :
499            if self.info["groupquotabase"].lower() == "group" :
500                base = group.ident
501            else :   
502                base = self.info["groupquotabase"]
503            result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s)(pykotaPrinterName=%s))" % \
504                                      (self.userCharsetToDatabase(group.Name), self.userCharsetToDatabase(printer.Name)), \
505                                      ["pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaMaxJobSize"], \
506                                      base=base)
507            if result :
508                fields = result[0][1]
509                grouppquota.ident = result[0][0]
510                grouppquota.SoftLimit = fields.get("pykotaSoftLimit")
511                if grouppquota.SoftLimit is not None :
512                    if grouppquota.SoftLimit[0].upper() == "NONE" :
513                        grouppquota.SoftLimit = None
514                    else :   
515                        grouppquota.SoftLimit = int(grouppquota.SoftLimit[0])
516                grouppquota.HardLimit = fields.get("pykotaHardLimit")
517                if grouppquota.HardLimit is not None :
518                    if grouppquota.HardLimit[0].upper() == "NONE" :
519                        grouppquota.HardLimit = None
520                    else :   
521                        grouppquota.HardLimit = int(grouppquota.HardLimit[0])
522                grouppquota.DateLimit = fields.get("pykotaDateLimit")
523                if grouppquota.DateLimit is not None :
524                    if grouppquota.DateLimit[0].upper() == "NONE" : 
525                        grouppquota.DateLimit = None
526                    else :   
527                        grouppquota.DateLimit = grouppquota.DateLimit[0]
528                grouppquota.MaxJobSize = fields.get("pykotaMaxJobSize")
529                if grouppquota.MaxJobSize is not None :
530                    if grouppquota.MaxJobSize[0].upper() == "NONE" :
531                        grouppquota.MaxJobSize = None
532                    else :   
533                        grouppquota.MaxJobSize = int(grouppquota.MaxJobSize[0])
534                grouppquota.PageCounter = 0
535                grouppquota.LifePageCounter = 0
536                usernamesfilter = "".join(["(pykotaUserName=%s)" % self.userCharsetToDatabase(member.Name) for member in self.getGroupMembers(group)])
537                if usernamesfilter :
538                    usernamesfilter = "(|%s)" % usernamesfilter
539                if self.info["userquotabase"].lower() == "user" :
540                    base = self.info["userbase"]
541                else :
542                    base = self.info["userquotabase"]
543                result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)%s)" % \
544                                          (self.userCharsetToDatabase(printer.Name), usernamesfilter), \
545                                          ["pykotaPageCounter", "pykotaLifePageCounter"], base=base)
546                if result :
547                    for userpquota in result :   
548                        grouppquota.PageCounter += int(userpquota[1].get("pykotaPageCounter", [0])[0] or 0)
549                        grouppquota.LifePageCounter += int(userpquota[1].get("pykotaLifePageCounter", [0])[0] or 0)
550                grouppquota.Exists = True
551        return grouppquota
552       
553    def getPrinterLastJobFromBackend(self, printer) :       
554        """Extracts a printer's last job information."""
555        lastjob = StorageLastJob(self, printer)
556        pname = self.userCharsetToDatabase(printer.Name)
557        result = self.doSearch("(&(objectClass=pykotaLastjob)(|(pykotaPrinterName=%s)(%s=%s)))" % \
558                                  (pname, self.info["printerrdn"], pname), \
559                                  ["pykotaLastJobIdent"], \
560                                  base=self.info["lastjobbase"])
561        if result :
562            lastjob.lastjobident = result[0][0]
563            lastjobident = result[0][1]["pykotaLastJobIdent"][0]
564            result = None
565            try :
566                result = self.doSearch("objectClass=pykotaJob", [ "pykotaJobSizeBytes", 
567                                                                  "pykotaHostName", 
568                                                                  "pykotaUserName", 
569                                                                  "pykotaPrinterName", 
570                                                                  "pykotaJobId", 
571                                                                  "pykotaPrinterPageCounter", 
572                                                                  "pykotaJobSize", 
573                                                                  "pykotaAction", 
574                                                                  "pykotaJobPrice", 
575                                                                  "pykotaFileName", 
576                                                                  "pykotaTitle", 
577                                                                  "pykotaCopies", 
578                                                                  "pykotaOptions", 
579                                                                  "pykotaBillingCode", 
580                                                                  "pykotaPages", 
581                                                                  "pykotaMD5Sum", 
582                                                                  "pykotaPrecomputedJobSize",
583                                                                  "pykotaPrecomputedJobPrice",
584                                                                  "createTimestamp" ], 
585                                                                base="cn=%s,%s" % (lastjobident, self.info["jobbase"]), scope=ldap.SCOPE_BASE)
586            except PyKotaStorageError :   
587                pass # Last job entry exists, but job probably doesn't exist anymore.
588            if result :
589                fields = result[0][1]
590                lastjob.ident = result[0][0]
591                lastjob.JobId = fields.get("pykotaJobId")[0]
592                lastjob.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
593                lastjob.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0])
594                try :
595                    lastjob.JobSize = int(fields.get("pykotaJobSize", [0])[0])
596                except ValueError :   
597                    lastjob.JobSize = None
598                try :   
599                    lastjob.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
600                except ValueError :   
601                    lastjob.JobPrice = None
602                lastjob.JobAction = fields.get("pykotaAction", [""])[0]
603                lastjob.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
604                lastjob.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
605                lastjob.JobCopies = int(fields.get("pykotaCopies", [0])[0])
606                lastjob.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
607                lastjob.JobHostName = fields.get("pykotaHostName", [""])[0]
608                lastjob.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
609                lastjob.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
610                lastjob.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
611                lastjob.JobPages = fields.get("pykotaPages", [""])[0]
612                try :
613                    lastjob.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
614                except ValueError :   
615                    lastjob.PrecomputedJobSize = None
616                try :   
617                    lastjob.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
618                except ValueError :   
619                    lastjob.PrecomputedJobPrice = None
620                if lastjob.JobTitle == lastjob.JobFileName == lastjob.JobOptions == "hidden" :
621                    (lastjob.JobTitle, lastjob.JobFileName, lastjob.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
622                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
623                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
624                lastjob.JobDate = mxtime.strftime("%Y-%m-%d %H:%M:%S")
625                lastjob.Exists = True
626        return lastjob
627       
628    def getGroupMembersFromBackend(self, group) :       
629        """Returns the group's members list."""
630        groupmembers = []
631        gname = self.userCharsetToDatabase(group.Name)
632        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % \
633                                  (gname, self.info["grouprdn"], gname), \
634                                  [self.info["groupmembers"]], \
635                                  base=self.info["groupbase"])
636        if result :
637            for username in result[0][1].get(self.info["groupmembers"], []) :
638                groupmembers.append(self.getUser(self.databaseToUserCharset(username)))
639        return groupmembers       
640       
641    def getUserGroupsFromBackend(self, user) :       
642        """Returns the user's groups list."""
643        groups = []
644        uname = self.userCharsetToDatabase(user.Name)
645        result = self.doSearch("(&(objectClass=pykotaGroup)(%s=%s))" % \
646                                  (self.info["groupmembers"], uname), \
647                                  [self.info["grouprdn"], "pykotaGroupName", "pykotaLimitBy"], \
648                                  base=self.info["groupbase"])
649        if result :
650            for (groupid, fields) in result :
651                groupname = self.databaseToUserCharset((fields.get("pykotaGroupName", [None]) or fields.get(self.info["grouprdn"], [None]))[0])
652                group = self.getFromCache("GROUPS", groupname)
653                if group is None :
654                    group = StorageGroup(self, groupname)
655                    group.ident = groupid
656                    group.LimitBy = fields.get("pykotaLimitBy")
657                    if group.LimitBy is not None :
658                        group.LimitBy = group.LimitBy[0]
659                    else :   
660                        group.LimitBy = "quota"
661                    group.AccountBalance = 0.0
662                    group.LifeTimePaid = 0.0
663                    for member in self.getGroupMembers(group) :
664                        if member.Exists :
665                            group.AccountBalance += member.AccountBalance
666                            group.LifeTimePaid += member.LifeTimePaid
667                    group.Exists = True
668                    self.cacheEntry("GROUPS", group.Name, group)
669                groups.append(group)
670        return groups       
671       
672    def getParentPrintersFromBackend(self, printer) :   
673        """Get all the printer groups this printer is a member of."""
674        pgroups = []
675        result = self.doSearch("(&(objectClass=pykotaPrinter)(uniqueMember=%s))" % \
676                                  printer.ident, \
677                                  ["pykotaPrinterName"], \
678                                  base=self.info["printerbase"])
679        if result :
680            for (printerid, fields) in result :
681                if printerid != printer.ident : # In case of integrity violation.
682                    parentprinter = self.getPrinter(self.databaseToUserCharset(fields.get("pykotaPrinterName")[0]))
683                    if parentprinter.Exists :
684                        pgroups.append(parentprinter)
685        return pgroups
686       
687    def getMatchingPrinters(self, printerpattern) :
688        """Returns the list of all printers for which name matches a certain pattern."""
689        printers = []
690        # see comment at the same place in pgstorage.py
691        result = self.doSearch("objectClass=pykotaPrinter", \
692                                  ["pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerJob", "pykotaMaxJobSize", "pykotaPassThrough", "uniqueMember", "description"], \
693                                  base=self.info["printerbase"])
694        if result :
695            patterns = printerpattern.split(",")
696            try :
697                patdict = {}.fromkeys(patterns)
698            except AttributeError :   
699                # Python v2.2 or earlier
700                patdict = {}
701                for p in patterns :
702                    patdict[p] = None
703            for (printerid, fields) in result :
704                printername = self.databaseToUserCharset(fields.get("pykotaPrinterName", [""])[0] or fields.get(self.info["printerrdn"], [""])[0])
705                if patdict.has_key(printername) or self.tool.matchString(printername, patterns) :
706                    printer = StoragePrinter(self, printername)
707                    printer.ident = printerid
708                    printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0] or 0.0)
709                    printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0] or 0.0)
710                    printer.MaxJobSize = int(fields.get("pykotaMaxJobSize", [0])[0])
711                    printer.PassThrough = fields.get("pykotaPassThrough", [None])[0]
712                    if printer.PassThrough in (1, "1", "t", "true", "TRUE", "True") :
713                        printer.PassThrough = 1
714                    else :
715                        printer.PassThrough = 0
716                    printer.uniqueMember = fields.get("uniqueMember", [])
717                    printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
718                    printer.Exists = True
719                    printers.append(printer)
720                    self.cacheEntry("PRINTERS", printer.Name, printer)
721        return printers       
722       
723    def getMatchingUsers(self, userpattern) :
724        """Returns the list of all users for which name matches a certain pattern."""
725        users = []
726        # see comment at the same place in pgstorage.py
727        result = self.doSearch("objectClass=pykotaAccount", \
728                                  ["pykotaUserName", "pykotaLimitBy", self.info["usermail"], "description"], \
729                                  base=self.info["userbase"])
730        if result :
731            patterns = userpattern.split(",")
732            try :
733                patdict = {}.fromkeys(patterns)
734            except AttributeError :   
735                # Python v2.2 or earlier
736                patdict = {}
737                for p in patterns :
738                    patdict[p] = None
739            for (userid, fields) in result :
740                username = self.databaseToUserCharset(fields.get("pykotaUserName", [""])[0] or fields.get(self.info["userrdn"], [""])[0])
741                if patdict.has_key(username) or self.tool.matchString(username, patterns) :
742                    user = StorageUser(self, username)
743                    user.ident = userid
744                    user.Email = fields.get(self.info["usermail"], [None])[0]
745                    user.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
746                    user.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
747                    uname = self.userCharsetToDatabase(username)
748                    result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % \
749                                              (uname, self.info["balancerdn"], uname), \
750                                              ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments", "pykotaOverCharge"], \
751                                              base=self.info["balancebase"])
752                    if not result :
753                        raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
754                    else :
755                        fields = result[0][1]
756                        user.idbalance = result[0][0]
757                        user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
758                        user.AccountBalance = fields.get("pykotaBalance")
759                        if user.AccountBalance is not None :
760                            if user.AccountBalance[0].upper() == "NONE" :
761                                user.AccountBalance = None
762                            else :   
763                                user.AccountBalance = float(user.AccountBalance[0])
764                        user.AccountBalance = user.AccountBalance or 0.0       
765                        user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
766                        if user.LifeTimePaid is not None :
767                            if user.LifeTimePaid[0].upper() == "NONE" :
768                                user.LifeTimePaid = None
769                            else :   
770                                user.LifeTimePaid = float(user.LifeTimePaid[0])
771                        user.LifeTimePaid = user.LifeTimePaid or 0.0       
772                        user.Payments = []
773                        for payment in fields.get("pykotaPayments", []) :
774                            try :
775                                (date, amount, description) = payment.split(" # ")
776                            except ValueError :
777                                # Payment with no description (old Payment)
778                                (date, amount) = payment.split(" # ")
779                                description = ""
780                            else :   
781                                description = self.databaseToUserCharset(base64.decodestring(description))
782                            user.Payments.append((date, float(amount), description))
783                    user.Exists = True
784                    users.append(user)
785                    self.cacheEntry("USERS", user.Name, user)
786        return users       
787       
788    def getMatchingGroups(self, grouppattern) :
789        """Returns the list of all groups for which name matches a certain pattern."""
790        groups = []
791        # see comment at the same place in pgstorage.py
792        result = self.doSearch("objectClass=pykotaGroup", \
793                                  ["pykotaGroupName", "pykotaLimitBy", "description"], \
794                                  base=self.info["groupbase"])
795        if result :
796            patterns = grouppattern.split(",")
797            try :
798                patdict = {}.fromkeys(patterns)
799            except AttributeError :   
800                # Python v2.2 or earlier
801                patdict = {}
802                for p in patterns :
803                    patdict[p] = None
804            for (groupid, fields) in result :
805                groupname = self.databaseToUserCharset(fields.get("pykotaGroupName", [""])[0] or fields.get(self.info["grouprdn"], [""])[0])
806                if patdict.has_key(groupname) or self.tool.matchString(groupname, patterns) :
807                    group = StorageGroup(self, groupname)
808                    group.ident = groupid
809                    group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
810                    group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
811                    group.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
812                    group.AccountBalance = 0.0
813                    group.LifeTimePaid = 0.0
814                    for member in self.getGroupMembers(group) :
815                        if member.Exists :
816                            group.AccountBalance += member.AccountBalance
817                            group.LifeTimePaid += member.LifeTimePaid
818                    group.Exists = True
819                    groups.append(group)
820                    self.cacheEntry("GROUPS", group.Name, group)
821        return groups
822       
823    def getPrinterUsersAndQuotas(self, printer, names=["*"]) :       
824        """Returns the list of users who uses a given printer, along with their quotas."""
825        usersandquotas = []
826        pname = self.userCharsetToDatabase(printer.Name)
827        names = [self.userCharsetToDatabase(n) for n in names]
828        if self.info["userquotabase"].lower() == "user" :
829            base = self.info["userbase"]
830        else :
831            base = self.info["userquotabase"]
832        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % \
833                                  (pname, "".join(["(pykotaUserName=%s)" % uname for uname in names])), \
834                                  ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], \
835                                  base=base)
836        if result :
837            for (userquotaid, fields) in result :
838                user = self.getUser(self.databaseToUserCharset(fields.get("pykotaUserName")[0]))
839                userpquota = StorageUserPQuota(self, user, printer)
840                userpquota.ident = userquotaid
841                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
842                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
843                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
844                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
845                if userpquota.SoftLimit is not None :
846                    if userpquota.SoftLimit[0].upper() == "NONE" :
847                        userpquota.SoftLimit = None
848                    else :   
849                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
850                userpquota.HardLimit = fields.get("pykotaHardLimit")
851                if userpquota.HardLimit is not None :
852                    if userpquota.HardLimit[0].upper() == "NONE" :
853                        userpquota.HardLimit = None
854                    elif userpquota.HardLimit is not None :   
855                        userpquota.HardLimit = int(userpquota.HardLimit[0])
856                userpquota.DateLimit = fields.get("pykotaDateLimit")
857                if userpquota.DateLimit is not None :
858                    if userpquota.DateLimit[0].upper() == "NONE" : 
859                        userpquota.DateLimit = None
860                    else :   
861                        userpquota.DateLimit = userpquota.DateLimit[0]
862                userpquota.Exists = True
863                usersandquotas.append((user, userpquota))
864                self.cacheEntry("USERPQUOTAS", "%s@%s" % (user.Name, printer.Name), userpquota)
865        usersandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
866        return usersandquotas
867               
868    def getPrinterGroupsAndQuotas(self, printer, names=["*"]) :       
869        """Returns the list of groups which uses a given printer, along with their quotas."""
870        groupsandquotas = []
871        pname = self.userCharsetToDatabase(printer.Name)
872        names = [self.userCharsetToDatabase(n) for n in names]
873        if self.info["groupquotabase"].lower() == "group" :
874            base = self.info["groupbase"]
875        else :
876            base = self.info["groupquotabase"]
877        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s)(|%s))" % \
878                                  (pname, "".join(["(pykotaGroupName=%s)" % gname for gname in names])), \
879                                  ["pykotaGroupName"], \
880                                  base=base)
881        if result :
882            for (groupquotaid, fields) in result :
883                group = self.getGroup(self.databaseToUserCharset(fields.get("pykotaGroupName")[0]))
884                grouppquota = self.getGroupPQuota(group, printer)
885                groupsandquotas.append((group, grouppquota))
886        groupsandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
887        return groupsandquotas
888       
889    def addPrinter(self, printer) :
890        """Adds a printer to the quota storage, returns the old value if it already exists."""
891        oldentry = self.getPrinter(printer.Name)
892        if oldentry.Exists :
893            return oldentry # we return the existing entry
894        printername = self.userCharsetToDatabase(printer.Name)
895        fields = { self.info["printerrdn"] : printername,
896                   "objectClass" : ["pykotaObject", "pykotaPrinter"],
897                   "cn" : printername,
898                   "pykotaPrinterName" : printername,
899                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
900                   "pykotaMaxJobSize" : str(printer.MaxJobSize or 0),
901                   "description" : self.userCharsetToDatabase(printer.Description or ""),
902                   "pykotaPricePerPage" : str(printer.PricePerPage or 0.0),
903                   "pykotaPricePerJob" : str(printer.PricePerJob or 0.0),
904                 } 
905        dn = "%s=%s,%s" % (self.info["printerrdn"], printername, self.info["printerbase"])
906        self.doAdd(dn, fields)
907        printer.isDirty = False
908        return None # the entry created doesn't need further modification
909       
910    def addUser(self, user) :       
911        """Adds a user to the quota storage, returns the old value if it already exists."""
912        oldentry = self.getUser(user.Name)
913        if oldentry.Exists :
914            return oldentry # we return the existing entry
915        uname = self.userCharsetToDatabase(user.Name)
916        newfields = {
917                       "pykotaUserName" : uname,
918                       "pykotaLimitBy" : (user.LimitBy or "quota"),
919                       "description" : self.userCharsetToDatabase(user.Description or ""),
920                       self.info["usermail"] : user.Email or "",
921                    }   
922                       
923        mustadd = 1
924        if self.info["newuser"].lower() != 'below' :
925            try :
926                (where, action) = [s.strip() for s in self.info["newuser"].split(",")]
927            except ValueError :
928                (where, action) = (self.info["newuser"].strip(), "fail")
929            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
930                                      (where, self.info["userrdn"], uname), \
931                                      None, \
932                                      base=self.info["userbase"])
933            if result :
934                (dn, fields) = result[0]
935                oc = fields.get("objectClass", fields.get("objectclass", []))
936                oc.extend(["pykotaAccount", "pykotaAccountBalance"])
937                fields.update(newfields)
938                fields.update({ "pykotaBalance" : str(user.AccountBalance or 0.0),
939                                "pykotaOverCharge" : str(user.OverCharge),
940                                "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), })   
941                self.doModify(dn, fields)
942                mustadd = 0
943            else :
944                message = _("Unable to find an existing objectClass %s entry with %s=%s to attach pykotaAccount objectClass") % (where, self.info["userrdn"], user.Name)
945                if action.lower() == "warn" :   
946                    self.tool.printInfo(_("%s. A new entry will be created instead.") % message, "warn")
947                else : # 'fail' or incorrect setting
948                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
949               
950        if mustadd :
951            if self.info["userbase"] == self.info["balancebase"] :           
952                fields = { self.info["userrdn"] : uname,
953                           "objectClass" : ["pykotaObject", "pykotaAccount", "pykotaAccountBalance"],
954                           "cn" : uname,
955                           "pykotaBalance" : str(user.AccountBalance or 0.0),
956                           "pykotaOverCharge" : str(user.OverCharge),
957                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
958                         } 
959            else :             
960                fields = { self.info["userrdn"] : uname,
961                           "objectClass" : ["pykotaObject", "pykotaAccount"],
962                           "cn" : uname,
963                         } 
964            fields.update(newfields)         
965            dn = "%s=%s,%s" % (self.info["userrdn"], uname, self.info["userbase"])
966            self.doAdd(dn, fields)
967            if self.info["userbase"] != self.info["balancebase"] :           
968                fields = { self.info["balancerdn"] : uname,
969                           "objectClass" : ["pykotaObject", "pykotaAccountBalance"],
970                           "cn" : uname,
971                           "pykotaBalance" : str(user.AccountBalance or 0.0),
972                           "pykotaOverCharge" : str(user.OverCharge),
973                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
974                         } 
975                dn = "%s=%s,%s" % (self.info["balancerdn"], uname, self.info["balancebase"])
976                self.doAdd(dn, fields)
977        user.idbalance = dn
978        if user.PaymentsBacklog :
979            for (value, comment) in user.PaymentsBacklog :
980                self.writeNewPayment(user, value, comment)
981            user.PaymentsBacklog = []
982        user.isDirty = False
983        return None # the entry created doesn't need further modification
984       
985    def addGroup(self, group) :       
986        """Adds a group to the quota storage, returns the old value if it already exists."""
987        oldentry = self.getGroup(group.Name)
988        if oldentry.Exists :
989            return oldentry # we return the existing entry
990        gname = self.userCharsetToDatabase(group.Name)
991        newfields = { 
992                      "pykotaGroupName" : gname,
993                      "pykotaLimitBy" : (group.LimitBy or "quota"),
994                      "description" : self.userCharsetToDatabase(group.Description or "")
995                    } 
996        mustadd = 1
997        if self.info["newgroup"].lower() != 'below' :
998            try :
999                (where, action) = [s.strip() for s in self.info["newgroup"].split(",")]
1000            except ValueError :
1001                (where, action) = (self.info["newgroup"].strip(), "fail")
1002            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
1003                                      (where, self.info["grouprdn"], gname), \
1004                                      None, \
1005                                      base=self.info["groupbase"])
1006            if result :
1007                (dn, fields) = result[0]
1008                oc = fields.get("objectClass", fields.get("objectclass", []))
1009                oc.extend(["pykotaGroup"])
1010                fields.update(newfields)
1011                self.doModify(dn, fields)
1012                mustadd = 0
1013            else :
1014                message = _("Unable to find an existing entry to attach pykotaGroup objectclass %s") % group.Name
1015                if action.lower() == "warn" :   
1016                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
1017                else : # 'fail' or incorrect setting
1018                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
1019               
1020        if mustadd :
1021            fields = { self.info["grouprdn"] : gname,
1022                       "objectClass" : ["pykotaObject", "pykotaGroup"],
1023                       "cn" : gname,
1024                     } 
1025            fields.update(newfields)         
1026            dn = "%s=%s,%s" % (self.info["grouprdn"], gname, self.info["groupbase"])
1027            self.doAdd(dn, fields)
1028        group.isDirty = False
1029        return None # the entry created doesn't need further modification
1030       
1031    def addUserToGroup(self, user, group) :   
1032        """Adds an user to a group."""
1033        if user.Name not in [u.Name for u in self.getGroupMembers(group)] :
1034            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1035            if result :
1036                fields = result[0][1]
1037                if not fields.has_key(self.info["groupmembers"]) :
1038                    fields[self.info["groupmembers"]] = []
1039                fields[self.info["groupmembers"]].append(self.userCharsetToDatabase(user.Name))
1040                self.doModify(group.ident, fields)
1041                group.Members.append(user)
1042               
1043    def delUserFromGroup(self, user, group) :   
1044        """Removes an user from a group."""
1045        if user.Name in [u.Name for u in self.getGroupMembers(group)] :
1046            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)
1047            if result :
1048                fields = result[0][1]
1049                if not fields.has_key(self.info["groupmembers"]) :
1050                    fields[self.info["groupmembers"]] = []
1051                try :   
1052                    fields[self.info["groupmembers"]].remove(self.userCharsetToDatabase(user.Name))
1053                except ValueError :
1054                    pass # TODO : Strange, shouldn't it be there ?
1055                else :
1056                    self.doModify(group.ident, fields)
1057                    group.Members.remove(user)
1058               
1059    def addUserPQuota(self, upq) :
1060        """Initializes a user print quota on a printer."""
1061        # first check if an entry already exists
1062        oldentry = self.getUserPQuota(upq.User, upq.Printer)
1063        if oldentry.Exists :
1064            return oldentry # we return the existing entry
1065        uuid = self.genUUID()
1066        uname = self.userCharsetToDatabase(upq.User.Name)
1067        pname = self.userCharsetToDatabase(upq.Printer.Name)
1068        fields = { "cn" : uuid,
1069                   "objectClass" : ["pykotaObject", "pykotaUserPQuota"],
1070                   "pykotaUserName" : uname,
1071                   "pykotaPrinterName" : pname,
1072                   "pykotaSoftLimit" : str(upq.SoftLimit),
1073                   "pykotaHardLimit" : str(upq.HardLimit),
1074                   "pykotaDateLimit" : str(upq.DateLimit),
1075                   "pykotaPageCounter" : str(upq.PageCounter or 0),
1076                   "pykotaLifePageCounter" : str(upq.LifePageCounter or 0),
1077                   "pykotaWarnCount" : str(upq.WarnCount or 0),
1078                   "pykotaMaxJobSize" : str(upq.MaxJobSize or 0),
1079                 } 
1080        if self.info["userquotabase"].lower() == "user" :
1081            dn = "cn=%s,%s" % (uuid, upq.User.ident)
1082        else :   
1083            dn = "cn=%s,%s" % (uuid, self.info["userquotabase"])
1084        self.doAdd(dn, fields)
1085        upq.isDirty = False
1086        return None # the entry created doesn't need further modification
1087       
1088    def addGroupPQuota(self, gpq) :
1089        """Initializes a group print quota on a printer."""
1090        oldentry = self.getGroupPQuota(gpq.Group, gpq.Printer)
1091        if oldentry.Exists :
1092            return oldentry # we return the existing entry
1093        uuid = self.genUUID()
1094        gname = self.userCharsetToDatabase(gpq.Group.Name)
1095        pname = self.userCharsetToDatabase(gpq.Printer.Name)
1096        fields = { "cn" : uuid,
1097                   "objectClass" : ["pykotaObject", "pykotaGroupPQuota"],
1098                   "pykotaGroupName" : gname,
1099                   "pykotaPrinterName" : pname,
1100                   "pykotaDateLimit" : "None",
1101                 } 
1102        if self.info["groupquotabase"].lower() == "group" :
1103            dn = "cn=%s,%s" % (uuid, gpq.Group.ident)
1104        else :   
1105            dn = "cn=%s,%s" % (uuid, self.info["groupquotabase"])
1106        self.doAdd(dn, fields)
1107        gpq.isDirty = False
1108        return None # the entry created doesn't need further modification
1109       
1110    def savePrinter(self, printer) :   
1111        """Saves the printer to the database in a single operation."""
1112        fields = {
1113                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
1114                   "pykotaMaxJobSize" : str(printer.MaxJobSize or 0),
1115                   "description" : self.userCharsetToDatabase(printer.Description or ""),
1116                   "pykotaPricePerPage" : str(printer.PricePerPage or 0.0),
1117                   "pykotaPricePerJob" : str(printer.PricePerJob or 0.0),
1118                 }
1119        self.doModify(printer.ident, fields)
1120       
1121    def saveUser(self, user) :
1122        """Saves the user to the database in a single operation."""
1123        newfields = {
1124                       "pykotaLimitBy" : (user.LimitBy or "quota"),
1125                       "description" : self.userCharsetToDatabase(user.Description or ""), 
1126                       self.info["usermail"] : user.Email or "",
1127                    }   
1128        self.doModify(user.ident, newfields)
1129       
1130        newfields = { "pykotaBalance" : str(user.AccountBalance or 0.0),
1131                      "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
1132                      "pykotaOverCharge" : str(user.OverCharge),
1133                    }
1134        self.doModify(user.idbalance, newfields)
1135       
1136    def saveGroup(self, group) :
1137        """Saves the group to the database in a single operation."""
1138        newfields = {
1139                       "pykotaLimitBy" : (group.LimitBy or "quota"),
1140                       "description" : self.userCharsetToDatabase(group.Description or ""), 
1141                    }   
1142        self.doModify(group.ident, newfields)
1143       
1144    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
1145        """Sets the date limit permanently for a user print quota."""
1146        fields = {
1147                   "pykotaDateLimit" : str(datelimit),
1148                 }
1149        return self.doModify(userpquota.ident, fields)
1150           
1151    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
1152        """Sets the date limit permanently for a group print quota."""
1153        fields = {
1154                   "pykotaDateLimit" : str(datelimit),
1155                 }
1156        return self.doModify(grouppquota.ident, fields)
1157       
1158    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
1159        """Increase page counters for a user print quota."""
1160        fields = {
1161                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1162                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1163                 }
1164        return self.doModify(userpquota.ident, fields)         
1165       
1166    def decreaseUserAccountBalance(self, user, amount) :   
1167        """Decreases user's account balance from an amount."""
1168        fields = {
1169                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
1170                 }
1171        return self.doModify(user.idbalance, fields, flushcache=1)         
1172       
1173    def writeNewPayment(self, user, amount, comment="") :
1174        """Adds a new payment to the payments history."""
1175        payments = []
1176        for payment in user.Payments :
1177            payments.append("%s # %s # %s" % (payment[0], str(payment[1]), base64.encodestring(self.userCharsetToDatabase(payment[2])).strip()))
1178        payments.append("%s # %s # %s" % (str(DateTime.now()), str(amount), base64.encodestring(self.userCharsetToDatabase(comment)).strip()))
1179        fields = {
1180                   "pykotaPayments" : payments,
1181                 }
1182        return self.doModify(user.idbalance, fields)         
1183       
1184    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
1185        """Sets the last job's size permanently."""
1186        fields = {
1187                   "pykotaJobSize" : str(jobsize),
1188                   "pykotaJobPrice" : str(jobprice),
1189                 }
1190        self.doModify(lastjob.ident, fields)         
1191       
1192    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) :
1193        """Adds a job in a printer's history."""
1194        uname = self.userCharsetToDatabase(user.Name)
1195        pname = self.userCharsetToDatabase(printer.Name)
1196        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1197            uuid = self.genUUID()
1198            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
1199        else :   
1200            uuid = printer.LastJob.ident[3:].split(",")[0]
1201            dn = printer.LastJob.ident
1202        if self.privacy :   
1203            # For legal reasons, we want to hide the title, filename and options
1204            title = filename = options = "hidden"
1205        fields = {
1206                   "objectClass" : ["pykotaObject", "pykotaJob"],
1207                   "cn" : uuid,
1208                   "pykotaUserName" : uname,
1209                   "pykotaPrinterName" : pname,
1210                   "pykotaJobId" : jobid,
1211                   "pykotaPrinterPageCounter" : str(pagecounter),
1212                   "pykotaAction" : action,
1213                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
1214                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
1215                   "pykotaCopies" : str(copies), 
1216                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
1217                   "pykotaHostName" : str(clienthost), 
1218                   "pykotaJobSizeBytes" : str(jobsizebytes),
1219                   "pykotaMD5Sum" : str(jobmd5sum),
1220                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
1221                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
1222                   "pykotaPrecomputedJobSize" : str(precomputedsize),
1223                   "pykotaPrecomputedJobPrice" : str(precomputedprice),
1224                 }
1225        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1226            if jobsize is not None :         
1227                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1228            self.doAdd(dn, fields)
1229        else :   
1230            # here we explicitly want to reset jobsize to 'None' if needed
1231            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1232            self.doModify(dn, fields)
1233           
1234        if printer.LastJob.Exists :
1235            fields = {
1236                       "pykotaLastJobIdent" : uuid,
1237                     }
1238            self.doModify(printer.LastJob.lastjobident, fields)         
1239        else :   
1240            lastjuuid = self.genUUID()
1241            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
1242            fields = {
1243                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
1244                       "cn" : lastjuuid,
1245                       "pykotaPrinterName" : pname,
1246                       "pykotaLastJobIdent" : uuid,
1247                     } 
1248            self.doAdd(lastjdn, fields)         
1249           
1250    def saveUserPQuota(self, userpquota) :
1251        """Saves an user print quota entry."""
1252        fields = { 
1253                   "pykotaSoftLimit" : str(userpquota.SoftLimit),
1254                   "pykotaHardLimit" : str(userpquota.HardLimit),
1255                   "pykotaDateLimit" : str(userpquota.DateLimit),
1256                   "pykotaWarnCount" : str(userpquota.WarnCount or 0),
1257                   "pykotaPageCounter" : str(userpquota.PageCounter or 0),
1258                   "pykotaLifePageCounter" : str(userpquota.LifePageCounter or 0),
1259                   "pykotaMaxJobSize" : str(userpquota.MaxJobSize or 0),
1260                 }
1261        self.doModify(userpquota.ident, fields)
1262       
1263    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1264        """Sets the warn counter value for a user quota."""
1265        fields = { 
1266                   "pykotaWarnCount" : str(warncount or 0),
1267                 }
1268        self.doModify(userpquota.ident, fields)
1269       
1270    def increaseUserPQuotaWarnCount(self, userpquota) :
1271        """Increases the warn counter value for a user quota."""
1272        fields = {
1273                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1274                 }
1275        return self.doModify(userpquota.ident, fields)         
1276       
1277    def saveGroupPQuota(self, grouppquota) :
1278        """Saves a group print quota entry."""
1279        fields = { 
1280                   "pykotaSoftLimit" : str(grouppquota.SoftLimit),
1281                   "pykotaHardLimit" : str(grouppquota.HardLimit),
1282                   "pykotaDateLimit" : str(grouppquota.DateLimit),
1283                   "pykotaMaxJobSize" : str(grouppquota.MaxJobSize or 0),
1284                 }
1285        self.doModify(grouppquota.ident, fields)
1286           
1287    def writePrinterToGroup(self, pgroup, printer) :
1288        """Puts a printer into a printer group."""
1289        if printer.ident not in pgroup.uniqueMember :
1290            pgroup.uniqueMember.append(printer.ident)
1291            fields = {
1292                       "uniqueMember" : pgroup.uniqueMember
1293                     } 
1294            self.doModify(pgroup.ident, fields)         
1295           
1296    def removePrinterFromGroup(self, pgroup, printer) :
1297        """Removes a printer from a printer group."""
1298        try :
1299            pgroup.uniqueMember.remove(printer.ident)
1300        except ValueError :   
1301            pass
1302        else :   
1303            fields = {
1304                       "uniqueMember" : pgroup.uniqueMember,
1305                     } 
1306            self.doModify(pgroup.ident, fields)         
1307           
1308    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, jobid=None, limit=100, start=None, end=None) :
1309        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1310        precond = "(objectClass=pykotaJob)"
1311        where = []
1312        if user is not None :
1313            where.append("(pykotaUserName=%s)" % self.userCharsetToDatabase(user.Name))
1314        if printer is not None :
1315            where.append("(pykotaPrinterName=%s)" % self.userCharsetToDatabase(printer.Name))
1316        if hostname is not None :
1317            where.append("(pykotaHostName=%s)" % hostname)
1318        if billingcode is not None :
1319            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1320        if jobid is not None :
1321            where.append("(pykotaJobId=%s)" % jobid) # TODO : jobid is text, so self.userCharsetToDatabase(jobid) but do all of them as well.
1322        if where :   
1323            where = "(&%s)" % "".join([precond] + where)
1324        else :   
1325            where = precond
1326        jobs = []   
1327        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1328                                               "pykotaHostName", 
1329                                               "pykotaUserName", 
1330                                               "pykotaPrinterName", 
1331                                               "pykotaJobId", 
1332                                               "pykotaPrinterPageCounter", 
1333                                               "pykotaAction", 
1334                                               "pykotaJobSize", 
1335                                               "pykotaJobPrice", 
1336                                               "pykotaFileName", 
1337                                               "pykotaTitle", 
1338                                               "pykotaCopies", 
1339                                               "pykotaOptions", 
1340                                               "pykotaBillingCode", 
1341                                               "pykotaPages", 
1342                                               "pykotaMD5Sum", 
1343                                               "pykotaPrecomputedJobSize",
1344                                               "pykotaPrecomputedJobPrice",
1345                                               "createTimestamp" ], 
1346                                      base=self.info["jobbase"])
1347        if result :
1348            for (ident, fields) in result :
1349                job = StorageJob(self)
1350                job.ident = ident
1351                job.JobId = fields.get("pykotaJobId")[0]
1352                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1353                try :
1354                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1355                except ValueError :   
1356                    job.JobSize = None
1357                try :   
1358                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1359                except ValueError :
1360                    job.JobPrice = None
1361                job.JobAction = fields.get("pykotaAction", [""])[0]
1362                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1363                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1364                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1365                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1366                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1367                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1368                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1369                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1370                job.JobPages = fields.get("pykotaPages", [""])[0]
1371                try :
1372                    job.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
1373                except ValueError :   
1374                    job.PrecomputedJobSize = None
1375                try :   
1376                    job.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
1377                except ValueError :
1378                    job.PrecomputedJobPrice = None
1379                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1380                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1381                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
1382                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
1383                job.JobDate = mxtime.strftime("%Y-%m-%d %H:%M:%S")
1384                if ((start is None) and (end is None)) or \
1385                   ((start is None) and (job.JobDate <= end)) or \
1386                   ((end is None) and (job.JobDate >= start)) or \
1387                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1388                    job.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
1389                    job.PrinterName = self.databaseToUserCharset(fields.get("pykotaPrinterName")[0])
1390                    job.Exists = True
1391                    jobs.append(job)
1392            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1393            if limit :   
1394                jobs = jobs[:int(limit)]
1395        return jobs
1396       
1397    def deleteUser(self, user) :   
1398        """Completely deletes an user from the Quota Storage."""
1399        uname = self.userCharsetToDatabase(user.Name)
1400        todelete = []   
1401        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % uname, base=self.info["jobbase"])
1402        for (ident, fields) in result :
1403            todelete.append(ident)
1404        if self.info["userquotabase"].lower() == "user" :
1405            base = self.info["userbase"]
1406        else :
1407            base = self.info["userquotabase"]
1408        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % uname, \
1409                                  ["pykotaPrinterName", "pykotaUserName"], \
1410                                  base=base)
1411        for (ident, fields) in result :
1412            # ensure the user print quota entry will be deleted
1413            todelete.append(ident)
1414           
1415            # if last job of current printer was printed by the user
1416            # to delete, we also need to delete the printer's last job entry.
1417            printer = self.getPrinter(self.databaseToUserCharset(fields["pykotaPrinterName"][0]))
1418            if printer.LastJob.UserName == user.Name :
1419                todelete.append(printer.LastJob.lastjobident)
1420           
1421        for ident in todelete :   
1422            self.doDelete(ident)
1423           
1424        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1425        if result :
1426            fields = result[0][1]
1427            for k in fields.keys() :
1428                if k.startswith("pykota") :
1429                    del fields[k]
1430                elif k.lower() == "objectclass" :   
1431                    todelete = []
1432                    for i in range(len(fields[k])) :
1433                        if fields[k][i].startswith("pykota") : 
1434                            todelete.append(i)
1435                    todelete.sort()       
1436                    todelete.reverse()
1437                    for i in todelete :
1438                        del fields[k][i]
1439            if fields.get("objectClass") or fields.get("objectclass") :
1440                self.doModify(user.ident, fields, ignoreold=0)       
1441            else :   
1442                self.doDelete(user.ident)
1443        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % \
1444                                   uname, \
1445                                   ["pykotaUserName"], \
1446                                   base=self.info["balancebase"])
1447        for (ident, fields) in result :
1448            self.doDelete(ident)
1449       
1450    def deleteGroup(self, group) :   
1451        """Completely deletes a group from the Quota Storage."""
1452        gname = self.userCharsetToDatabase(group.Name)
1453        if self.info["groupquotabase"].lower() == "group" :
1454            base = self.info["groupbase"]
1455        else :
1456            base = self.info["groupquotabase"]
1457        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % \
1458                                  gname, \
1459                                  ["pykotaGroupName"], \
1460                                  base=base)
1461        for (ident, fields) in result :
1462            self.doDelete(ident)
1463        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1464        if result :
1465            fields = result[0][1]
1466            for k in fields.keys() :
1467                if k.startswith("pykota") :
1468                    del fields[k]
1469                elif k.lower() == "objectclass" :   
1470                    todelete = []
1471                    for i in range(len(fields[k])) :
1472                        if fields[k][i].startswith("pykota") : 
1473                            todelete.append(i)
1474                    todelete.sort()       
1475                    todelete.reverse()
1476                    for i in todelete :
1477                        del fields[k][i]
1478            if fields.get("objectClass") or fields.get("objectclass") :
1479                self.doModify(group.ident, fields, ignoreold=0)       
1480            else :   
1481                self.doDelete(group.ident)
1482               
1483    def deleteManyBillingCodes(self, billingcodes) :
1484        """Deletes many billing codes."""
1485        for bcode in billingcodes :
1486            bcode.delete()
1487       
1488    def deleteManyUsers(self, users) :       
1489        """Deletes many users."""
1490        for user in users :
1491            user.delete()
1492           
1493    def deleteManyGroups(self, groups) :       
1494        """Deletes many groups."""
1495        for group in groups :
1496            group.delete()
1497       
1498    def deleteManyPrinters(self, printers) :       
1499        """Deletes many printers."""
1500        for printer in printers :
1501            printer.delete()
1502       
1503    def deleteManyUserPQuotas(self, printers, users) :       
1504        """Deletes many user print quota entries."""
1505        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1506        for printer in printers :
1507            for user in users :
1508                upq = self.getUserPQuota(user, printer)
1509                if upq.Exists :
1510                    upq.delete()
1511           
1512    def deleteManyGroupPQuotas(self, printers, groups) :
1513        """Deletes many group print quota entries."""
1514        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1515        for printer in printers :
1516            for group in groups :
1517                gpq = self.getGroupPQuota(group, printer)
1518                if gpq.Exists :
1519                    gpq.delete()
1520               
1521    def deleteUserPQuota(self, upquota) :   
1522        """Completely deletes an user print quota entry from the database."""
1523        uname = self.userCharsetToDatabase(upquota.User.Name)
1524        pname = self.userCharsetToDatabase(upquota.Printer.Name)
1525        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s)(pykotaPrinterName=%s))" \
1526                                   % (uname, pname), \
1527                                   base=self.info["jobbase"])
1528        for (ident, fields) in result :
1529            self.doDelete(ident)
1530        if upquota.Printer.LastJob.UserName == upquota.User.Name :
1531            self.doDelete(upquota.Printer.LastJob.lastjobident)
1532        self.doDelete(upquota.ident)
1533       
1534    def deleteGroupPQuota(self, gpquota) :   
1535        """Completely deletes a group print quota entry from the database."""
1536        self.doDelete(gpquota.ident)
1537               
1538    def deletePrinter(self, printer) :   
1539        """Completely deletes a printer from the Quota Storage."""
1540        pname = self.userCharsetToDatabase(printer.Name)
1541        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % pname, base=self.info["lastjobbase"])
1542        for (ident, fields) in result :
1543            self.doDelete(ident)
1544        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % pname, base=self.info["jobbase"])
1545        for (ident, fields) in result :
1546            self.doDelete(ident)
1547        if self.info["groupquotabase"].lower() == "group" :
1548            base = self.info["groupbase"]
1549        else :
1550            base = self.info["groupquotabase"]
1551        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1552        for (ident, fields) in result :
1553            self.doDelete(ident)
1554        if self.info["userquotabase"].lower() == "user" :
1555            base = self.info["userbase"]
1556        else :
1557            base = self.info["userquotabase"]
1558        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1559        for (ident, fields) in result :
1560            self.doDelete(ident)
1561        for parent in self.getParentPrinters(printer) : 
1562            try :
1563                parent.uniqueMember.remove(printer.ident)
1564            except ValueError :   
1565                pass
1566            else :   
1567                fields = {
1568                           "uniqueMember" : parent.uniqueMember,
1569                         } 
1570                self.doModify(parent.ident, fields)         
1571        self.doDelete(printer.ident)   
1572       
1573    def deleteBillingCode(self, code) :
1574        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1575        self.doDelete(code.ident)
1576       
1577    def sortRecords(self, fields, records, default, ordering) :     
1578        """Sort records based on list of fields prefixed with '+' (ASC) or '-' (DESC)."""
1579        fieldindexes = {}
1580        for i in range(len(fields)) :
1581            fieldindexes[fields[i]] = i
1582        if not ordering :   
1583            ordering = default
1584        orderby = []   
1585        for orderkey in ordering :
1586            # Create ordering hints, ignoring unknown fields
1587            if orderkey.startswith("-") :
1588                index = fieldindexes.get(orderkey[1:])
1589                if index is not None :
1590                    orderby.append((-1, index))
1591            elif orderkey.startswith("+") :
1592                index = fieldindexes.get(orderkey[1:])
1593                if index is not None :
1594                    orderby.append((+1, index))
1595            else :   
1596                index = fieldindexes.get(orderkey)
1597                if index is not None :
1598                    orderby.append((+1, index))
1599               
1600        def compare(x, y, orderby=orderby) :   
1601            """Compares two records."""
1602            i = 0
1603            nbkeys = len(orderby)
1604            while i < nbkeys :
1605                (sign, index) = orderby[i]
1606                result = cmp(x[i], y[i])
1607                if not result :
1608                    i += 1
1609                else :   
1610                    return sign * result
1611            return 0 # identical keys       
1612           
1613        records.sort(compare)
1614        return records
1615       
1616    def extractPrinters(self, extractonly={}, ordering=[]) :
1617        """Extracts all printer records."""
1618        pname = extractonly.get("printername")
1619        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1620        if entries :
1621            fields = ("dn", "printername", "priceperpage", "priceperjob", "description", "maxjobsize", "passthrough")
1622            result = []
1623            for entry in entries :
1624                if entry.PassThrough in (1, "1", "t", "true", "T", "TRUE", "True") :
1625                    passthrough = "t"
1626                else :   
1627                    passthrough = "f"
1628                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description, entry.MaxJobSize, passthrough))
1629            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering) 
1630       
1631    def extractUsers(self, extractonly={}, ordering=[]) :
1632        """Extracts all user records."""
1633        uname = extractonly.get("username")
1634        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1635        if entries :
1636            fields = ("dn", "username", "balance", "lifetimepaid", "limitby", "email", "description", "overcharge")
1637            result = []
1638            for entry in entries :
1639                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email, entry.Description, entry.OverCharge))
1640            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1641       
1642    def extractBillingcodes(self, extractonly={}, ordering=[]) :
1643        """Extracts all billing codes records."""
1644        billingcode = extractonly.get("billingcode")
1645        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1646        if entries :
1647            fields = ("dn", "billingcode", "balance", "pagecounter", "description")
1648            result = []
1649            for entry in entries :
1650                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1651            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1652       
1653    def extractGroups(self, extractonly={}, ordering=[]) :
1654        """Extracts all group records."""
1655        gname = extractonly.get("groupname")
1656        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1657        if entries :
1658            fields = ("dn", "groupname", "limitby", "balance", "lifetimepaid", "description")
1659            result = []
1660            for entry in entries :
1661                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid, entry.Description))
1662            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1663       
1664    def extractPayments(self, extractonly={}, ordering=[]) :
1665        """Extracts all payment records."""
1666        startdate = extractonly.get("start")
1667        enddate = extractonly.get("end")
1668        (startdate, enddate) = self.cleanDates(startdate, enddate)
1669        uname = extractonly.get("username")
1670        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1671        if entries :
1672            fields = ("username", "amount", "date", "description")
1673            result = []
1674            for entry in entries :
1675                for (date, amount, description) in entry.Payments :
1676                    if ((startdate is None) and (enddate is None)) or \
1677                       ((startdate is None) and (date <= enddate)) or \
1678                       ((enddate is None) and (date >= startdate)) or \
1679                       ((date >= startdate) and (date <= enddate)) :
1680                        result.append((entry.Name, amount, date, description))
1681            return [fields] + self.sortRecords(fields, result, ["+date"], ordering)
1682       
1683    def extractUpquotas(self, extractonly={}, ordering=[]) :
1684        """Extracts all userpquota records."""
1685        pname = extractonly.get("printername")
1686        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1687        if entries :
1688            fields = ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit")
1689            result = []
1690            uname = extractonly.get("username")
1691            for entry in entries :
1692                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1693                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1694            return [fields] + self.sortRecords(fields, result, ["+userdn"], ordering)
1695       
1696    def extractGpquotas(self, extractonly={}, ordering=[]) :
1697        """Extracts all grouppquota records."""
1698        pname = extractonly.get("printername")
1699        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1700        if entries :
1701            fields = ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit")
1702            result = []
1703            gname = extractonly.get("groupname")
1704            for entry in entries :
1705                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1706                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1707            return [fields] + self.sortRecords(fields, result, ["+groupdn"], ordering)
1708       
1709    def extractUmembers(self, extractonly={}, ordering=[]) :
1710        """Extracts all user groups members."""
1711        gname = extractonly.get("groupname")
1712        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1713        if entries :
1714            fields = ("groupname", "username", "groupdn", "userdn")
1715            result = []
1716            uname = extractonly.get("username")
1717            for entry in entries :
1718                for member in entry.Members :
1719                    if (uname is None) or (member.Name == uname) :
1720                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1721            return [fields] + self.sortRecords(fields, result, ["+groupdn", "+userdn"], ordering)
1722               
1723    def extractPmembers(self, extractonly={}, ordering=[]) :
1724        """Extracts all printer groups members."""
1725        pname = extractonly.get("printername")
1726        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1727        if entries :
1728            fields = ("pgroupname", "printername", "pgroupdn", "printerdn")
1729            result = []
1730            pgname = extractonly.get("pgroupname")
1731            for entry in entries :
1732                for parent in self.getParentPrinters(entry) :
1733                    if (pgname is None) or (parent.Name == pgname) :
1734                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1735            return [fields] + self.sortRecords(fields, result, ["+pgroupdn", "+printerdn"], ordering)
1736       
1737    def extractHistory(self, extractonly={}, ordering=[]) :
1738        """Extracts all jobhistory records."""
1739        uname = extractonly.get("username")
1740        if uname :
1741            user = self.getUser(uname)
1742        else :   
1743            user = None
1744        pname = extractonly.get("printername")
1745        if pname :
1746            printer = self.getPrinter(pname)
1747        else :   
1748            printer = None
1749        startdate = extractonly.get("start")
1750        enddate = extractonly.get("end")
1751        (startdate, enddate) = self.cleanDates(startdate, enddate)
1752        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), jobid=extractonly.get("jobid"), limit=None, start=startdate, end=enddate)
1753        if entries :
1754            fields = ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode", "precomputedjobsize", "precomputedjobprice")
1755            result = []
1756            for entry in entries :
1757                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)) 
1758            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1759           
1760    def getBillingCodeFromBackend(self, label) :
1761        """Extracts billing code information given its label : returns first matching billing code."""
1762        code = StorageBillingCode(self, label)
1763        ulabel = self.userCharsetToDatabase(label)
1764        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % \
1765                                  ulabel, \
1766                                  ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], \
1767                                  base=self.info["billingcodebase"])
1768        if result :
1769            fields = result[0][1]       # take only first matching code, ignore the rest
1770            code.ident = result[0][0]
1771            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1772            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1773            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1774            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1775            code.Exists = True
1776        return code   
1777       
1778    def addBillingCode(self, bcode) :
1779        """Adds a billing code to the quota storage, returns it."""
1780        oldentry = self.getBillingCode(bcode.BillingCode)
1781        if oldentry.Exists :
1782            return oldentry # we return the existing entry
1783        uuid = self.genUUID()
1784        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1785        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1786                   "cn" : uuid,
1787                   "pykotaBillingCode" : self.userCharsetToDatabase(bcode.BillingCode),
1788                   "pykotaPageCounter" : str(bcode.PageCounter or 0),
1789                   "pykotaBalance" : str(bcode.Balance or 0.0),
1790                   "description" : self.userCharsetToDatabase(bcode.Description or ""), 
1791                 } 
1792        self.doAdd(dn, fields)
1793        bcode.isDirty = False
1794        return None # the entry created doesn't need further modification
1795       
1796    def saveBillingCode(self, bcode) :
1797        """Sets the new description for a billing code."""
1798        fields = {
1799                   "description" : self.userCharsetToDatabase(bcode.Description or ""), 
1800                   "pykotaPageCounter" : str(bcode.PageCounter or 0),
1801                   "pykotaBalance" : str(bcode.Balance or 0.0),
1802                 }
1803        self.doModify(bcode.ident, fields)
1804           
1805    def getMatchingBillingCodes(self, billingcodepattern) :
1806        """Returns the list of all billing codes which match a certain pattern."""
1807        codes = []
1808        result = self.doSearch("objectClass=pykotaBilling", \
1809                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1810                                base=self.info["billingcodebase"])
1811        if result :
1812            patterns = billingcodepattern.split(",")
1813            try :
1814                patdict = {}.fromkeys(patterns)
1815            except AttributeError :   
1816                # Python v2.2 or earlier
1817                patdict = {}
1818                for p in patterns :
1819                    patdict[p] = None
1820            for (codeid, fields) in result :
1821                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1822                if patdict.has_key(codename) or self.tool.matchString(codename, patterns) :
1823                    code = StorageBillingCode(self, codename)
1824                    code.ident = codeid
1825                    code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1826                    code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1827                    code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1828                    code.Exists = True
1829                    codes.append(code)
1830                    self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1831        return codes       
1832       
1833    def consumeBillingCode(self, bcode, pagecounter, balance) :
1834        """Consumes from a billing code."""
1835        fields = {
1836                   "pykotaBalance" : { "operator" : "-", "value" : balance, "convert" : float },
1837                   "pykotaPageCounter" : { "operator" : "+", "value" : pagecounter, "convert" : int },
1838                 }
1839        return self.doModify(bcode.ident, fields)         
1840
1841    def refundJob(self, jobident) :   
1842        """Marks a job as refunded in the history."""
1843        dn = "cn=%s,%s" % (ident, self.info["jobbase"])
1844        fields = {
1845                     "pykotaAction" : "REFUND",
1846                 }   
1847        self.doModify(dn, fields)         
1848       
1849    def storageUserFromRecord(self, username, record) :
1850        """Returns a StorageUser instance from a database record."""
1851        user = StorageUser(self, username)
1852        user.Exists = True
1853        return user
1854       
1855    def storageGroupFromRecord(self, groupname, record) :
1856        """Returns a StorageGroup instance from a database record."""
1857        group = StorageGroup(self, groupname)
1858        group.Exists = True
1859        return group
1860       
1861    def storagePrinterFromRecord(self, printername, record) :
1862        """Returns a StoragePrinter instance from a database record."""
1863        printer = StoragePrinter(self, printername)
1864        printer.Exists = True
1865        return printer
1866       
1867    def setJobAttributesFromRecord(self, job, record) :   
1868        """Sets the attributes of a job from a database record."""
1869        job.Exists = True
1870       
1871    def storageJobFromRecord(self, record) :
1872        """Returns a StorageJob instance from a database record."""
1873        job = StorageJob(self)
1874        self.setJobAttributesFromRecord(job, record)
1875        return job
1876       
1877    def storageLastJobFromRecord(self, printer, record) :
1878        """Returns a StorageLastJob instance from a database record."""
1879        lastjob = StorageLastJob(self, printer)
1880        self.setJobAttributesFromRecord(lastjob, record)
1881        return lastjob
1882       
1883    def storageUserPQuotaFromRecord(self, user, printer, record) :
1884        """Returns a StorageUserPQuota instance from a database record."""
1885        userpquota = StorageUserPQuota(self, user, printer)
1886        userpquota.Exists = True
1887        return userpquota
1888       
1889    def storageGroupPQuotaFromRecord(self, group, printer, record) :
1890        """Returns a StorageGroupPQuota instance from a database record."""
1891        grouppquota = StorageGroupPQuota(self, group, printer)
1892        grouppquota.Exists = True
1893        return grouppquota
1894       
1895    def storageBillingCodeFromRecord(self, billingcode, record) :
1896        """Returns a StorageBillingCode instance from a database record."""
1897        code = StorageBillingCode(self, billingcode)
1898        code.Exists = True
1899        return code
Note: See TracBrowser for help on using the browser.