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

Revision 2753, 93.2 kB (checked in by jerome, 18 years ago)

Fixed removal of an user from an LDAP group.

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