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

Revision 2751, 93.1 kB (checked in by jerome, 18 years ago)

Logs something instead of failing when an entry to delete is missing : normal case if you delete printers and users at the same time in two different xterms.

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