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

Revision 3363, 100.2 kB (checked in by jerome, 16 years ago)

Fixed encoding problems for LDAP.

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