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

Revision 3086, 97.7 kB (checked in by jerome, 17 years ago)

Fixed a big problem with the user's overcharging factor in the LDAP backend.

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