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

Revision 2754, 94.1 kB (checked in by jerome, 18 years ago)

Huge speed improvement in data retrieval when wildcards are not used.

  • 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 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
25#
26# My IANA assigned number, for
27# "Conseil Internet & Logiciels Libres, J�me Alet"
28# is 16868. Use this as a base to create the LDAP schema.
29#
30
31import sys
32import os
33import types
34import time
35import md5
36import base64
37import random
38
39from mx import DateTime
40
41from pykota.storage import PyKotaStorageError, BaseStorage, StorageObject, \
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"], "pykotaOverCharge", "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            user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
369            result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["balancerdn"], username), ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], base=self.info["balancebase"])
370            if not result :
371                raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
372            else :
373                fields = result[0][1]
374                user.idbalance = result[0][0]
375                user.AccountBalance = fields.get("pykotaBalance")
376                if user.AccountBalance is not None :
377                    if user.AccountBalance[0].upper() == "NONE" :
378                        user.AccountBalance = None
379                    else :   
380                        user.AccountBalance = float(user.AccountBalance[0])
381                user.AccountBalance = user.AccountBalance or 0.0       
382                user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
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 = 1
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 = 1
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 = 1
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 = 1
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 = 1
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 = 1
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 = 1
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 = 1
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"], "pykotaOverCharge", "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.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
747                    user.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
748                    uname = self.userCharsetToDatabase(username)
749                    result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % \
750                                              (uname, self.info["balancerdn"], uname), \
751                                              ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], \
752                                              base=self.info["balancebase"])
753                    if not result :
754                        raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
755                    else :
756                        fields = result[0][1]
757                        user.idbalance = result[0][0]
758                        user.AccountBalance = fields.get("pykotaBalance")
759                        if user.AccountBalance is not None :
760                            if user.AccountBalance[0].upper() == "NONE" :
761                                user.AccountBalance = None
762                            else :   
763                                user.AccountBalance = float(user.AccountBalance[0])
764                        user.AccountBalance = user.AccountBalance or 0.0       
765                        user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
766                        if user.LifeTimePaid is not None :
767                            if user.LifeTimePaid[0].upper() == "NONE" :
768                                user.LifeTimePaid = None
769                            else :   
770                                user.LifeTimePaid = float(user.LifeTimePaid[0])
771                        user.LifeTimePaid = user.LifeTimePaid or 0.0       
772                        user.Payments = []
773                        for payment in fields.get("pykotaPayments", []) :
774                            try :
775                                (date, amount, description) = payment.split(" # ")
776                            except ValueError :
777                                # Payment with no description (old Payment)
778                                (date, amount) = payment.split(" # ")
779                                description = ""
780                            else :   
781                                description = self.databaseToUserCharset(base64.decodestring(description))
782                            user.Payments.append((date, float(amount), description))
783                    user.Exists = 1
784                    users.append(user)
785                    self.cacheEntry("USERS", user.Name, user)
786        return users       
787       
788    def getMatchingGroups(self, grouppattern) :
789        """Returns the list of all groups for which name matches a certain pattern."""
790        groups = []
791        # see comment at the same place in pgstorage.py
792        result = self.doSearch("objectClass=pykotaGroup", \
793                                  ["pykotaGroupName", "pykotaLimitBy", "description"], \
794                                  base=self.info["groupbase"])
795        if result :
796            patterns = grouppattern.split(",")
797            try :
798                patdict = {}.fromkeys(patterns)
799            except AttributeError :   
800                # Python v2.2 or earlier
801                patdict = {}
802                for p in patterns :
803                    patdict[p] = None
804            for (groupid, fields) in result :
805                groupname = self.databaseToUserCharset(fields.get("pykotaGroupName", [""])[0] or fields.get(self.info["grouprdn"], [""])[0])
806                if patdict.has_key(groupname) or self.tool.matchString(groupname, patterns) :
807                    group = StorageGroup(self, groupname)
808                    group.ident = groupid
809                    group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
810                    group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
811                    group.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
812                    group.AccountBalance = 0.0
813                    group.LifeTimePaid = 0.0
814                    for member in self.getGroupMembers(group) :
815                        if member.Exists :
816                            group.AccountBalance += member.AccountBalance
817                            group.LifeTimePaid += member.LifeTimePaid
818                    group.Exists = 1
819                    groups.append(group)
820                    self.cacheEntry("GROUPS", group.Name, group)
821        return groups
822       
823    def getPrinterUsersAndQuotas(self, printer, names=["*"]) :       
824        """Returns the list of users who uses a given printer, along with their quotas."""
825        usersandquotas = []
826        pname = self.userCharsetToDatabase(printer.Name)
827        names = [self.userCharsetToDatabase(n) for n in names]
828        if self.info["userquotabase"].lower() == "user" :
829           base = self.info["userbase"]
830        else :
831           base = self.info["userquotabase"]
832        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % \
833                                  (pname, "".join(["(pykotaUserName=%s)" % uname for uname in names])), \
834                                  ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], \
835                                  base=base)
836        if result :
837            for (userquotaid, fields) in result :
838                user = self.getUser(self.databaseToUserCharset(fields.get("pykotaUserName")[0]))
839                userpquota = StorageUserPQuota(self, user, printer)
840                userpquota.ident = userquotaid
841                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
842                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
843                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
844                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
845                if userpquota.SoftLimit is not None :
846                    if userpquota.SoftLimit[0].upper() == "NONE" :
847                        userpquota.SoftLimit = None
848                    else :   
849                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
850                userpquota.HardLimit = fields.get("pykotaHardLimit")
851                if userpquota.HardLimit is not None :
852                    if userpquota.HardLimit[0].upper() == "NONE" :
853                        userpquota.HardLimit = None
854                    elif userpquota.HardLimit is not None :   
855                        userpquota.HardLimit = int(userpquota.HardLimit[0])
856                userpquota.DateLimit = fields.get("pykotaDateLimit")
857                if userpquota.DateLimit is not None :
858                    if userpquota.DateLimit[0].upper() == "NONE" : 
859                        userpquota.DateLimit = None
860                    else :   
861                        userpquota.DateLimit = userpquota.DateLimit[0]
862                userpquota.Exists = 1
863                usersandquotas.append((user, userpquota))
864                self.cacheEntry("USERPQUOTAS", "%s@%s" % (user.Name, printer.Name), userpquota)
865        usersandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
866        return usersandquotas
867               
868    def getPrintersUsersAndPQuotas(self, pnames = ["*"], unames=["*"]) :   
869        """Returns all printers, users and users print quota entries which match a set of names."""
870        printers = {}
871        users = {}
872        upquotas = {}
873        for printer in self.getMatchingPrinters(",".join(pnames)) :
874            printers[printer.Name] = printer
875        for user in self.getMatchingUsers(",".join(unames)) :
876            users[user.Name] = user
877        for (p, printer) in printers.items() :
878            for (u, user) in users.items() :
879                upqkey = "%s@%s" % (u, p)
880                upquotas[upqkey] = self.getUserPQuota(user, printer)
881        return (printers, users, upquotas)
882       
883    def getPrintersGroupsAndPQuotas(self, pnames = ["*"], gnames=["*"]) :   
884        """Returns all printers, groups and groups print quota entries which match a set of names."""
885        printers = {}
886        groups = {}
887        gpquotas = {}
888        for printer in self.getMatchingPrinters(",".join(pnames)) :
889            printers[printer.Name] = printer
890        for group in self.getMatchingGroups(",".join(gnames)) :
891            groups[group.Name] = group
892        for (p, printer) in printers.items() :
893            for (g, group) in groups.items() :
894                gpqkey = "%s@%s" % (g, p)
895                gpquotas[gpqkey] = self.getGroupPQuota(group, printer)
896        return (printers, groups, gpquotas)
897       
898    def getPrinterGroupsAndQuotas(self, printer, names=["*"]) :       
899        """Returns the list of groups which uses a given printer, along with their quotas."""
900        groupsandquotas = []
901        pname = self.userCharsetToDatabase(printer.Name)
902        names = [self.userCharsetToDatabase(n) for n in names]
903        if self.info["groupquotabase"].lower() == "group" :
904           base = self.info["groupbase"]
905        else :
906           base = self.info["groupquotabase"]
907        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s)(|%s))" % \
908                                  (pname, "".join(["(pykotaGroupName=%s)" % gname for gname in names])), \
909                                  ["pykotaGroupName"], \
910                                  base=base)
911        if result :
912            for (groupquotaid, fields) in result :
913                group = self.getGroup(self.databaseToUserCharset(fields.get("pykotaGroupName")[0]))
914                grouppquota = self.getGroupPQuota(group, printer)
915                groupsandquotas.append((group, grouppquota))
916        groupsandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
917        return groupsandquotas
918       
919    def addPrinter(self, printername) :       
920        """Adds a printer to the quota storage, returns it."""
921        printername = self.userCharsetToDatabase(printername)
922        fields = { self.info["printerrdn"] : printername,
923                   "objectClass" : ["pykotaObject", "pykotaPrinter"],
924                   "cn" : printername,
925                   "pykotaPrinterName" : printername,
926                   "pykotaPricePerPage" : "0.0",
927                   "pykotaPricePerJob" : "0.0",
928                   "pykotaMaxJobSize" : "0",
929                   "pykotaPassThrough" : "0",
930                 } 
931        dn = "%s=%s,%s" % (self.info["printerrdn"], printername, self.info["printerbase"])
932        self.doAdd(dn, fields)
933        return self.getPrinter(printername)
934       
935    def addUser(self, user) :       
936        """Adds a user to the quota storage, returns it."""
937        uname = self.userCharsetToDatabase(user.Name)
938        newfields = {
939                       "pykotaUserName" : uname,
940                       "pykotaLimitBy" : (user.LimitBy or "quota"),
941                       "pykotaOverCharge" : str(user.OverCharge),
942                       "description" : self.userCharsetToDatabase(user.Description or "")
943                    }   
944                       
945        if user.Email :
946            newfields.update({self.info["usermail"]: user.Email})
947        mustadd = 1
948        if self.info["newuser"].lower() != 'below' :
949            try :
950                (where, action) = [s.strip() for s in self.info["newuser"].split(",")]
951            except ValueError :
952                (where, action) = (self.info["newuser"].strip(), "fail")
953            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
954                                      (where, self.info["userrdn"], uname), \
955                                      None, \
956                                      base=self.info["userbase"])
957            if result :
958                (dn, fields) = result[0]
959                oc = fields.get("objectClass", fields.get("objectclass", []))
960                oc.extend(["pykotaAccount", "pykotaAccountBalance"])
961                fields.update(newfields)
962                fields.update({ "pykotaBalance" : str(user.AccountBalance or 0.0),
963                                "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), })   
964                self.doModify(dn, fields)
965                mustadd = 0
966            else :
967                message = _("Unable to find an existing objectClass %s entry with %s=%s to attach pykotaAccount objectClass") % (where, self.info["userrdn"], user.Name)
968                if action.lower() == "warn" :   
969                    self.tool.printInfo(_("%s. A new entry will be created instead.") % message, "warn")
970                else : # 'fail' or incorrect setting
971                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
972               
973        if mustadd :
974            if self.info["userbase"] == self.info["balancebase"] :           
975                fields = { self.info["userrdn"] : uname,
976                           "objectClass" : ["pykotaObject", "pykotaAccount", "pykotaAccountBalance"],
977                           "cn" : uname,
978                           "pykotaBalance" : str(user.AccountBalance or 0.0),
979                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
980                         } 
981            else :             
982                fields = { self.info["userrdn"] : uname,
983                           "objectClass" : ["pykotaObject", "pykotaAccount"],
984                           "cn" : uname,
985                         } 
986            fields.update(newfields)         
987            dn = "%s=%s,%s" % (self.info["userrdn"], uname, self.info["userbase"])
988            self.doAdd(dn, fields)
989            if self.info["userbase"] != self.info["balancebase"] :           
990                fields = { self.info["balancerdn"] : uname,
991                           "objectClass" : ["pykotaObject", "pykotaAccountBalance"],
992                           "cn" : uname,
993                           "pykotaBalance" : str(user.AccountBalance or 0.0),
994                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
995                         } 
996                dn = "%s=%s,%s" % (self.info["balancerdn"], uname, self.info["balancebase"])
997                self.doAdd(dn, fields)
998           
999        return self.getUser(user.Name)
1000       
1001    def addGroup(self, group) :       
1002        """Adds a group to the quota storage, returns it."""
1003        gname = self.userCharsetToDatabase(group.Name)
1004        newfields = { 
1005                      "pykotaGroupName" : gname,
1006                      "pykotaLimitBy" : (group.LimitBy or "quota"),
1007                      "description" : self.userCharsetToDatabase(group.Description or "")
1008                    } 
1009        mustadd = 1
1010        if self.info["newgroup"].lower() != 'below' :
1011            try :
1012                (where, action) = [s.strip() for s in self.info["newgroup"].split(",")]
1013            except ValueError :
1014                (where, action) = (self.info["newgroup"].strip(), "fail")
1015            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
1016                                      (where, self.info["grouprdn"], gname), \
1017                                      None, \
1018                                      base=self.info["groupbase"])
1019            if result :
1020                (dn, fields) = result[0]
1021                oc = fields.get("objectClass", fields.get("objectclass", []))
1022                oc.extend(["pykotaGroup"])
1023                fields.update(newfields)
1024                self.doModify(dn, fields)
1025                mustadd = 0
1026            else :
1027                message = _("Unable to find an existing entry to attach pykotaGroup objectclass %s") % group.Name
1028                if action.lower() == "warn" :   
1029                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
1030                else : # 'fail' or incorrect setting
1031                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
1032               
1033        if mustadd :
1034            fields = { self.info["grouprdn"] : gname,
1035                       "objectClass" : ["pykotaObject", "pykotaGroup"],
1036                       "cn" : gname,
1037                     } 
1038            fields.update(newfields)         
1039            dn = "%s=%s,%s" % (self.info["grouprdn"], gname, self.info["groupbase"])
1040            self.doAdd(dn, fields)
1041        return self.getGroup(group.Name)
1042       
1043    def addUserToGroup(self, user, group) :   
1044        """Adds an user to a group."""
1045        if user.Name not in [u.Name for u in self.getGroupMembers(group)] :
1046            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1047            if result :
1048                fields = result[0][1]
1049                if not fields.has_key(self.info["groupmembers"]) :
1050                    fields[self.info["groupmembers"]] = []
1051                fields[self.info["groupmembers"]].append(self.userCharsetToDatabase(user.Name))
1052                self.doModify(group.ident, fields)
1053                group.Members.append(user)
1054               
1055    def delUserFromGroup(self, user, group) :   
1056        """Removes an user from a group."""
1057        if user.Name in [u.Name for u in self.getGroupMembers(group)] :
1058            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)
1059            if result :
1060                fields = result[0][1]
1061                if not fields.has_key(self.info["groupmembers"]) :
1062                    fields[self.info["groupmembers"]] = []
1063                try :   
1064                    fields[self.info["groupmembers"]].remove(self.userCharsetToDatabase(user.Name))
1065                except ValueError :
1066                    pass # TODO : Strange, shouldn't it be there ?
1067                else :
1068                    self.doModify(group.ident, fields)
1069                    group.Members.remove(user)
1070               
1071    def addUserPQuota(self, upq) :
1072        """Initializes a user print quota on a printer."""
1073        # first check if an entry already exists
1074        oldentry = self.getUserPQuota(upq.User, upq.Printer)
1075        if oldentry.Exists :
1076            return oldentry # we return the existing entry
1077        uuid = self.genUUID()
1078        uname = self.userCharsetToDatabase(upq.User.Name)
1079        pname = self.userCharsetToDatabase(upq.Printer.Name)
1080        fields = { "cn" : uuid,
1081                   "objectClass" : ["pykotaObject", "pykotaUserPQuota"],
1082                   "pykotaUserName" : uname,
1083                   "pykotaPrinterName" : pname,
1084                   "pykotaSoftLimit" : str(upq.SoftLimit),
1085                   "pykotaHardLimit" : str(upq.HardLimit),
1086                   "pykotaDateLimit" : str(upq.DateLimit),
1087                   "pykotaPageCounter" : str(upq.PageCounter or 0),
1088                   "pykotaLifePageCounter" : str(upq.LifePageCounter or 0),
1089                   "pykotaWarnCount" : str(upq.WarnCount or 0),
1090                   "pykotaMaxJobSize" : str(upq.MaxJobSize or 0),
1091                 } 
1092        if self.info["userquotabase"].lower() == "user" :
1093            dn = "cn=%s,%s" % (uuid, upq.User.ident)
1094        else :   
1095            dn = "cn=%s,%s" % (uuid, self.info["userquotabase"])
1096        self.doAdd(dn, fields)
1097        upq.isDirty = False
1098        return None # the entry created doesn't need further modification
1099       
1100    def addGroupPQuota(self, gpq) :
1101        """Initializes a group print quota on a printer."""
1102        oldentry = self.getGroupPQuota(gpq.Group, gpq.Printer)
1103        if oldentry.Exists :
1104            return oldentry # we return the existing entry
1105        uuid = self.genUUID()
1106        gname = self.userCharsetToDatabase(gpq.Group.Name)
1107        pname = self.userCharsetToDatabase(gpq.Printer.Name)
1108        fields = { "cn" : uuid,
1109                   "objectClass" : ["pykotaObject", "pykotaGroupPQuota"],
1110                   "pykotaGroupName" : gname,
1111                   "pykotaPrinterName" : pname,
1112                   "pykotaDateLimit" : "None",
1113                 } 
1114        if self.info["groupquotabase"].lower() == "group" :
1115            dn = "cn=%s,%s" % (uuid, gpq.Group.ident)
1116        else :   
1117            dn = "cn=%s,%s" % (uuid, self.info["groupquotabase"])
1118        self.doAdd(dn, fields)
1119        gpq.isDirty = False
1120        return None # the entry created doesn't need further modification
1121       
1122    def savePrinter(self, printer) :   
1123        """Saves the printer to the database in a single operation."""
1124        fields = {
1125                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
1126                   "pykotaMaxJobSize" : (printer.MaxJobSize and str(printer.MaxJobSize)) or "0",
1127                   "description" : self.userCharsetToDatabase(printer.Description or ""),
1128                   "pykotaPricePerPage" : str(printer.PricePerPage),
1129                   "pykotaPricePerJob" : str(printer.PricePerJob),
1130                 }
1131        self.doModify(printer.ident, fields)
1132       
1133    def saveUser(self, user) :
1134        """Saves the user to the database in a single operation."""
1135        newfields = {
1136                       "pykotaLimitBy" : (user.LimitBy or "quota"),
1137                       "pykotaOverCharge" : str(user.OverCharge),
1138                       "description" : self.userCharsetToDatabase(user.Description or ""), 
1139                    }   
1140        if user.Email :
1141            newfields.update({self.info["usermail"]: user.Email})
1142        self.doModify(user.ident, newfields)
1143       
1144        newfields = { "pykotaBalance" : str(user.AccountBalance or 0.0),
1145                      "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
1146                    }
1147        self.doModify(user.idbalance, newfields)
1148       
1149    def saveGroup(self, group) :
1150        """Saves the group to the database in a single operation."""
1151        newfields = {
1152                       "pykotaLimitBy" : (group.LimitBy or "quota"),
1153                       "description" : self.userCharsetToDatabase(group.Description or ""), 
1154                    }   
1155        self.doModify(group.ident, newfields)
1156       
1157    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
1158        """Sets the date limit permanently for a user print quota."""
1159        fields = {
1160                   "pykotaDateLimit" : datelimit,
1161                 }
1162        return self.doModify(userpquota.ident, fields)
1163           
1164    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
1165        """Sets the date limit permanently for a group print quota."""
1166        fields = {
1167                   "pykotaDateLimit" : datelimit,
1168                 }
1169        return self.doModify(grouppquota.ident, fields)
1170       
1171    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
1172        """Increase page counters for a user print quota."""
1173        fields = {
1174                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1175                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1176                 }
1177        return self.doModify(userpquota.ident, fields)         
1178       
1179    def decreaseUserAccountBalance(self, user, amount) :   
1180        """Decreases user's account balance from an amount."""
1181        fields = {
1182                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
1183                 }
1184        return self.doModify(user.idbalance, fields, flushcache=1)         
1185       
1186    def writeNewPayment(self, user, amount, comment="") :
1187        """Adds a new payment to the payments history."""
1188        payments = []
1189        for payment in user.Payments :
1190            payments.append("%s # %s # %s" % (payment[0], str(payment[1]), base64.encodestring(self.userCharsetToDatabase(payment[2])).strip()))
1191        payments.append("%s # %s # %s" % (str(DateTime.now()), str(amount), base64.encodestring(self.userCharsetToDatabase(comment)).strip()))
1192        fields = {
1193                   "pykotaPayments" : payments,
1194                 }
1195        return self.doModify(user.idbalance, fields)         
1196       
1197    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
1198        """Sets the last job's size permanently."""
1199        fields = {
1200                   "pykotaJobSize" : str(jobsize),
1201                   "pykotaJobPrice" : str(jobprice),
1202                 }
1203        self.doModify(lastjob.ident, fields)         
1204       
1205    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) :
1206        """Adds a job in a printer's history."""
1207        uname = self.userCharsetToDatabase(user.Name)
1208        pname = self.userCharsetToDatabase(printer.Name)
1209        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1210            uuid = self.genUUID()
1211            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
1212        else :   
1213            uuid = printer.LastJob.ident[3:].split(",")[0]
1214            dn = printer.LastJob.ident
1215        if self.privacy :   
1216            # For legal reasons, we want to hide the title, filename and options
1217            title = filename = options = "hidden"
1218        fields = {
1219                   "objectClass" : ["pykotaObject", "pykotaJob"],
1220                   "cn" : uuid,
1221                   "pykotaUserName" : uname,
1222                   "pykotaPrinterName" : pname,
1223                   "pykotaJobId" : jobid,
1224                   "pykotaPrinterPageCounter" : str(pagecounter),
1225                   "pykotaAction" : action,
1226                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
1227                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
1228                   "pykotaCopies" : str(copies), 
1229                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
1230                   "pykotaHostName" : str(clienthost), 
1231                   "pykotaJobSizeBytes" : str(jobsizebytes),
1232                   "pykotaMD5Sum" : str(jobmd5sum),
1233                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
1234                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
1235                   "pykotaPrecomputedJobSize" : str(precomputedsize),
1236                   "pykotaPrecomputedJobPrice" : str(precomputedprice),
1237                 }
1238        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1239            if jobsize is not None :         
1240                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1241            self.doAdd(dn, fields)
1242        else :   
1243            # here we explicitly want to reset jobsize to 'None' if needed
1244            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1245            self.doModify(dn, fields)
1246           
1247        if printer.LastJob.Exists :
1248            fields = {
1249                       "pykotaLastJobIdent" : uuid,
1250                     }
1251            self.doModify(printer.LastJob.lastjobident, fields)         
1252        else :   
1253            lastjuuid = self.genUUID()
1254            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
1255            fields = {
1256                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
1257                       "cn" : lastjuuid,
1258                       "pykotaPrinterName" : pname,
1259                       "pykotaLastJobIdent" : uuid,
1260                     } 
1261            self.doAdd(lastjdn, fields)         
1262           
1263    def saveUserPQuota(self, userpquota) :
1264        """Saves an user print quota entry."""
1265        fields = { 
1266                   "pykotaSoftLimit" : str(userpquota.SoftLimit),
1267                   "pykotaHardLimit" : str(userpquota.HardLimit),
1268                   "pykotaDateLimit" : str(userpquota.DateLimit),
1269                   "pykotaWarnCount" : str(userpquota.WarnCount or 0),
1270                   "pykotaPageCounter" : str(userpquota.PageCounter or 0),
1271                   "pykotaLifePageCounter" : str(userpquota.LifePageCounter or 0),
1272                   "pykotaMaxJobSize" : str(userpquota.MaxJobSize or 0),
1273                 }
1274        self.doModify(userpquota.ident, fields)
1275       
1276    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1277        """Sets the warn counter value for a user quota."""
1278        fields = { 
1279                   "pykotaWarnCount" : str(warncount or 0),
1280                 }
1281        self.doModify(userpquota.ident, fields)
1282       
1283    def increaseUserPQuotaWarnCount(self, userpquota) :
1284        """Increases the warn counter value for a user quota."""
1285        fields = {
1286                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1287                 }
1288        return self.doModify(userpquota.ident, fields)         
1289       
1290    def saveGroupPQuota(self, grouppquota) :
1291        """Saves a group print quota entry."""
1292        fields = { 
1293                   "pykotaSoftLimit" : str(grouppquota.SoftLimit),
1294                   "pykotaHardLimit" : str(grouppquota.HardLimit),
1295                   "pykotaDateLimit" : str(grouppquota.DateLimit),
1296                   "pykotaMaxJobSize" : str(userpquota.MaxJobSize or 0),
1297                 }
1298        self.doModify(grouppquota.ident, fields)
1299           
1300    def writePrinterToGroup(self, pgroup, printer) :
1301        """Puts a printer into a printer group."""
1302        if printer.ident not in pgroup.uniqueMember :
1303            pgroup.uniqueMember.append(printer.ident)
1304            fields = {
1305                       "uniqueMember" : pgroup.uniqueMember
1306                     } 
1307            self.doModify(pgroup.ident, fields)         
1308           
1309    def removePrinterFromGroup(self, pgroup, printer) :
1310        """Removes a printer from a printer group."""
1311        try :
1312            pgroup.uniqueMember.remove(printer.ident)
1313        except ValueError :   
1314            pass
1315        else :   
1316            fields = {
1317                       "uniqueMember" : pgroup.uniqueMember,
1318                     } 
1319            self.doModify(pgroup.ident, fields)         
1320           
1321    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
1322        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1323        precond = "(objectClass=pykotaJob)"
1324        where = []
1325        if user is not None :
1326            where.append("(pykotaUserName=%s)" % self.userCharsetToDatabase(user.Name))
1327        if printer is not None :
1328            where.append("(pykotaPrinterName=%s)" % self.userCharsetToDatabase(printer.Name))
1329        if hostname is not None :
1330            where.append("(pykotaHostName=%s)" % hostname)
1331        if billingcode is not None :
1332            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1333        if where :   
1334            where = "(&%s)" % "".join([precond] + where)
1335        else :   
1336            where = precond
1337        jobs = []   
1338        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1339                                               "pykotaHostName", 
1340                                               "pykotaUserName", 
1341                                               "pykotaPrinterName", 
1342                                               "pykotaJobId", 
1343                                               "pykotaPrinterPageCounter", 
1344                                               "pykotaAction", 
1345                                               "pykotaJobSize", 
1346                                               "pykotaJobPrice", 
1347                                               "pykotaFileName", 
1348                                               "pykotaTitle", 
1349                                               "pykotaCopies", 
1350                                               "pykotaOptions", 
1351                                               "pykotaBillingCode", 
1352                                               "pykotaPages", 
1353                                               "pykotaMD5Sum", 
1354                                               "pykotaPrecomputedJobSize",
1355                                               "pykotaPrecomputedJobPrice",
1356                                               "createTimestamp" ], 
1357                                      base=self.info["jobbase"])
1358        if result :
1359            for (ident, fields) in result :
1360                job = StorageJob(self)
1361                job.ident = ident
1362                job.JobId = fields.get("pykotaJobId")[0]
1363                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1364                try :
1365                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1366                except ValueError :   
1367                    job.JobSize = None
1368                try :   
1369                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1370                except ValueError :
1371                    job.JobPrice = None
1372                job.JobAction = fields.get("pykotaAction", [""])[0]
1373                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1374                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1375                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1376                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1377                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1378                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1379                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1380                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1381                job.JobPages = fields.get("pykotaPages", [""])[0]
1382                try :
1383                    job.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
1384                except ValueError :   
1385                    job.PrecomputedJobSize = None
1386                try :   
1387                    job.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
1388                except ValueError :
1389                    job.PrecomputedJobPrice = None
1390                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1391                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1392                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
1393                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
1394                job.JobDate = mxtime.strftime("%Y%m%d %H:%M:%S")
1395                if ((start is None) and (end is None)) or \
1396                   ((start is None) and (job.JobDate <= end)) or \
1397                   ((end is None) and (job.JobDate >= start)) or \
1398                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1399                    job.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
1400                    job.PrinterName = self.databaseToUserCharset(fields.get("pykotaPrinterName")[0])
1401                    job.Exists = 1
1402                    jobs.append(job)
1403            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1404            if limit :   
1405                jobs = jobs[:int(limit)]
1406        return jobs
1407       
1408    def deleteUser(self, user) :   
1409        """Completely deletes an user from the Quota Storage."""
1410        uname = self.userCharsetToDatabase(user.Name)
1411        todelete = []   
1412        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % uname, base=self.info["jobbase"])
1413        for (ident, fields) in result :
1414            todelete.append(ident)
1415        if self.info["userquotabase"].lower() == "user" :
1416            base = self.info["userbase"]
1417        else :
1418            base = self.info["userquotabase"]
1419        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % uname, \
1420                                  ["pykotaPrinterName", "pykotaUserName"], \
1421                                  base=base)
1422        for (ident, fields) in result :
1423            # ensure the user print quota entry will be deleted
1424            todelete.append(ident)
1425           
1426            # if last job of current printer was printed by the user
1427            # to delete, we also need to delete the printer's last job entry.
1428            printer = self.getPrinter(self.databaseToUserCharset(fields["pykotaPrinterName"][0]))
1429            if printer.LastJob.UserName == user.Name :
1430                todelete.append(printer.LastJob.lastjobident)
1431           
1432        for ident in todelete :   
1433            self.doDelete(ident)
1434           
1435        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1436        if result :
1437            fields = result[0][1]
1438            for k in fields.keys() :
1439                if k.startswith("pykota") :
1440                    del fields[k]
1441                elif k.lower() == "objectclass" :   
1442                    todelete = []
1443                    for i in range(len(fields[k])) :
1444                        if fields[k][i].startswith("pykota") : 
1445                            todelete.append(i)
1446                    todelete.sort()       
1447                    todelete.reverse()
1448                    for i in todelete :
1449                        del fields[k][i]
1450            if fields.get("objectClass") or fields.get("objectclass") :
1451                self.doModify(user.ident, fields, ignoreold=0)       
1452            else :   
1453                self.doDelete(user.ident)
1454        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % \
1455                                   uname, \
1456                                   ["pykotaUserName"], \
1457                                   base=self.info["balancebase"])
1458        for (ident, fields) in result :
1459            self.doDelete(ident)
1460       
1461    def deleteGroup(self, group) :   
1462        """Completely deletes a group from the Quota Storage."""
1463        gname = self.userCharsetToDatabase(group.Name)
1464        if self.info["groupquotabase"].lower() == "group" :
1465            base = self.info["groupbase"]
1466        else :
1467            base = self.info["groupquotabase"]
1468        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % \
1469                                  gname, \
1470                                  ["pykotaGroupName"], \
1471                                  base=base)
1472        for (ident, fields) in result :
1473            self.doDelete(ident)
1474        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1475        if result :
1476            fields = result[0][1]
1477            for k in fields.keys() :
1478                if k.startswith("pykota") :
1479                    del fields[k]
1480                elif k.lower() == "objectclass" :   
1481                    todelete = []
1482                    for i in range(len(fields[k])) :
1483                        if fields[k][i].startswith("pykota") : 
1484                            todelete.append(i)
1485                    todelete.sort()       
1486                    todelete.reverse()
1487                    for i in todelete :
1488                        del fields[k][i]
1489            if fields.get("objectClass") or fields.get("objectclass") :
1490                self.doModify(group.ident, fields, ignoreold=0)       
1491            else :   
1492                self.doDelete(group.ident)
1493               
1494    def deleteManyUserPQuotas(self, printers, users) :       
1495        """Deletes many user print quota entries."""
1496        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1497        for printer in printers :
1498            for user in users :
1499                upq = self.getUserPQuota(user, printer)
1500                if upq.Exists :
1501                    upq.delete()
1502           
1503    def deleteManyGroupPQuotas(self, printers, groups) :
1504        """Deletes many group print quota entries."""
1505        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1506        for printer in printers :
1507            for group in groups :
1508                gpq = self.getGroupPQuota(group, printer)
1509                if gpq.Exists :
1510                    gpq.delete()
1511               
1512    def deleteUserPQuota(self, upquota) :   
1513        """Completely deletes an user print quota entry from the database."""
1514        uname = self.userCharsetToDatabase(upquota.User.Name)
1515        pname = self.userCharsetToDatabase(upquota.Printer.Name)
1516        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s)(pykotaPrinterName=%s))" \
1517                                   % (uname, pname), \
1518                                   base=self.info["jobbase"])
1519        for (ident, fields) in result :
1520            self.doDelete(ident)
1521        if upquota.Printer.LastJob.UserName == upquota.User.Name :
1522            self.doDelete(upquota.Printer.LastJob.lastjobident)
1523        self.doDelete(upquota.ident)
1524       
1525    def deleteGroupPQuota(self, gpquota) :   
1526        """Completely deletes a group print quota entry from the database."""
1527        self.doDelete(gpquota.ident)
1528               
1529    def deletePrinter(self, printer) :   
1530        """Completely deletes a printer from the Quota Storage."""
1531        pname = self.userCharsetToDatabase(printer.Name)
1532        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % pname, base=self.info["lastjobbase"])
1533        for (ident, fields) in result :
1534            self.doDelete(ident)
1535        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % pname, base=self.info["jobbase"])
1536        for (ident, fields) in result :
1537            self.doDelete(ident)
1538        if self.info["groupquotabase"].lower() == "group" :
1539            base = self.info["groupbase"]
1540        else :
1541            base = self.info["groupquotabase"]
1542        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1543        for (ident, fields) in result :
1544            self.doDelete(ident)
1545        if self.info["userquotabase"].lower() == "user" :
1546            base = self.info["userbase"]
1547        else :
1548            base = self.info["userquotabase"]
1549        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1550        for (ident, fields) in result :
1551            self.doDelete(ident)
1552        for parent in self.getParentPrinters(printer) : 
1553            try :
1554                parent.uniqueMember.remove(printer.ident)
1555            except ValueError :   
1556                pass
1557            else :   
1558                fields = {
1559                           "uniqueMember" : parent.uniqueMember,
1560                         } 
1561                self.doModify(parent.ident, fields)         
1562        self.doDelete(printer.ident)   
1563       
1564    def deleteBillingCode(self, code) :
1565        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1566        self.doDelete(code.ident)
1567       
1568    def extractPrinters(self, extractonly={}) :
1569        """Extracts all printer records."""
1570        pname = extractonly.get("printername")
1571        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1572        if entries :
1573            result = [ ("dn", "printername", "priceperpage", "priceperjob", "description", "maxjobsize", "passthrough") ]
1574            for entry in entries :
1575                if entry.PassThrough in (1, "1", "t", "true", "T", "TRUE", "True") :
1576                    passthrough = "t"
1577                else :   
1578                    passthrough = "f"
1579                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description, entry.MaxJobSize, passthrough))
1580            return result 
1581       
1582    def extractUsers(self, extractonly={}) :
1583        """Extracts all user records."""
1584        uname = extractonly.get("username")
1585        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1586        if entries :
1587            result = [ ("dn", "username", "balance", "lifetimepaid", "limitby", "email", "description") ]
1588            for entry in entries :
1589                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email, entry.Description))
1590            return result 
1591       
1592    def extractBillingcodes(self, extractonly={}) :
1593        """Extracts all billing codes records."""
1594        billingcode = extractonly.get("billingcode")
1595        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1596        if entries :
1597            result = [ ("dn", "billingcode", "balance", "pagecounter", "description") ]
1598            for entry in entries :
1599                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1600            return result 
1601       
1602    def extractGroups(self, extractonly={}) :
1603        """Extracts all group records."""
1604        gname = extractonly.get("groupname")
1605        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1606        if entries :
1607            result = [ ("dn", "groupname", "limitby", "balance", "lifetimepaid", "description") ]
1608            for entry in entries :
1609                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid, entry.Description))
1610            return result 
1611       
1612    def extractPayments(self, extractonly={}) :
1613        """Extracts all payment records."""
1614        uname = extractonly.get("username")
1615        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1616        if entries :
1617            result = [ ("username", "amount", "date", "description") ]
1618            for entry in entries :
1619                for (date, amount, description) in entry.Payments :
1620                    result.append((entry.Name, amount, date, description))
1621            return result       
1622       
1623    def extractUpquotas(self, extractonly={}) :
1624        """Extracts all userpquota records."""
1625        pname = extractonly.get("printername")
1626        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1627        if entries :
1628            result = [ ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1629            uname = extractonly.get("username")
1630            for entry in entries :
1631                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1632                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1633            return result
1634       
1635    def extractGpquotas(self, extractonly={}) :
1636        """Extracts all grouppquota records."""
1637        pname = extractonly.get("printername")
1638        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1639        if entries :
1640            result = [ ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1641            gname = extractonly.get("groupname")
1642            for entry in entries :
1643                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1644                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1645            return result
1646       
1647    def extractUmembers(self, extractonly={}) :
1648        """Extracts all user groups members."""
1649        gname = extractonly.get("groupname")
1650        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1651        if entries :
1652            result = [ ("groupname", "username", "groupdn", "userdn") ]
1653            uname = extractonly.get("username")
1654            for entry in entries :
1655                for member in entry.Members :
1656                    if (uname is None) or (member.Name == uname) :
1657                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1658            return result       
1659               
1660    def extractPmembers(self, extractonly={}) :
1661        """Extracts all printer groups members."""
1662        pname = extractonly.get("printername")
1663        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1664        if entries :
1665            result = [ ("pgroupname", "printername", "pgroupdn", "printerdn") ]
1666            pgname = extractonly.get("pgroupname")
1667            for entry in entries :
1668                for parent in self.getParentPrinters(entry) :
1669                    if (pgname is None) or (parent.Name == pgname) :
1670                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1671            return result       
1672       
1673    def extractHistory(self, extractonly={}) :
1674        """Extracts all jobhistory records."""
1675        uname = extractonly.get("username")
1676        if uname :
1677            user = self.getUser(uname)
1678        else :   
1679            user = None
1680        pname = extractonly.get("printername")
1681        if pname :
1682            printer = self.getPrinter(pname)
1683        else :   
1684            printer = None
1685        startdate = extractonly.get("start")
1686        enddate = extractonly.get("end")
1687        (startdate, enddate) = self.cleanDates(startdate, enddate)
1688        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), limit=None, start=startdate, end=enddate)
1689        if entries :
1690            result = [ ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode", "precomputedjobsize", "precomputedjobprice") ] 
1691            for entry in entries :
1692                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)) 
1693            return result
1694           
1695    def getBillingCodeFromBackend(self, label) :
1696        """Extracts billing code information given its label : returns first matching billing code."""
1697        code = StorageBillingCode(self, label)
1698        ulabel = self.userCharsetToDatabase(label)
1699        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % \
1700                                  ulabel, \
1701                                  ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], \
1702                                  base=self.info["billingcodebase"])
1703        if result :
1704            fields = result[0][1]       # take only first matching code, ignore the rest
1705            code.ident = result[0][0]
1706            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1707            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1708            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1709            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1710            code.Exists = 1
1711        return code   
1712       
1713    def addBillingCode(self, label) :
1714        """Adds a billing code to the quota storage, returns it."""
1715        uuid = self.genUUID()
1716        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1717        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1718                   "cn" : uuid,
1719                   "pykotaBillingCode" : self.userCharsetToDatabase(label),
1720                   "pykotaPageCounter" : "0",
1721                   "pykotaBalance" : "0.0",
1722                 } 
1723        self.doAdd(dn, fields)
1724        return self.getBillingCode(label)
1725       
1726    def saveBillingCode(self, code) :
1727        """Sets the new description for a billing code."""
1728        fields = {
1729                   "description" : self.userCharsetToDatabase(code.Description or ""), 
1730                   "pykotaPageCounter" : str(code.PageCounter),
1731                   "pykotaBalance" : str(code.Balance),
1732                 }
1733        self.doModify(code.ident, fields)
1734           
1735    def getMatchingBillingCodes(self, billingcodepattern) :
1736        """Returns the list of all billing codes which match a certain pattern."""
1737        codes = []
1738        result = self.doSearch("objectClass=pykotaBilling", \
1739                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1740                                base=self.info["billingcodebase"])
1741        if result :
1742            patterns = billingcodepattern.split(",")
1743            for (codeid, fields) in result :
1744                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1745                if self.tool.matchString(codename, patterns) :
1746                    code = StorageBillingCode(self, codename)
1747                    code.ident = codeid
1748                    code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1749                    code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1750                    code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1751                    code.Exists = 1
1752                    codes.append(code)
1753                    self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1754        return codes       
1755       
1756    def consumeBillingCode(self, code, pagecounter, balance) :
1757        """Consumes from a billing code."""
1758        fields = {
1759                   "pykotaBalance" : { "operator" : "-", "value" : balance, "convert" : float },
1760                   "pykotaPageCounter" : { "operator" : "+", "value" : pagecounter, "convert" : int },
1761                 }
1762        return self.doModify(code.ident, fields)         
Note: See TracBrowser for help on using the browser.