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

Revision 3243, 100.7 kB (checked in by jerome, 17 years ago)

Fixed a problem with old payments and/or payments with no description.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota : Print Quotas for CUPS and LPRng
5#
6# (c) 2003, 2004, 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                            if amount.endswith(" #") :   
783                                amount = amount[:-2] # TODO : should be catched earlier, the bug is above I think
784                            user.Payments.append((date, float(amount), description))
785                    user.Exists = True
786                    users.append(user)
787                    self.cacheEntry("USERS", user.Name, user)
788        return users       
789       
790    def getMatchingGroups(self, grouppattern) :
791        """Returns the list of all groups for which name matches a certain pattern."""
792        groups = []
793        # see comment at the same place in pgstorage.py
794        result = self.doSearch("objectClass=pykotaGroup", \
795                                  ["pykotaGroupName", "pykotaLimitBy", "description"], \
796                                  base=self.info["groupbase"])
797        if result :
798            patterns = grouppattern.split(",")
799            try :
800                patdict = {}.fromkeys(patterns)
801            except AttributeError :   
802                # Python v2.2 or earlier
803                patdict = {}
804                for p in patterns :
805                    patdict[p] = None
806            for (groupid, fields) in result :
807                groupname = self.databaseToUserCharset(fields.get("pykotaGroupName", [""])[0] or fields.get(self.info["grouprdn"], [""])[0])
808                if patdict.has_key(groupname) or self.tool.matchString(groupname, patterns) :
809                    group = StorageGroup(self, groupname)
810                    group.ident = groupid
811                    group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
812                    group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
813                    group.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
814                    group.AccountBalance = 0.0
815                    group.LifeTimePaid = 0.0
816                    for member in self.getGroupMembers(group) :
817                        if member.Exists :
818                            group.AccountBalance += member.AccountBalance
819                            group.LifeTimePaid += member.LifeTimePaid
820                    group.Exists = True
821                    groups.append(group)
822                    self.cacheEntry("GROUPS", group.Name, group)
823        return groups
824       
825    def getPrinterUsersAndQuotas(self, printer, names=["*"]) :       
826        """Returns the list of users who uses a given printer, along with their quotas."""
827        usersandquotas = []
828        pname = self.userCharsetToDatabase(printer.Name)
829        names = [self.userCharsetToDatabase(n) for n in names]
830        if self.info["userquotabase"].lower() == "user" :
831            base = self.info["userbase"]
832        else :
833            base = self.info["userquotabase"]
834        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % \
835                                  (pname, "".join(["(pykotaUserName=%s)" % uname for uname in names])), \
836                                  ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], \
837                                  base=base)
838        if result :
839            for (userquotaid, fields) in result :
840                user = self.getUser(self.databaseToUserCharset(fields.get("pykotaUserName")[0]))
841                userpquota = StorageUserPQuota(self, user, printer)
842                userpquota.ident = userquotaid
843                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
844                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
845                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
846                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
847                if userpquota.SoftLimit is not None :
848                    if userpquota.SoftLimit[0].upper() == "NONE" :
849                        userpquota.SoftLimit = None
850                    else :   
851                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
852                userpquota.HardLimit = fields.get("pykotaHardLimit")
853                if userpquota.HardLimit is not None :
854                    if userpquota.HardLimit[0].upper() == "NONE" :
855                        userpquota.HardLimit = None
856                    elif userpquota.HardLimit is not None :   
857                        userpquota.HardLimit = int(userpquota.HardLimit[0])
858                userpquota.DateLimit = fields.get("pykotaDateLimit")
859                if userpquota.DateLimit is not None :
860                    if userpquota.DateLimit[0].upper() == "NONE" : 
861                        userpquota.DateLimit = None
862                    else :   
863                        userpquota.DateLimit = userpquota.DateLimit[0]
864                userpquota.Exists = True
865                usersandquotas.append((user, userpquota))
866                self.cacheEntry("USERPQUOTAS", "%s@%s" % (user.Name, printer.Name), userpquota)
867        usersandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
868        return usersandquotas
869               
870    def getPrinterGroupsAndQuotas(self, printer, names=["*"]) :       
871        """Returns the list of groups which uses a given printer, along with their quotas."""
872        groupsandquotas = []
873        pname = self.userCharsetToDatabase(printer.Name)
874        names = [self.userCharsetToDatabase(n) for n in names]
875        if self.info["groupquotabase"].lower() == "group" :
876            base = self.info["groupbase"]
877        else :
878            base = self.info["groupquotabase"]
879        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s)(|%s))" % \
880                                  (pname, "".join(["(pykotaGroupName=%s)" % gname for gname in names])), \
881                                  ["pykotaGroupName"], \
882                                  base=base)
883        if result :
884            for (groupquotaid, fields) in result :
885                group = self.getGroup(self.databaseToUserCharset(fields.get("pykotaGroupName")[0]))
886                grouppquota = self.getGroupPQuota(group, printer)
887                groupsandquotas.append((group, grouppquota))
888        groupsandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
889        return groupsandquotas
890       
891    def addPrinter(self, printer) :
892        """Adds a printer to the quota storage, returns the old value if it already exists."""
893        oldentry = self.getPrinter(printer.Name)
894        if oldentry.Exists :
895            return oldentry # we return the existing entry
896        printername = self.userCharsetToDatabase(printer.Name)
897        fields = { self.info["printerrdn"] : printername,
898                   "objectClass" : ["pykotaObject", "pykotaPrinter"],
899                   "cn" : printername,
900                   "pykotaPrinterName" : printername,
901                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
902                   "pykotaMaxJobSize" : str(printer.MaxJobSize or 0),
903                   "description" : self.userCharsetToDatabase(printer.Description or ""),
904                   "pykotaPricePerPage" : str(printer.PricePerPage or 0.0),
905                   "pykotaPricePerJob" : str(printer.PricePerJob or 0.0),
906                 } 
907        dn = "%s=%s,%s" % (self.info["printerrdn"], printername, self.info["printerbase"])
908        self.doAdd(dn, fields)
909        printer.isDirty = False
910        return None # the entry created doesn't need further modification
911       
912    def addUser(self, user) :       
913        """Adds a user to the quota storage, returns the old value if it already exists."""
914        oldentry = self.getUser(user.Name)
915        if oldentry.Exists :
916            return oldentry # we return the existing entry
917        uname = self.userCharsetToDatabase(user.Name)
918        newfields = {
919                       "pykotaUserName" : uname,
920                       "pykotaLimitBy" : (user.LimitBy or "quota"),
921                       "description" : self.userCharsetToDatabase(user.Description or ""),
922                       self.info["usermail"] : user.Email or "",
923                    }   
924                       
925        mustadd = 1
926        if self.info["newuser"].lower() != 'below' :
927            try :
928                (where, action) = [s.strip() for s in self.info["newuser"].split(",")]
929            except ValueError :
930                (where, action) = (self.info["newuser"].strip(), "fail")
931            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
932                                      (where, self.info["userrdn"], uname), \
933                                      None, \
934                                      base=self.info["userbase"])
935            if result :
936                (dn, fields) = result[0]
937                oc = fields.get("objectClass", fields.get("objectclass", []))
938                oc.extend(["pykotaAccount", "pykotaAccountBalance"])
939                fields.update(newfields)
940                fields.update({ "pykotaBalance" : str(user.AccountBalance or 0.0),
941                                "pykotaOverCharge" : str(user.OverCharge),
942                                "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), })   
943                self.doModify(dn, fields)
944                mustadd = 0
945            else :
946                message = _("Unable to find an existing objectClass %s entry with %s=%s to attach pykotaAccount objectClass") % (where, self.info["userrdn"], user.Name)
947                if action.lower() == "warn" :   
948                    self.tool.printInfo(_("%s. A new entry will be created instead.") % message, "warn")
949                else : # 'fail' or incorrect setting
950                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
951               
952        if mustadd :
953            if self.info["userbase"] == self.info["balancebase"] :           
954                fields = { self.info["userrdn"] : uname,
955                           "objectClass" : ["pykotaObject", "pykotaAccount", "pykotaAccountBalance"],
956                           "cn" : uname,
957                           "pykotaBalance" : str(user.AccountBalance or 0.0),
958                           "pykotaOverCharge" : str(user.OverCharge),
959                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
960                         } 
961            else :             
962                fields = { self.info["userrdn"] : uname,
963                           "objectClass" : ["pykotaObject", "pykotaAccount"],
964                           "cn" : uname,
965                         } 
966            fields.update(newfields)         
967            dn = "%s=%s,%s" % (self.info["userrdn"], uname, self.info["userbase"])
968            self.doAdd(dn, fields)
969            if self.info["userbase"] != self.info["balancebase"] :           
970                fields = { self.info["balancerdn"] : uname,
971                           "objectClass" : ["pykotaObject", "pykotaAccountBalance"],
972                           "cn" : uname,
973                           "pykotaBalance" : str(user.AccountBalance or 0.0),
974                           "pykotaOverCharge" : str(user.OverCharge),
975                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
976                         } 
977                dn = "%s=%s,%s" % (self.info["balancerdn"], uname, self.info["balancebase"])
978                self.doAdd(dn, fields)
979        user.idbalance = dn
980        if user.PaymentsBacklog :
981            for (value, comment) in user.PaymentsBacklog :
982                self.writeNewPayment(user, value, comment)
983            user.PaymentsBacklog = []
984        user.isDirty = False
985        return None # the entry created doesn't need further modification
986       
987    def addGroup(self, group) :       
988        """Adds a group to the quota storage, returns the old value if it already exists."""
989        oldentry = self.getGroup(group.Name)
990        if oldentry.Exists :
991            return oldentry # we return the existing entry
992        gname = self.userCharsetToDatabase(group.Name)
993        newfields = { 
994                      "pykotaGroupName" : gname,
995                      "pykotaLimitBy" : (group.LimitBy or "quota"),
996                      "description" : self.userCharsetToDatabase(group.Description or "")
997                    } 
998        mustadd = 1
999        if self.info["newgroup"].lower() != 'below' :
1000            try :
1001                (where, action) = [s.strip() for s in self.info["newgroup"].split(",")]
1002            except ValueError :
1003                (where, action) = (self.info["newgroup"].strip(), "fail")
1004            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
1005                                      (where, self.info["grouprdn"], gname), \
1006                                      None, \
1007                                      base=self.info["groupbase"])
1008            if result :
1009                (dn, fields) = result[0]
1010                oc = fields.get("objectClass", fields.get("objectclass", []))
1011                oc.extend(["pykotaGroup"])
1012                fields.update(newfields)
1013                self.doModify(dn, fields)
1014                mustadd = 0
1015            else :
1016                message = _("Unable to find an existing entry to attach pykotaGroup objectclass %s") % group.Name
1017                if action.lower() == "warn" :   
1018                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
1019                else : # 'fail' or incorrect setting
1020                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
1021               
1022        if mustadd :
1023            fields = { self.info["grouprdn"] : gname,
1024                       "objectClass" : ["pykotaObject", "pykotaGroup"],
1025                       "cn" : gname,
1026                     } 
1027            fields.update(newfields)         
1028            dn = "%s=%s,%s" % (self.info["grouprdn"], gname, self.info["groupbase"])
1029            self.doAdd(dn, fields)
1030        group.isDirty = False
1031        return None # the entry created doesn't need further modification
1032       
1033    def addUserToGroup(self, user, group) :   
1034        """Adds an user to a group."""
1035        if user.Name not in [u.Name for u in self.getGroupMembers(group)] :
1036            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1037            if result :
1038                fields = result[0][1]
1039                if not fields.has_key(self.info["groupmembers"]) :
1040                    fields[self.info["groupmembers"]] = []
1041                fields[self.info["groupmembers"]].append(self.userCharsetToDatabase(user.Name))
1042                self.doModify(group.ident, fields)
1043                group.Members.append(user)
1044               
1045    def delUserFromGroup(self, user, group) :   
1046        """Removes an user from a group."""
1047        if user.Name in [u.Name for u in self.getGroupMembers(group)] :
1048            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)
1049            if result :
1050                fields = result[0][1]
1051                if not fields.has_key(self.info["groupmembers"]) :
1052                    fields[self.info["groupmembers"]] = []
1053                try :   
1054                    fields[self.info["groupmembers"]].remove(self.userCharsetToDatabase(user.Name))
1055                except ValueError :
1056                    pass # TODO : Strange, shouldn't it be there ?
1057                else :
1058                    self.doModify(group.ident, fields)
1059                    group.Members.remove(user)
1060               
1061    def addUserPQuota(self, upq) :
1062        """Initializes a user print quota on a printer."""
1063        # first check if an entry already exists
1064        oldentry = self.getUserPQuota(upq.User, upq.Printer)
1065        if oldentry.Exists :
1066            return oldentry # we return the existing entry
1067        uuid = self.genUUID()
1068        uname = self.userCharsetToDatabase(upq.User.Name)
1069        pname = self.userCharsetToDatabase(upq.Printer.Name)
1070        fields = { "cn" : uuid,
1071                   "objectClass" : ["pykotaObject", "pykotaUserPQuota"],
1072                   "pykotaUserName" : uname,
1073                   "pykotaPrinterName" : pname,
1074                   "pykotaSoftLimit" : str(upq.SoftLimit),
1075                   "pykotaHardLimit" : str(upq.HardLimit),
1076                   "pykotaDateLimit" : str(upq.DateLimit),
1077                   "pykotaPageCounter" : str(upq.PageCounter or 0),
1078                   "pykotaLifePageCounter" : str(upq.LifePageCounter or 0),
1079                   "pykotaWarnCount" : str(upq.WarnCount or 0),
1080                   "pykotaMaxJobSize" : str(upq.MaxJobSize or 0),
1081                 } 
1082        if self.info["userquotabase"].lower() == "user" :
1083            dn = "cn=%s,%s" % (uuid, upq.User.ident)
1084        else :   
1085            dn = "cn=%s,%s" % (uuid, self.info["userquotabase"])
1086        self.doAdd(dn, fields)
1087        upq.isDirty = False
1088        return None # the entry created doesn't need further modification
1089       
1090    def addGroupPQuota(self, gpq) :
1091        """Initializes a group print quota on a printer."""
1092        oldentry = self.getGroupPQuota(gpq.Group, gpq.Printer)
1093        if oldentry.Exists :
1094            return oldentry # we return the existing entry
1095        uuid = self.genUUID()
1096        gname = self.userCharsetToDatabase(gpq.Group.Name)
1097        pname = self.userCharsetToDatabase(gpq.Printer.Name)
1098        fields = { "cn" : uuid,
1099                   "objectClass" : ["pykotaObject", "pykotaGroupPQuota"],
1100                   "pykotaGroupName" : gname,
1101                   "pykotaPrinterName" : pname,
1102                   "pykotaDateLimit" : "None",
1103                 } 
1104        if self.info["groupquotabase"].lower() == "group" :
1105            dn = "cn=%s,%s" % (uuid, gpq.Group.ident)
1106        else :   
1107            dn = "cn=%s,%s" % (uuid, self.info["groupquotabase"])
1108        self.doAdd(dn, fields)
1109        gpq.isDirty = False
1110        return None # the entry created doesn't need further modification
1111       
1112    def savePrinter(self, printer) :   
1113        """Saves the printer to the database in a single operation."""
1114        fields = {
1115                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
1116                   "pykotaMaxJobSize" : str(printer.MaxJobSize or 0),
1117                   "description" : self.userCharsetToDatabase(printer.Description or ""),
1118                   "pykotaPricePerPage" : str(printer.PricePerPage or 0.0),
1119                   "pykotaPricePerJob" : str(printer.PricePerJob or 0.0),
1120                 }
1121        self.doModify(printer.ident, fields)
1122       
1123    def saveUser(self, user) :
1124        """Saves the user to the database in a single operation."""
1125        newfields = {
1126                       "pykotaLimitBy" : (user.LimitBy or "quota"),
1127                       "description" : self.userCharsetToDatabase(user.Description or ""), 
1128                       self.info["usermail"] : user.Email or "",
1129                    }   
1130        self.doModify(user.ident, newfields)
1131       
1132        newfields = { "pykotaBalance" : str(user.AccountBalance or 0.0),
1133                      "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
1134                      "pykotaOverCharge" : str(user.OverCharge),
1135                    }
1136        self.doModify(user.idbalance, newfields)
1137       
1138    def saveGroup(self, group) :
1139        """Saves the group to the database in a single operation."""
1140        newfields = {
1141                       "pykotaLimitBy" : (group.LimitBy or "quota"),
1142                       "description" : self.userCharsetToDatabase(group.Description or ""), 
1143                    }   
1144        self.doModify(group.ident, newfields)
1145       
1146    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
1147        """Sets the date limit permanently for a user print quota."""
1148        fields = {
1149                   "pykotaDateLimit" : str(datelimit),
1150                 }
1151        return self.doModify(userpquota.ident, fields)
1152           
1153    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
1154        """Sets the date limit permanently for a group print quota."""
1155        fields = {
1156                   "pykotaDateLimit" : str(datelimit),
1157                 }
1158        return self.doModify(grouppquota.ident, fields)
1159       
1160    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
1161        """Increase page counters for a user print quota."""
1162        fields = {
1163                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1164                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1165                 }
1166        return self.doModify(userpquota.ident, fields)         
1167       
1168    def decreaseUserAccountBalance(self, user, amount) :   
1169        """Decreases user's account balance from an amount."""
1170        fields = {
1171                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
1172                 }
1173        return self.doModify(user.idbalance, fields, flushcache=1)         
1174       
1175    def writeNewPayment(self, user, amount, comment="") :
1176        """Adds a new payment to the payments history."""
1177        payments = []
1178        for payment in user.Payments :
1179            payments.append("%s # %s # %s" % (payment[0], str(payment[1]), base64.encodestring(self.userCharsetToDatabase(payment[2])).strip()))
1180        payments.append("%s # %s # %s" % (str(DateTime.now()), str(amount), base64.encodestring(self.userCharsetToDatabase(comment)).strip()))
1181        fields = {
1182                   "pykotaPayments" : payments,
1183                 }
1184        return self.doModify(user.idbalance, fields)         
1185       
1186    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
1187        """Sets the last job's size permanently."""
1188        fields = {
1189                   "pykotaJobSize" : str(jobsize),
1190                   "pykotaJobPrice" : str(jobprice),
1191                 }
1192        self.doModify(lastjob.ident, fields)         
1193       
1194    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) :
1195        """Adds a job in a printer's history."""
1196        uname = self.userCharsetToDatabase(user.Name)
1197        pname = self.userCharsetToDatabase(printer.Name)
1198        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1199            uuid = self.genUUID()
1200            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
1201        else :   
1202            uuid = printer.LastJob.ident[3:].split(",")[0]
1203            dn = printer.LastJob.ident
1204        if self.privacy :   
1205            # For legal reasons, we want to hide the title, filename and options
1206            title = filename = options = "hidden"
1207        fields = {
1208                   "objectClass" : ["pykotaObject", "pykotaJob"],
1209                   "cn" : uuid,
1210                   "pykotaUserName" : uname,
1211                   "pykotaPrinterName" : pname,
1212                   "pykotaJobId" : jobid,
1213                   "pykotaPrinterPageCounter" : str(pagecounter),
1214                   "pykotaAction" : action,
1215                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
1216                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
1217                   "pykotaCopies" : str(copies), 
1218                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
1219                   "pykotaHostName" : str(clienthost), 
1220                   "pykotaJobSizeBytes" : str(jobsizebytes),
1221                   "pykotaMD5Sum" : str(jobmd5sum),
1222                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
1223                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
1224                   "pykotaPrecomputedJobSize" : str(precomputedsize),
1225                   "pykotaPrecomputedJobPrice" : str(precomputedprice),
1226                 }
1227        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1228            if jobsize is not None :         
1229                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1230            self.doAdd(dn, fields)
1231        else :   
1232            # here we explicitly want to reset jobsize to 'None' if needed
1233            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1234            self.doModify(dn, fields)
1235           
1236        if printer.LastJob.Exists :
1237            fields = {
1238                       "pykotaLastJobIdent" : uuid,
1239                     }
1240            self.doModify(printer.LastJob.lastjobident, fields)         
1241        else :   
1242            lastjuuid = self.genUUID()
1243            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
1244            fields = {
1245                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
1246                       "cn" : lastjuuid,
1247                       "pykotaPrinterName" : pname,
1248                       "pykotaLastJobIdent" : uuid,
1249                     } 
1250            self.doAdd(lastjdn, fields)         
1251           
1252    def saveUserPQuota(self, userpquota) :
1253        """Saves an user print quota entry."""
1254        fields = { 
1255                   "pykotaSoftLimit" : str(userpquota.SoftLimit),
1256                   "pykotaHardLimit" : str(userpquota.HardLimit),
1257                   "pykotaDateLimit" : str(userpquota.DateLimit),
1258                   "pykotaWarnCount" : str(userpquota.WarnCount or 0),
1259                   "pykotaPageCounter" : str(userpquota.PageCounter or 0),
1260                   "pykotaLifePageCounter" : str(userpquota.LifePageCounter or 0),
1261                   "pykotaMaxJobSize" : str(userpquota.MaxJobSize or 0),
1262                 }
1263        self.doModify(userpquota.ident, fields)
1264       
1265    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1266        """Sets the warn counter value for a user quota."""
1267        fields = { 
1268                   "pykotaWarnCount" : str(warncount or 0),
1269                 }
1270        self.doModify(userpquota.ident, fields)
1271       
1272    def increaseUserPQuotaWarnCount(self, userpquota) :
1273        """Increases the warn counter value for a user quota."""
1274        fields = {
1275                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1276                 }
1277        return self.doModify(userpquota.ident, fields)         
1278       
1279    def saveGroupPQuota(self, grouppquota) :
1280        """Saves a group print quota entry."""
1281        fields = { 
1282                   "pykotaSoftLimit" : str(grouppquota.SoftLimit),
1283                   "pykotaHardLimit" : str(grouppquota.HardLimit),
1284                   "pykotaDateLimit" : str(grouppquota.DateLimit),
1285                   "pykotaMaxJobSize" : str(grouppquota.MaxJobSize or 0),
1286                 }
1287        self.doModify(grouppquota.ident, fields)
1288           
1289    def writePrinterToGroup(self, pgroup, printer) :
1290        """Puts a printer into a printer group."""
1291        if printer.ident not in pgroup.uniqueMember :
1292            pgroup.uniqueMember.append(printer.ident)
1293            fields = {
1294                       "uniqueMember" : pgroup.uniqueMember
1295                     } 
1296            self.doModify(pgroup.ident, fields)         
1297           
1298    def removePrinterFromGroup(self, pgroup, printer) :
1299        """Removes a printer from a printer group."""
1300        try :
1301            pgroup.uniqueMember.remove(printer.ident)
1302        except ValueError :   
1303            pass
1304        else :   
1305            fields = {
1306                       "uniqueMember" : pgroup.uniqueMember,
1307                     } 
1308            self.doModify(pgroup.ident, fields)         
1309           
1310    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, jobid=None, limit=100, start=None, end=None) :
1311        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1312        precond = "(objectClass=pykotaJob)"
1313        where = []
1314        if user is not None :
1315            where.append("(pykotaUserName=%s)" % self.userCharsetToDatabase(user.Name))
1316        if printer is not None :
1317            where.append("(pykotaPrinterName=%s)" % self.userCharsetToDatabase(printer.Name))
1318        if hostname is not None :
1319            where.append("(pykotaHostName=%s)" % hostname)
1320        if billingcode is not None :
1321            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1322        if jobid is not None :
1323            where.append("(pykotaJobId=%s)" % jobid) # TODO : jobid is text, so self.userCharsetToDatabase(jobid) but do all of them as well.
1324        if where :   
1325            where = "(&%s)" % "".join([precond] + where)
1326        else :   
1327            where = precond
1328        jobs = []   
1329        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1330                                               "pykotaHostName", 
1331                                               "pykotaUserName", 
1332                                               "pykotaPrinterName", 
1333                                               "pykotaJobId", 
1334                                               "pykotaPrinterPageCounter", 
1335                                               "pykotaAction", 
1336                                               "pykotaJobSize", 
1337                                               "pykotaJobPrice", 
1338                                               "pykotaFileName", 
1339                                               "pykotaTitle", 
1340                                               "pykotaCopies", 
1341                                               "pykotaOptions", 
1342                                               "pykotaBillingCode", 
1343                                               "pykotaPages", 
1344                                               "pykotaMD5Sum", 
1345                                               "pykotaPrecomputedJobSize",
1346                                               "pykotaPrecomputedJobPrice",
1347                                               "createTimestamp" ], 
1348                                      base=self.info["jobbase"])
1349        if result :
1350            for (ident, fields) in result :
1351                job = StorageJob(self)
1352                job.ident = ident
1353                job.JobId = fields.get("pykotaJobId")[0]
1354                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1355                try :
1356                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1357                except ValueError :   
1358                    job.JobSize = None
1359                try :   
1360                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1361                except ValueError :
1362                    job.JobPrice = None
1363                job.JobAction = fields.get("pykotaAction", [""])[0]
1364                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1365                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1366                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1367                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1368                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1369                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1370                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1371                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1372                job.JobPages = fields.get("pykotaPages", [""])[0]
1373                try :
1374                    job.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
1375                except ValueError :   
1376                    job.PrecomputedJobSize = None
1377                try :   
1378                    job.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
1379                except ValueError :
1380                    job.PrecomputedJobPrice = None
1381                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1382                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1383                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
1384                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
1385                job.JobDate = mxtime.strftime("%Y-%m-%d %H:%M:%S")
1386                if ((start is None) and (end is None)) or \
1387                   ((start is None) and (job.JobDate <= end)) or \
1388                   ((end is None) and (job.JobDate >= start)) or \
1389                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1390                    job.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
1391                    job.PrinterName = self.databaseToUserCharset(fields.get("pykotaPrinterName")[0])
1392                    job.Exists = True
1393                    jobs.append(job)
1394            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1395            if limit :   
1396                jobs = jobs[:int(limit)]
1397        return jobs
1398       
1399    def deleteUser(self, user) :   
1400        """Completely deletes an user from the Quota Storage."""
1401        uname = self.userCharsetToDatabase(user.Name)
1402        todelete = []   
1403        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % uname, base=self.info["jobbase"])
1404        for (ident, fields) in result :
1405            todelete.append(ident)
1406        if self.info["userquotabase"].lower() == "user" :
1407            base = self.info["userbase"]
1408        else :
1409            base = self.info["userquotabase"]
1410        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % uname, \
1411                                  ["pykotaPrinterName", "pykotaUserName"], \
1412                                  base=base)
1413        for (ident, fields) in result :
1414            # ensure the user print quota entry will be deleted
1415            todelete.append(ident)
1416           
1417            # if last job of current printer was printed by the user
1418            # to delete, we also need to delete the printer's last job entry.
1419            printer = self.getPrinter(self.databaseToUserCharset(fields["pykotaPrinterName"][0]))
1420            if printer.LastJob.UserName == user.Name :
1421                todelete.append(printer.LastJob.lastjobident)
1422           
1423        for ident in todelete :   
1424            self.doDelete(ident)
1425           
1426        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1427        if result :
1428            fields = result[0][1]
1429            for k in fields.keys() :
1430                if k.startswith("pykota") :
1431                    del fields[k]
1432                elif k.lower() == "objectclass" :   
1433                    todelete = []
1434                    for i in range(len(fields[k])) :
1435                        if fields[k][i].startswith("pykota") : 
1436                            todelete.append(i)
1437                    todelete.sort()       
1438                    todelete.reverse()
1439                    for i in todelete :
1440                        del fields[k][i]
1441            if fields.get("objectClass") or fields.get("objectclass") :
1442                self.doModify(user.ident, fields, ignoreold=0)       
1443            else :   
1444                self.doDelete(user.ident)
1445        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % \
1446                                   uname, \
1447                                   ["pykotaUserName"], \
1448                                   base=self.info["balancebase"])
1449        for (ident, fields) in result :
1450            self.doDelete(ident)
1451       
1452    def deleteGroup(self, group) :   
1453        """Completely deletes a group from the Quota Storage."""
1454        gname = self.userCharsetToDatabase(group.Name)
1455        if self.info["groupquotabase"].lower() == "group" :
1456            base = self.info["groupbase"]
1457        else :
1458            base = self.info["groupquotabase"]
1459        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % \
1460                                  gname, \
1461                                  ["pykotaGroupName"], \
1462                                  base=base)
1463        for (ident, fields) in result :
1464            self.doDelete(ident)
1465        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1466        if result :
1467            fields = result[0][1]
1468            for k in fields.keys() :
1469                if k.startswith("pykota") :
1470                    del fields[k]
1471                elif k.lower() == "objectclass" :   
1472                    todelete = []
1473                    for i in range(len(fields[k])) :
1474                        if fields[k][i].startswith("pykota") : 
1475                            todelete.append(i)
1476                    todelete.sort()       
1477                    todelete.reverse()
1478                    for i in todelete :
1479                        del fields[k][i]
1480            if fields.get("objectClass") or fields.get("objectclass") :
1481                self.doModify(group.ident, fields, ignoreold=0)       
1482            else :   
1483                self.doDelete(group.ident)
1484               
1485    def deleteManyBillingCodes(self, billingcodes) :
1486        """Deletes many billing codes."""
1487        for bcode in billingcodes :
1488            bcode.delete()
1489       
1490    def deleteManyUsers(self, users) :       
1491        """Deletes many users."""
1492        for user in users :
1493            user.delete()
1494           
1495    def deleteManyGroups(self, groups) :       
1496        """Deletes many groups."""
1497        for group in groups :
1498            group.delete()
1499       
1500    def deleteManyPrinters(self, printers) :       
1501        """Deletes many printers."""
1502        for printer in printers :
1503            printer.delete()
1504       
1505    def deleteManyUserPQuotas(self, printers, users) :       
1506        """Deletes many user print quota entries."""
1507        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1508        for printer in printers :
1509            for user in users :
1510                upq = self.getUserPQuota(user, printer)
1511                if upq.Exists :
1512                    upq.delete()
1513           
1514    def deleteManyGroupPQuotas(self, printers, groups) :
1515        """Deletes many group print quota entries."""
1516        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1517        for printer in printers :
1518            for group in groups :
1519                gpq = self.getGroupPQuota(group, printer)
1520                if gpq.Exists :
1521                    gpq.delete()
1522               
1523    def deleteUserPQuota(self, upquota) :   
1524        """Completely deletes an user print quota entry from the database."""
1525        uname = self.userCharsetToDatabase(upquota.User.Name)
1526        pname = self.userCharsetToDatabase(upquota.Printer.Name)
1527        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s)(pykotaPrinterName=%s))" \
1528                                   % (uname, pname), \
1529                                   base=self.info["jobbase"])
1530        for (ident, fields) in result :
1531            self.doDelete(ident)
1532        if upquota.Printer.LastJob.UserName == upquota.User.Name :
1533            self.doDelete(upquota.Printer.LastJob.lastjobident)
1534        self.doDelete(upquota.ident)
1535       
1536    def deleteGroupPQuota(self, gpquota) :   
1537        """Completely deletes a group print quota entry from the database."""
1538        self.doDelete(gpquota.ident)
1539               
1540    def deletePrinter(self, printer) :   
1541        """Completely deletes a printer from the Quota Storage."""
1542        pname = self.userCharsetToDatabase(printer.Name)
1543        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % pname, base=self.info["lastjobbase"])
1544        for (ident, fields) in result :
1545            self.doDelete(ident)
1546        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % pname, base=self.info["jobbase"])
1547        for (ident, fields) in result :
1548            self.doDelete(ident)
1549        if self.info["groupquotabase"].lower() == "group" :
1550            base = self.info["groupbase"]
1551        else :
1552            base = self.info["groupquotabase"]
1553        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1554        for (ident, fields) in result :
1555            self.doDelete(ident)
1556        if self.info["userquotabase"].lower() == "user" :
1557            base = self.info["userbase"]
1558        else :
1559            base = self.info["userquotabase"]
1560        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1561        for (ident, fields) in result :
1562            self.doDelete(ident)
1563        for parent in self.getParentPrinters(printer) : 
1564            try :
1565                parent.uniqueMember.remove(printer.ident)
1566            except ValueError :   
1567                pass
1568            else :   
1569                fields = {
1570                           "uniqueMember" : parent.uniqueMember,
1571                         } 
1572                self.doModify(parent.ident, fields)         
1573        self.doDelete(printer.ident)   
1574       
1575    def deleteBillingCode(self, code) :
1576        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1577        self.doDelete(code.ident)
1578       
1579    def sortRecords(self, fields, records, default, ordering) :     
1580        """Sort records based on list of fields prefixed with '+' (ASC) or '-' (DESC)."""
1581        fieldindexes = {}
1582        for i in range(len(fields)) :
1583            fieldindexes[fields[i]] = i
1584        if not ordering :   
1585            ordering = default
1586        orderby = []   
1587        for orderkey in ordering :
1588            # Create ordering hints, ignoring unknown fields
1589            if orderkey.startswith("-") :
1590                index = fieldindexes.get(orderkey[1:])
1591                if index is not None :
1592                    orderby.append((-1, index))
1593            elif orderkey.startswith("+") :
1594                index = fieldindexes.get(orderkey[1:])
1595                if index is not None :
1596                    orderby.append((+1, index))
1597            else :   
1598                index = fieldindexes.get(orderkey)
1599                if index is not None :
1600                    orderby.append((+1, index))
1601               
1602        def compare(x, y, orderby=orderby) :   
1603            """Compares two records."""
1604            i = 0
1605            nbkeys = len(orderby)
1606            while i < nbkeys :
1607                (sign, index) = orderby[i]
1608                result = cmp(x[i], y[i])
1609                if not result :
1610                    i += 1
1611                else :   
1612                    return sign * result
1613            return 0 # identical keys       
1614           
1615        records.sort(compare)
1616        return records
1617       
1618    def extractPrinters(self, extractonly={}, ordering=[]) :
1619        """Extracts all printer records."""
1620        pname = extractonly.get("printername")
1621        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1622        if entries :
1623            fields = ("dn", "printername", "priceperpage", "priceperjob", "description", "maxjobsize", "passthrough")
1624            result = []
1625            for entry in entries :
1626                if entry.PassThrough in (1, "1", "t", "true", "T", "TRUE", "True") :
1627                    passthrough = "t"
1628                else :   
1629                    passthrough = "f"
1630                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description, entry.MaxJobSize, passthrough))
1631            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering) 
1632       
1633    def extractUsers(self, extractonly={}, ordering=[]) :
1634        """Extracts all user records."""
1635        uname = extractonly.get("username")
1636        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1637        if entries :
1638            fields = ("dn", "username", "balance", "lifetimepaid", "limitby", "email", "description", "overcharge")
1639            result = []
1640            for entry in entries :
1641                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email, entry.Description, entry.OverCharge))
1642            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1643       
1644    def extractBillingcodes(self, extractonly={}, ordering=[]) :
1645        """Extracts all billing codes records."""
1646        billingcode = extractonly.get("billingcode")
1647        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1648        if entries :
1649            fields = ("dn", "billingcode", "balance", "pagecounter", "description")
1650            result = []
1651            for entry in entries :
1652                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1653            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1654       
1655    def extractGroups(self, extractonly={}, ordering=[]) :
1656        """Extracts all group records."""
1657        gname = extractonly.get("groupname")
1658        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1659        if entries :
1660            fields = ("dn", "groupname", "limitby", "balance", "lifetimepaid", "description")
1661            result = []
1662            for entry in entries :
1663                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid, entry.Description))
1664            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1665       
1666    def extractPayments(self, extractonly={}, ordering=[]) :
1667        """Extracts all payment records."""
1668        startdate = extractonly.get("start")
1669        enddate = extractonly.get("end")
1670        (startdate, enddate) = self.cleanDates(startdate, enddate)
1671        uname = extractonly.get("username")
1672        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1673        if entries :
1674            fields = ("username", "amount", "date", "description")
1675            result = []
1676            for entry in entries :
1677                for (date, amount, description) in entry.Payments :
1678                    if ((startdate is None) and (enddate is None)) or \
1679                       ((startdate is None) and (date <= enddate)) or \
1680                       ((enddate is None) and (date >= startdate)) or \
1681                       ((date >= startdate) and (date <= enddate)) :
1682                        result.append((entry.Name, amount, date, description))
1683            return [fields] + self.sortRecords(fields, result, ["+date"], ordering)
1684       
1685    def extractUpquotas(self, extractonly={}, ordering=[]) :
1686        """Extracts all userpquota records."""
1687        pname = extractonly.get("printername")
1688        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1689        if entries :
1690            fields = ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit")
1691            result = []
1692            uname = extractonly.get("username")
1693            for entry in entries :
1694                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1695                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1696            return [fields] + self.sortRecords(fields, result, ["+userdn"], ordering)
1697       
1698    def extractGpquotas(self, extractonly={}, ordering=[]) :
1699        """Extracts all grouppquota records."""
1700        pname = extractonly.get("printername")
1701        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1702        if entries :
1703            fields = ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit")
1704            result = []
1705            gname = extractonly.get("groupname")
1706            for entry in entries :
1707                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1708                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1709            return [fields] + self.sortRecords(fields, result, ["+groupdn"], ordering)
1710       
1711    def extractUmembers(self, extractonly={}, ordering=[]) :
1712        """Extracts all user groups members."""
1713        gname = extractonly.get("groupname")
1714        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1715        if entries :
1716            fields = ("groupname", "username", "groupdn", "userdn")
1717            result = []
1718            uname = extractonly.get("username")
1719            for entry in entries :
1720                for member in entry.Members :
1721                    if (uname is None) or (member.Name == uname) :
1722                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1723            return [fields] + self.sortRecords(fields, result, ["+groupdn", "+userdn"], ordering)
1724               
1725    def extractPmembers(self, extractonly={}, ordering=[]) :
1726        """Extracts all printer groups members."""
1727        pname = extractonly.get("printername")
1728        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1729        if entries :
1730            fields = ("pgroupname", "printername", "pgroupdn", "printerdn")
1731            result = []
1732            pgname = extractonly.get("pgroupname")
1733            for entry in entries :
1734                for parent in self.getParentPrinters(entry) :
1735                    if (pgname is None) or (parent.Name == pgname) :
1736                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1737            return [fields] + self.sortRecords(fields, result, ["+pgroupdn", "+printerdn"], ordering)
1738       
1739    def extractHistory(self, extractonly={}, ordering=[]) :
1740        """Extracts all jobhistory records."""
1741        uname = extractonly.get("username")
1742        if uname :
1743            user = self.getUser(uname)
1744        else :   
1745            user = None
1746        pname = extractonly.get("printername")
1747        if pname :
1748            printer = self.getPrinter(pname)
1749        else :   
1750            printer = None
1751        startdate = extractonly.get("start")
1752        enddate = extractonly.get("end")
1753        (startdate, enddate) = self.cleanDates(startdate, enddate)
1754        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), jobid=extractonly.get("jobid"), limit=None, start=startdate, end=enddate)
1755        if entries :
1756            fields = ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode", "precomputedjobsize", "precomputedjobprice")
1757            result = []
1758            for entry in entries :
1759                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)) 
1760            return [fields] + self.sortRecords(fields, result, ["+dn"], ordering)
1761           
1762    def getBillingCodeFromBackend(self, label) :
1763        """Extracts billing code information given its label : returns first matching billing code."""
1764        code = StorageBillingCode(self, label)
1765        ulabel = self.userCharsetToDatabase(label)
1766        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % \
1767                                  ulabel, \
1768                                  ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], \
1769                                  base=self.info["billingcodebase"])
1770        if result :
1771            fields = result[0][1]       # take only first matching code, ignore the rest
1772            code.ident = result[0][0]
1773            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1774            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1775            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1776            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1777            code.Exists = True
1778        return code   
1779       
1780    def addBillingCode(self, bcode) :
1781        """Adds a billing code to the quota storage, returns it."""
1782        oldentry = self.getBillingCode(bcode.BillingCode)
1783        if oldentry.Exists :
1784            return oldentry # we return the existing entry
1785        uuid = self.genUUID()
1786        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1787        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1788                   "cn" : uuid,
1789                   "pykotaBillingCode" : self.userCharsetToDatabase(bcode.BillingCode),
1790                   "pykotaPageCounter" : str(bcode.PageCounter or 0),
1791                   "pykotaBalance" : str(bcode.Balance or 0.0),
1792                   "description" : self.userCharsetToDatabase(bcode.Description or ""), 
1793                 } 
1794        self.doAdd(dn, fields)
1795        bcode.isDirty = False
1796        return None # the entry created doesn't need further modification
1797       
1798    def saveBillingCode(self, bcode) :
1799        """Sets the new description for a billing code."""
1800        fields = {
1801                   "description" : self.userCharsetToDatabase(bcode.Description or ""), 
1802                   "pykotaPageCounter" : str(bcode.PageCounter or 0),
1803                   "pykotaBalance" : str(bcode.Balance or 0.0),
1804                 }
1805        self.doModify(bcode.ident, fields)
1806           
1807    def getMatchingBillingCodes(self, billingcodepattern) :
1808        """Returns the list of all billing codes which match a certain pattern."""
1809        codes = []
1810        result = self.doSearch("objectClass=pykotaBilling", \
1811                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1812                                base=self.info["billingcodebase"])
1813        if result :
1814            patterns = billingcodepattern.split(",")
1815            try :
1816                patdict = {}.fromkeys(patterns)
1817            except AttributeError :   
1818                # Python v2.2 or earlier
1819                patdict = {}
1820                for p in patterns :
1821                    patdict[p] = None
1822            for (codeid, fields) in result :
1823                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1824                if patdict.has_key(codename) or self.tool.matchString(codename, patterns) :
1825                    code = StorageBillingCode(self, codename)
1826                    code.ident = codeid
1827                    code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1828                    code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1829                    code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1830                    code.Exists = True
1831                    codes.append(code)
1832                    self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1833        return codes       
1834       
1835    def consumeBillingCode(self, bcode, pagecounter, balance) :
1836        """Consumes from a billing code."""
1837        fields = {
1838                   "pykotaBalance" : { "operator" : "-", "value" : balance, "convert" : float },
1839                   "pykotaPageCounter" : { "operator" : "+", "value" : pagecounter, "convert" : int },
1840                 }
1841        return self.doModify(bcode.ident, fields)         
1842
1843    def refundJob(self, jobident) :   
1844        """Marks a job as refunded in the history."""
1845        dn = "cn=%s,%s" % (ident, self.info["jobbase"])
1846        fields = {
1847                     "pykotaAction" : "REFUND",
1848                 }   
1849        self.doModify(dn, fields)         
1850       
1851    def storageUserFromRecord(self, username, record) :
1852        """Returns a StorageUser instance from a database record."""
1853        user = StorageUser(self, username)
1854        user.Exists = True
1855        return user
1856       
1857    def storageGroupFromRecord(self, groupname, record) :
1858        """Returns a StorageGroup instance from a database record."""
1859        group = StorageGroup(self, groupname)
1860        group.Exists = True
1861        return group
1862       
1863    def storagePrinterFromRecord(self, printername, record) :
1864        """Returns a StoragePrinter instance from a database record."""
1865        printer = StoragePrinter(self, printername)
1866        printer.Exists = True
1867        return printer
1868       
1869    def setJobAttributesFromRecord(self, job, record) :   
1870        """Sets the attributes of a job from a database record."""
1871        job.Exists = True
1872       
1873    def storageJobFromRecord(self, record) :
1874        """Returns a StorageJob instance from a database record."""
1875        job = StorageJob(self)
1876        self.setJobAttributesFromRecord(job, record)
1877        return job
1878       
1879    def storageLastJobFromRecord(self, printer, record) :
1880        """Returns a StorageLastJob instance from a database record."""
1881        lastjob = StorageLastJob(self, printer)
1882        self.setJobAttributesFromRecord(lastjob, record)
1883        return lastjob
1884       
1885    def storageUserPQuotaFromRecord(self, user, printer, record) :
1886        """Returns a StorageUserPQuota instance from a database record."""
1887        userpquota = StorageUserPQuota(self, user, printer)
1888        userpquota.Exists = True
1889        return userpquota
1890       
1891    def storageGroupPQuotaFromRecord(self, group, printer, record) :
1892        """Returns a StorageGroupPQuota instance from a database record."""
1893        grouppquota = StorageGroupPQuota(self, group, printer)
1894        grouppquota.Exists = True
1895        return grouppquota
1896       
1897    def storageBillingCodeFromRecord(self, billingcode, record) :
1898        """Returns a StorageBillingCode instance from a database record."""
1899        code = StorageBillingCode(self, billingcode)
1900        code.Exists = True
1901        return code
Note: See TracBrowser for help on using the browser.