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

Revision 2380, 74.9 kB (checked in by jerome, 19 years ago)

Adding, deleting, or listing billing codes now works with LDAP too (finally !)
Only modifying the counters and balance is not done yet.
Severity : be patient, all should be fine before August 1st :-)

  • 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 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
36from mx import DateTime
37
38from pykota.storage import PyKotaStorageError, BaseStorage, StorageObject, \
39                           StorageUser, StorageGroup, StoragePrinter, \
40                           StorageJob, StorageLastJob, StorageUserPQuota, \
41                           StorageGroupPQuota, StorageBillingCode
42
43try :
44    import ldap
45    import ldap.modlist
46except ImportError :   
47    raise PyKotaStorageError, "This python version (%s) doesn't seem to have the python-ldap module installed correctly." % sys.version.split()[0]
48else :   
49    try :
50        from ldap.cidict import cidict
51    except ImportError :   
52        import UserDict
53        sys.stderr.write("ERROR: PyKota requires a newer version of python-ldap. Workaround activated. Please upgrade python-ldap !\n")
54        class cidict(UserDict.UserDict) :
55            pass # Fake it all, and don't care for case insensitivity : users who need it will have to upgrade.
56   
57class Storage(BaseStorage) :
58    def __init__(self, pykotatool, host, dbname, user, passwd) :
59        """Opens the LDAP connection."""
60        self.savedtool = pykotatool
61        self.savedhost = host
62        self.saveddbname = dbname
63        self.saveduser = user
64        self.savedpasswd = passwd
65        self.secondStageInit()
66       
67    def secondStageInit(self) :   
68        """Second stage initialisation."""
69        BaseStorage.__init__(self, self.savedtool)
70        self.info = self.tool.config.getLDAPInfo()
71        message = ""
72        for tryit in range(3) :
73            try :
74                self.database = ldap.initialize(self.savedhost) 
75                if self.info["ldaptls"] :
76                    # we want TLS
77                    ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.info["cacert"])
78                    self.database.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
79                    self.database.start_tls_s()
80                self.database.simple_bind_s(self.saveduser, self.savedpasswd)
81                self.basedn = self.saveddbname
82            except ldap.SERVER_DOWN :   
83                message = "LDAP backend for PyKota seems to be down !"
84                self.tool.printInfo("%s" % message, "error")
85                self.tool.printInfo("Trying again in 2 seconds...", "warn")
86                time.sleep(2)
87            except ldap.LDAPError :   
88                message = "Unable to connect to LDAP server %s as %s." % (self.savedhost, self.saveduser)
89                self.tool.printInfo("%s" % message, "error")
90                self.tool.printInfo("Trying again in 2 seconds...", "warn")
91                time.sleep(2)
92            else :   
93                self.useldapcache = self.tool.config.getLDAPCache()
94                if self.useldapcache :
95                    self.tool.logdebug("Low-Level LDAP Caching enabled.")
96                    self.ldapcache = {} # low-level cache specific to LDAP backend
97                self.closed = 0
98                self.tool.logdebug("Database opened (host=%s, dbname=%s, user=%s)" % (self.savedhost, self.saveddbname, self.saveduser))
99                return # All is fine here.
100        raise PyKotaStorageError, message         
101           
102    def close(self) :   
103        """Closes the database connection."""
104        if not self.closed :
105            self.database.unbind_s()
106            self.closed = 1
107            self.tool.logdebug("Database closed.")
108       
109    def genUUID(self) :   
110        """Generates an unique identifier.
111       
112           TODO : this one is not unique accross several print servers, but should be sufficient for testing.
113        """
114        return md5.md5("%s" % time.time()).hexdigest()
115       
116    def normalizeFields(self, fields) :   
117        """Ensure all items are lists."""
118        for (k, v) in fields.items() :
119            if type(v) not in (types.TupleType, types.ListType) :
120                if not v :
121                    del fields[k]
122                else :   
123                    fields[k] = [ v ]
124        return fields       
125       
126    def beginTransaction(self) :   
127        """Starts a transaction."""
128        self.tool.logdebug("Transaction begins... WARNING : No transactions in LDAP !")
129       
130    def commitTransaction(self) :   
131        """Commits a transaction."""
132        self.tool.logdebug("Transaction committed. WARNING : No transactions in LDAP !")
133       
134    def rollbackTransaction(self) :     
135        """Rollbacks a transaction."""
136        self.tool.logdebug("Transaction aborted. WARNING : No transaction in LDAP !")
137       
138    def doSearch(self, key, fields=None, base="", scope=ldap.SCOPE_SUBTREE, flushcache=0) :
139        """Does an LDAP search query."""
140        message = ""
141        for tryit in range(3) :
142            try :
143                base = base or self.basedn
144                if self.useldapcache :
145                    # Here we overwrite the fields the app want, to try and
146                    # retrieve ALL user defined attributes ("*")
147                    # + the createTimestamp attribute, needed by job history
148                    #
149                    # This may not work with all LDAP servers
150                    # but works at least in OpenLDAP (2.1.25)
151                    # and iPlanet Directory Server (5.1 SP3)
152                    fields = ["*", "createTimestamp"]         
153                   
154                if self.useldapcache and (not flushcache) and (scope == ldap.SCOPE_BASE) and self.ldapcache.has_key(base) :
155                    entry = self.ldapcache[base]
156                    self.tool.logdebug("LDAP cache hit %s => %s" % (base, entry))
157                    result = [(base, entry)]
158                else :
159                    self.tool.logdebug("QUERY : Filter : %s, BaseDN : %s, Scope : %s, Attributes : %s" % (key, base, scope, fields))
160                    result = self.database.search_s(base, scope, key, fields)
161            except ldap.NO_SUCH_OBJECT, msg :       
162                raise PyKotaStorageError, (_("Search base %s doesn't seem to exist. Probable misconfiguration. Please double check /etc/pykota/pykota.conf : %s") % (base, msg))
163            except ldap.LDAPError, msg :   
164                message = (_("Search for %s(%s) from %s(scope=%s) returned no answer.") % (key, fields, base, scope)) + " : %s" % str(msg)
165                self.tool.printInfo("LDAP error : %s" % message, "error")
166                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
167                self.close()
168                self.secondStageInit()
169            else :     
170                self.tool.logdebug("QUERY : Result : %s" % result)
171                result = [ (dn, cidict(attrs)) for (dn, attrs) in result ]
172                if self.useldapcache :
173                    for (dn, attributes) in result :
174                        self.tool.logdebug("LDAP cache store %s => %s" % (dn, attributes))
175                        self.ldapcache[dn] = attributes
176                return result
177        raise PyKotaStorageError, message
178           
179    def doAdd(self, dn, fields) :
180        """Adds an entry in the LDAP directory."""
181        fields = self.normalizeFields(cidict(fields))
182        message = ""
183        for tryit in range(3) :
184            try :
185                self.tool.logdebug("QUERY : ADD(%s, %s)" % (dn, str(fields)))
186                entry = ldap.modlist.addModlist(fields)
187                self.tool.logdebug("%s" % entry)
188                self.database.add_s(dn, entry)
189            except ldap.LDAPError, msg :
190                message = (_("Problem adding LDAP entry (%s, %s)") % (dn, str(fields))) + " : %s" % str(msg)
191                self.tool.printInfo("LDAP error : %s" % message, "error")
192                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
193                self.close()
194                self.secondStageInit()
195            else :
196                if self.useldapcache :
197                    self.tool.logdebug("LDAP cache add %s => %s" % (dn, fields))
198                    self.ldapcache[dn] = fields
199                return dn
200        raise PyKotaStorageError, message
201           
202    def doDelete(self, dn) :
203        """Deletes an entry from the LDAP directory."""
204        message = ""
205        for tryit in range(3) :
206            try :
207                self.tool.logdebug("QUERY : Delete(%s)" % dn)
208                self.database.delete_s(dn)
209            except ldap.LDAPError, msg :
210                message = (_("Problem deleting LDAP entry (%s)") % dn) + " : %s" % str(msg)
211                self.tool.printInfo("LDAP error : %s" % message, "error")
212                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
213                self.close()
214                self.secondStageInit()
215            else :   
216                if self.useldapcache :
217                    try :
218                        self.tool.logdebug("LDAP cache del %s" % dn)
219                        del self.ldapcache[dn]
220                    except KeyError :   
221                        pass
222                return       
223        raise PyKotaStorageError, message
224           
225    def doModify(self, dn, fields, ignoreold=1, flushcache=0) :
226        """Modifies an entry in the LDAP directory."""
227        fields = cidict(fields)
228        for tryit in range(3) :
229            try :
230                # TODO : take care of, and update LDAP specific cache
231                if self.useldapcache and not flushcache :
232                    if self.ldapcache.has_key(dn) :
233                        old = self.ldapcache[dn]
234                        self.tool.logdebug("LDAP cache hit %s => %s" % (dn, old))
235                        oldentry = {}
236                        for (k, v) in old.items() :
237                            if k != "createTimestamp" :
238                                oldentry[k] = v
239                    else :   
240                        self.tool.logdebug("LDAP cache miss %s" % dn)
241                        oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE)[0][1]
242                else :       
243                    oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE, flushcache=flushcache)[0][1]
244                for (k, v) in fields.items() :
245                    if type(v) == type({}) :
246                        try :
247                            oldvalue = v["convert"](oldentry.get(k, [0])[0])
248                        except ValueError :   
249                            self.tool.logdebug("Error converting %s with %s(%s)" % (oldentry.get(k), k, v))
250                            oldvalue = 0
251                        if v["operator"] == '+' :
252                            newvalue = oldvalue + v["value"]
253                        else :   
254                            newvalue = oldvalue - v["value"]
255                        fields[k] = str(newvalue)
256                fields = self.normalizeFields(fields)
257                self.tool.logdebug("QUERY : Modify(%s, %s ==> %s)" % (dn, oldentry, fields))
258                entry = ldap.modlist.modifyModlist(oldentry, fields, ignore_oldexistent=ignoreold)
259                modentry = []
260                for (mop, mtyp, mval) in entry :
261                    if mtyp and (mtyp.lower() != "createtimestamp") :
262                        modentry.append((mop, mtyp, mval))
263                self.tool.logdebug("MODIFY : %s ==> %s ==> %s" % (fields, entry, modentry))
264                if modentry :
265                    self.database.modify_s(dn, modentry)
266            except ldap.LDAPError, msg :
267                message = (_("Problem modifying LDAP entry (%s, %s)") % (dn, fields)) + " : %s" % str(msg)
268                self.tool.printInfo("LDAP error : %s" % message, "error")
269                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
270                self.close()
271                self.secondStageInit()
272            else :
273                if self.useldapcache :
274                    cachedentry = self.ldapcache[dn]
275                    for (mop, mtyp, mval) in entry :
276                        if mop in (ldap.MOD_ADD, ldap.MOD_REPLACE) :
277                            cachedentry[mtyp] = mval
278                        else :
279                            try :
280                                del cachedentry[mtyp]
281                            except KeyError :   
282                                pass
283                    self.tool.logdebug("LDAP cache update %s => %s" % (dn, cachedentry))
284                return dn
285        raise PyKotaStorageError, message
286           
287    def filterNames(self, records, attribute) :       
288        """Returns a list of 'attribute' from a list of records.
289       
290           Logs any missing attribute.
291        """   
292        result = []
293        for (dn, record) in records :
294            attrval = record.get(attribute, [None])[0]
295            if attrval is None :
296                self.tool.printInfo("Object %s has no %s attribute !" % (dn, attribute), "error")
297            else :   
298                result.append(attrval)
299        return result       
300               
301    def getAllPrintersNames(self, printername=None) :   
302        """Extracts all printer names or only the printers' names matching the optional parameter."""
303        printernames = []
304        ldapfilter = "objectClass=pykotaPrinter"
305        if printername :
306            ldapfilter = "(&(%s)(pykotaPrinterName=%s))" % (ldapfilter, printername)
307        result = self.doSearch(ldapfilter, ["pykotaPrinterName"], base=self.info["printerbase"])
308        if result :
309            printernames = self.filterNames(result, "pykotaPrinterName")
310        return printernames
311       
312    def getAllUsersNames(self, username=None) :   
313        """Extracts all user names or only the users' names matching the optional parameter."""
314        usernames = []
315        ldapfilter = "objectClass=pykotaAccount"
316        if username :
317            ldapfilter = "(&(%s)(pykotaUserName=%s))" % (ldapfilter, username)
318        result = self.doSearch(ldapfilter, ["pykotaUserName"], base=self.info["userbase"])
319        if result :
320            usernames = self.filterNames(result, "pykotaUserName")
321        return usernames
322       
323    def getAllGroupsNames(self, groupname=None) :   
324        """Extracts all group names or only the groups' names matching the optional parameter."""
325        groupnames = []
326        ldapfilter = "objectClass=pykotaGroup"
327        if groupname :
328            ldapfilter = "(&(%s)(pykotaGroupName=%s))" % (ldapfilter, groupname)
329        result = self.doSearch(ldapfilter, ["pykotaGroupName"], base=self.info["groupbase"])
330        if result :
331            groupnames = self.filterNames(result, "pykotaGroupName")
332        return groupnames
333       
334    def getUserNbJobsFromHistory(self, user) :
335        """Returns the number of jobs the user has in history."""
336        result = self.doSearch("(&(pykotaUserName=%s)(objectClass=pykotaJob))" % user.Name, None, base=self.info["jobbase"])
337        return len(result)
338       
339    def getUserFromBackend(self, username) :   
340        """Extracts user information given its name."""
341        user = StorageUser(self, username)
342        result = self.doSearch("(&(objectClass=pykotaAccount)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["userrdn"], username), ["pykotaUserName", "pykotaLimitBy", self.info["usermail"], "pykotaOverCharge"], base=self.info["userbase"])
343        if result :
344            fields = result[0][1]
345            user.ident = result[0][0]
346            user.Name = fields.get("pykotaUserName", [username])[0] 
347            user.Email = fields.get(self.info["usermail"], [None])[0]
348            user.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
349            user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
350            result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["balancerdn"], username), ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], base=self.info["balancebase"])
351            if not result :
352                raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
353            else :
354                fields = result[0][1]
355                user.idbalance = result[0][0]
356                user.AccountBalance = fields.get("pykotaBalance")
357                if user.AccountBalance is not None :
358                    if user.AccountBalance[0].upper() == "NONE" :
359                        user.AccountBalance = None
360                    else :   
361                        user.AccountBalance = float(user.AccountBalance[0])
362                user.AccountBalance = user.AccountBalance or 0.0       
363                user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
364                if user.LifeTimePaid is not None :
365                    if user.LifeTimePaid[0].upper() == "NONE" :
366                        user.LifeTimePaid = None
367                    else :   
368                        user.LifeTimePaid = float(user.LifeTimePaid[0])
369                user.LifeTimePaid = user.LifeTimePaid or 0.0       
370                user.Payments = []
371                for payment in fields.get("pykotaPayments", []) :
372                    (date, amount) = payment.split(" # ")
373                    user.Payments.append((date, float(amount)))
374            user.Exists = 1
375        return user
376       
377    def getGroupFromBackend(self, groupname) :   
378        """Extracts group information given its name."""
379        group = StorageGroup(self, groupname)
380        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % (groupname, self.info["grouprdn"], groupname), ["pykotaGroupName", "pykotaLimitBy"], base=self.info["groupbase"])
381        if result :
382            fields = result[0][1]
383            group.ident = result[0][0]
384            group.Name = fields.get("pykotaGroupName", [groupname])[0] 
385            group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
386            group.AccountBalance = 0.0
387            group.LifeTimePaid = 0.0
388            for member in self.getGroupMembers(group) :
389                if member.Exists :
390                    group.AccountBalance += member.AccountBalance
391                    group.LifeTimePaid += member.LifeTimePaid
392            group.Exists = 1
393        return group
394       
395    def getPrinterFromBackend(self, printername) :       
396        """Extracts printer information given its name : returns first matching printer."""
397        printer = StoragePrinter(self, printername)
398        result = self.doSearch("(&(objectClass=pykotaPrinter)(|(pykotaPrinterName=%s)(%s=%s)))" % (printername, self.info["printerrdn"], printername), ["pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerJob", "uniqueMember", "description"], base=self.info["printerbase"])
399        if result :
400            fields = result[0][1]       # take only first matching printer, ignore the rest
401            printer.ident = result[0][0]
402            printer.Name = fields.get("pykotaPrinterName", [printername])[0] 
403            printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0])
404            printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0])
405            printer.uniqueMember = fields.get("uniqueMember", [])
406            printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
407            printer.Exists = 1
408        return printer   
409       
410    def getUserPQuotaFromBackend(self, user, printer) :       
411        """Extracts a user print quota."""
412        userpquota = StorageUserPQuota(self, user, printer)
413        if printer.Exists and user.Exists :
414            if self.info["userquotabase"].lower() == "user" :
415                base = user.ident
416            else :   
417                base = self.info["userquotabase"]
418            result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s)(pykotaPrinterName=%s))" % (user.Name, printer.Name), ["pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], base=base)
419            if result :
420                fields = result[0][1]
421                userpquota.ident = result[0][0]
422                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
423                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
424                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
425                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
426                if userpquota.SoftLimit is not None :
427                    if userpquota.SoftLimit[0].upper() == "NONE" :
428                        userpquota.SoftLimit = None
429                    else :   
430                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
431                userpquota.HardLimit = fields.get("pykotaHardLimit")
432                if userpquota.HardLimit is not None :
433                    if userpquota.HardLimit[0].upper() == "NONE" :
434                        userpquota.HardLimit = None
435                    elif userpquota.HardLimit is not None :   
436                        userpquota.HardLimit = int(userpquota.HardLimit[0])
437                userpquota.DateLimit = fields.get("pykotaDateLimit")
438                if userpquota.DateLimit is not None :
439                    if userpquota.DateLimit[0].upper() == "NONE" : 
440                        userpquota.DateLimit = None
441                    else :   
442                        userpquota.DateLimit = userpquota.DateLimit[0]
443                userpquota.Exists = 1
444        return userpquota
445       
446    def getGroupPQuotaFromBackend(self, group, printer) :       
447        """Extracts a group print quota."""
448        grouppquota = StorageGroupPQuota(self, group, printer)
449        if group.Exists :
450            if self.info["groupquotabase"].lower() == "group" :
451                base = group.ident
452            else :   
453                base = self.info["groupquotabase"]
454            result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s)(pykotaPrinterName=%s))" % (group.Name, printer.Name), ["pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit"], base=base)
455            if result :
456                fields = result[0][1]
457                grouppquota.ident = result[0][0]
458                grouppquota.SoftLimit = fields.get("pykotaSoftLimit")
459                if grouppquota.SoftLimit is not None :
460                    if grouppquota.SoftLimit[0].upper() == "NONE" :
461                        grouppquota.SoftLimit = None
462                    else :   
463                        grouppquota.SoftLimit = int(grouppquota.SoftLimit[0])
464                grouppquota.HardLimit = fields.get("pykotaHardLimit")
465                if grouppquota.HardLimit is not None :
466                    if grouppquota.HardLimit[0].upper() == "NONE" :
467                        grouppquota.HardLimit = None
468                    else :   
469                        grouppquota.HardLimit = int(grouppquota.HardLimit[0])
470                grouppquota.DateLimit = fields.get("pykotaDateLimit")
471                if grouppquota.DateLimit is not None :
472                    if grouppquota.DateLimit[0].upper() == "NONE" : 
473                        grouppquota.DateLimit = None
474                    else :   
475                        grouppquota.DateLimit = grouppquota.DateLimit[0]
476                grouppquota.PageCounter = 0
477                grouppquota.LifePageCounter = 0
478                usernamesfilter = "".join(["(pykotaUserName=%s)" % member.Name for member in self.getGroupMembers(group)])
479                if usernamesfilter :
480                    usernamesfilter = "(|%s)" % usernamesfilter
481                if self.info["userquotabase"].lower() == "user" :
482                    base = self.info["userbase"]
483                else :
484                    base = self.info["userquotabase"]
485                result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)%s)" % (printer.Name, usernamesfilter), ["pykotaPageCounter", "pykotaLifePageCounter"], base=base)
486                if result :
487                    for userpquota in result :   
488                        grouppquota.PageCounter += int(userpquota[1].get("pykotaPageCounter", [0])[0] or 0)
489                        grouppquota.LifePageCounter += int(userpquota[1].get("pykotaLifePageCounter", [0])[0] or 0)
490                grouppquota.Exists = 1
491        return grouppquota
492       
493    def getPrinterLastJobFromBackend(self, printer) :       
494        """Extracts a printer's last job information."""
495        lastjob = StorageLastJob(self, printer)
496        result = self.doSearch("(&(objectClass=pykotaLastjob)(|(pykotaPrinterName=%s)(%s=%s)))" % (printer.Name, self.info["printerrdn"], printer.Name), ["pykotaLastJobIdent"], base=self.info["lastjobbase"])
497        if result :
498            lastjob.lastjobident = result[0][0]
499            lastjobident = result[0][1]["pykotaLastJobIdent"][0]
500            result = None
501            try :
502                result = self.doSearch("objectClass=pykotaJob", [ "pykotaJobSizeBytes", 
503                                                                  "pykotaHostName", 
504                                                                  "pykotaUserName", 
505                                                                  "pykotaPrinterName", 
506                                                                  "pykotaJobId", 
507                                                                  "pykotaPrinterPageCounter", 
508                                                                  "pykotaJobSize", 
509                                                                  "pykotaAction", 
510                                                                  "pykotaJobPrice", 
511                                                                  "pykotaFileName", 
512                                                                  "pykotaTitle", 
513                                                                  "pykotaCopies", 
514                                                                  "pykotaOptions", 
515                                                                  "pykotaBillingCode", 
516                                                                  "pykotaPages", 
517                                                                  "pykotaMD5Sum", 
518                                                                  "createTimestamp" ], 
519                                                                base="cn=%s,%s" % (lastjobident, self.info["jobbase"]), scope=ldap.SCOPE_BASE)
520            except PyKotaStorageError :   
521                pass # Last job entry exists, but job probably doesn't exist anymore.
522            if result :
523                fields = result[0][1]
524                lastjob.ident = result[0][0]
525                lastjob.JobId = fields.get("pykotaJobId")[0]
526                lastjob.UserName = fields.get("pykotaUserName")[0]
527                lastjob.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0])
528                try :
529                    lastjob.JobSize = int(fields.get("pykotaJobSize", [0])[0])
530                except ValueError :   
531                    lastjob.JobSize = None
532                try :   
533                    lastjob.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
534                except ValueError :   
535                    lastjob.JobPrice = None
536                lastjob.JobAction = fields.get("pykotaAction", [""])[0]
537                lastjob.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
538                lastjob.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
539                lastjob.JobCopies = int(fields.get("pykotaCopies", [0])[0])
540                lastjob.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
541                lastjob.JobHostName = fields.get("pykotaHostName", [""])[0]
542                lastjob.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
543                lastjob.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
544                lastjob.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
545                lastjob.JobPages = fields.get("pykotaPages", [""])[0]
546                if lastjob.JobTitle == lastjob.JobFileName == lastjob.JobOptions == "hidden" :
547                    (lastjob.JobTitle, lastjob.JobFileName, lastjob.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
548                date = fields.get("createTimestamp", ["19700101000000"])[0]
549                year = int(date[:4])
550                month = int(date[4:6])
551                day = int(date[6:8])
552                hour = int(date[8:10])
553                minute = int(date[10:12])
554                second = int(date[12:14])
555                lastjob.JobDate = "%04i-%02i-%02i %02i:%02i:%02i" % (year, month, day, hour, minute, second)
556                lastjob.Exists = 1
557        return lastjob
558       
559    def getGroupMembersFromBackend(self, group) :       
560        """Returns the group's members list."""
561        groupmembers = []
562        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % (group.Name, self.info["grouprdn"], group.Name), [self.info["groupmembers"]], base=self.info["groupbase"])
563        if result :
564            for username in result[0][1].get(self.info["groupmembers"], []) :
565                groupmembers.append(self.getUser(username))
566        return groupmembers       
567       
568    def getUserGroupsFromBackend(self, user) :       
569        """Returns the user's groups list."""
570        groups = []
571        result = self.doSearch("(&(objectClass=pykotaGroup)(%s=%s))" % (self.info["groupmembers"], user.Name), [self.info["grouprdn"], "pykotaGroupName", "pykotaLimitBy"], base=self.info["groupbase"])
572        if result :
573            for (groupid, fields) in result :
574                groupname = (fields.get("pykotaGroupName", [None]) or fields.get(self.info["grouprdn"], [None]))[0]
575                group = self.getFromCache("GROUPS", groupname)
576                if group is None :
577                    group = StorageGroup(self, groupname)
578                    group.ident = groupid
579                    group.LimitBy = fields.get("pykotaLimitBy")
580                    if group.LimitBy is not None :
581                        group.LimitBy = group.LimitBy[0]
582                    else :   
583                        group.LimitBy = "quota"
584                    group.AccountBalance = 0.0
585                    group.LifeTimePaid = 0.0
586                    for member in self.getGroupMembers(group) :
587                        if member.Exists :
588                            group.AccountBalance += member.AccountBalance
589                            group.LifeTimePaid += member.LifeTimePaid
590                    group.Exists = 1
591                    self.cacheEntry("GROUPS", group.Name, group)
592                groups.append(group)
593        return groups       
594       
595    def getParentPrintersFromBackend(self, printer) :   
596        """Get all the printer groups this printer is a member of."""
597        pgroups = []
598        result = self.doSearch("(&(objectClass=pykotaPrinter)(uniqueMember=%s))" % printer.ident, ["pykotaPrinterName"], base=self.info["printerbase"])
599        if result :
600            for (printerid, fields) in result :
601                if printerid != printer.ident : # In case of integrity violation.
602                    parentprinter = self.getPrinter(fields.get("pykotaPrinterName")[0])
603                    if parentprinter.Exists :
604                        pgroups.append(parentprinter)
605        return pgroups
606       
607    def getMatchingPrinters(self, printerpattern) :
608        """Returns the list of all printers for which name matches a certain pattern."""
609        printers = []
610        # see comment at the same place in pgstorage.py
611        result = self.doSearch("(&(objectClass=pykotaPrinter)(|%s))" % "".join(["(pykotaPrinterName=%s)(%s=%s)" % (pname, self.info["printerrdn"], pname) for pname in printerpattern.split(",")]), ["pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerJob", "uniqueMember", "description"], base=self.info["printerbase"])
612        if result :
613            for (printerid, fields) in result :
614                printername = fields.get("pykotaPrinterName", [""])[0] or fields.get(self.info["printerrdn"], [""])[0]
615                printer = StoragePrinter(self, printername)
616                printer.ident = printerid
617                printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0] or 0.0)
618                printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0] or 0.0)
619                printer.uniqueMember = fields.get("uniqueMember", [])
620                printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
621                printer.Exists = 1
622                printers.append(printer)
623                self.cacheEntry("PRINTERS", printer.Name, printer)
624        return printers       
625       
626    def getPrinterUsersAndQuotas(self, printer, names=["*"]) :       
627        """Returns the list of users who uses a given printer, along with their quotas."""
628        usersandquotas = []
629        if self.info["userquotabase"].lower() == "user" :
630           base = self.info["userbase"]
631        else :
632           base = self.info["userquotabase"]
633        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % (printer.Name, "".join(["(pykotaUserName=%s)" % uname for uname in names])), ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], base=base)
634        if result :
635            for (userquotaid, fields) in result :
636                user = self.getUser(fields.get("pykotaUserName")[0])
637                userpquota = StorageUserPQuota(self, user, printer)
638                userpquota.ident = userquotaid
639                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
640                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
641                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
642                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
643                if userpquota.SoftLimit is not None :
644                    if userpquota.SoftLimit[0].upper() == "NONE" :
645                        userpquota.SoftLimit = None
646                    else :   
647                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
648                userpquota.HardLimit = fields.get("pykotaHardLimit")
649                if userpquota.HardLimit is not None :
650                    if userpquota.HardLimit[0].upper() == "NONE" :
651                        userpquota.HardLimit = None
652                    elif userpquota.HardLimit is not None :   
653                        userpquota.HardLimit = int(userpquota.HardLimit[0])
654                userpquota.DateLimit = fields.get("pykotaDateLimit")
655                if userpquota.DateLimit is not None :
656                    if userpquota.DateLimit[0].upper() == "NONE" : 
657                        userpquota.DateLimit = None
658                    else :   
659                        userpquota.DateLimit = userpquota.DateLimit[0]
660                userpquota.Exists = 1
661                usersandquotas.append((user, userpquota))
662                self.cacheEntry("USERPQUOTAS", "%s@%s" % (user.Name, printer.Name), userpquota)
663        usersandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
664        return usersandquotas
665               
666    def getPrinterGroupsAndQuotas(self, printer, names=["*"]) :       
667        """Returns the list of groups which uses a given printer, along with their quotas."""
668        groupsandquotas = []
669        if self.info["groupquotabase"].lower() == "group" :
670           base = self.info["groupbase"]
671        else :
672           base = self.info["groupquotabase"]
673        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s)(|%s))" % (printer.Name, "".join(["(pykotaGroupName=%s)" % gname for gname in names])), ["pykotaGroupName"], base=base)
674        if result :
675            for (groupquotaid, fields) in result :
676                group = self.getGroup(fields.get("pykotaGroupName")[0])
677                grouppquota = self.getGroupPQuota(group, printer)
678                groupsandquotas.append((group, grouppquota))
679        groupsandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
680        return groupsandquotas
681       
682    def addPrinter(self, printername) :       
683        """Adds a printer to the quota storage, returns it."""
684        fields = { self.info["printerrdn"] : printername,
685                   "objectClass" : ["pykotaObject", "pykotaPrinter"],
686                   "cn" : printername,
687                   "pykotaPrinterName" : printername,
688                   "pykotaPricePerPage" : "0.0",
689                   "pykotaPricePerJob" : "0.0",
690                 } 
691        dn = "%s=%s,%s" % (self.info["printerrdn"], printername, self.info["printerbase"])
692        self.doAdd(dn, fields)
693        return self.getPrinter(printername)
694       
695    def addUser(self, user) :       
696        """Adds a user to the quota storage, returns it."""
697        newfields = {
698                       "pykotaUserName" : user.Name,
699                       "pykotaLimitBy" : (user.LimitBy or "quota"),
700                       "pykotaOverCharge" : str(user.OverCharge),
701                    }   
702                       
703        if user.Email :
704            newfields.update({self.info["usermail"]: user.Email})
705        mustadd = 1
706        if self.info["newuser"].lower() != 'below' :
707            try :
708                (where, action) = [s.strip() for s in self.info["newuser"].split(",")]
709            except ValueError :
710                (where, action) = (self.info["newuser"].strip(), "fail")
711            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % (where, self.info["userrdn"], user.Name), None, base=self.info["userbase"])
712            if result :
713                (dn, fields) = result[0]
714                oc = fields.get("objectClass", fields.get("objectclass", []))
715                oc.extend(["pykotaAccount", "pykotaAccountBalance"])
716                fields.update(newfields)
717                fields.update({ "pykotaBalance" : str(user.AccountBalance or 0.0),
718                                "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), })   
719                self.doModify(dn, fields)
720                mustadd = 0
721            else :
722                message = _("Unable to find an existing objectClass %s entry with %s=%s to attach pykotaAccount objectClass") % (where, self.info["userrdn"], user.Name)
723                if action.lower() == "warn" :   
724                    self.tool.printInfo(_("%s. A new entry will be created instead.") % message, "warn")
725                else : # 'fail' or incorrect setting
726                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
727               
728        if mustadd :
729            if self.info["userbase"] == self.info["balancebase"] :           
730                fields = { self.info["userrdn"] : user.Name,
731                           "objectClass" : ["pykotaObject", "pykotaAccount", "pykotaAccountBalance"],
732                           "cn" : user.Name,
733                           "pykotaBalance" : str(user.AccountBalance or 0.0),
734                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
735                         } 
736            else :             
737                fields = { self.info["userrdn"] : user.Name,
738                           "objectClass" : ["pykotaObject", "pykotaAccount"],
739                           "cn" : user.Name,
740                         } 
741            fields.update(newfields)         
742            dn = "%s=%s,%s" % (self.info["userrdn"], user.Name, self.info["userbase"])
743            self.doAdd(dn, fields)
744            if self.info["userbase"] != self.info["balancebase"] :           
745                fields = { self.info["balancerdn"] : user.Name,
746                           "objectClass" : ["pykotaObject", "pykotaAccountBalance"],
747                           "cn" : user.Name,
748                           "pykotaBalance" : str(user.AccountBalance or 0.0),
749                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
750                         } 
751                dn = "%s=%s,%s" % (self.info["balancerdn"], user.Name, self.info["balancebase"])
752                self.doAdd(dn, fields)
753           
754        return self.getUser(user.Name)
755       
756    def addGroup(self, group) :       
757        """Adds a group to the quota storage, returns it."""
758        newfields = { 
759                      "pykotaGroupName" : group.Name,
760                      "pykotaLimitBy" : (group.LimitBy or "quota"),
761                    } 
762        mustadd = 1
763        if self.info["newgroup"].lower() != 'below' :
764            try :
765                (where, action) = [s.strip() for s in self.info["newgroup"].split(",")]
766            except ValueError :
767                (where, action) = (self.info["newgroup"].strip(), "fail")
768            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % (where, self.info["grouprdn"], group.Name), None, base=self.info["groupbase"])
769            if result :
770                (dn, fields) = result[0]
771                oc = fields.get("objectClass", fields.get("objectclass", []))
772                oc.extend(["pykotaGroup"])
773                fields.update(newfields)
774                self.doModify(dn, fields)
775                mustadd = 0
776            else :
777                message = _("Unable to find an existing entry to attach pykotaGroup objectclass %s") % group.Name
778                if action.lower() == "warn" :   
779                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
780                else : # 'fail' or incorrect setting
781                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
782               
783        if mustadd :
784            fields = { self.info["grouprdn"] : group.Name,
785                       "objectClass" : ["pykotaObject", "pykotaGroup"],
786                       "cn" : group.Name,
787                     } 
788            fields.update(newfields)         
789            dn = "%s=%s,%s" % (self.info["grouprdn"], group.Name, self.info["groupbase"])
790            self.doAdd(dn, fields)
791        return self.getGroup(group.Name)
792       
793    def addUserToGroup(self, user, group) :   
794        """Adds an user to a group."""
795        if user.Name not in [u.Name for u in self.getGroupMembers(group)] :
796            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
797            if result :
798                fields = result[0][1]
799                if not fields.has_key(self.info["groupmembers"]) :
800                    fields[self.info["groupmembers"]] = []
801                fields[self.info["groupmembers"]].append(user.Name)
802                self.doModify(group.ident, fields)
803                group.Members.append(user)
804               
805    def addUserPQuota(self, user, printer) :
806        """Initializes a user print quota on a printer."""
807        uuid = self.genUUID()
808        fields = { "cn" : uuid,
809                   "objectClass" : ["pykotaObject", "pykotaUserPQuota"],
810                   "pykotaUserName" : user.Name,
811                   "pykotaPrinterName" : printer.Name,
812                   "pykotaDateLimit" : "None",
813                   "pykotaPageCounter" : "0",
814                   "pykotaLifePageCounter" : "0",
815                   "pykotaWarnCount" : "0",
816                 } 
817        if self.info["userquotabase"].lower() == "user" :
818            dn = "cn=%s,%s" % (uuid, user.ident)
819        else :   
820            dn = "cn=%s,%s" % (uuid, self.info["userquotabase"])
821        self.doAdd(dn, fields)
822        return self.getUserPQuota(user, printer)
823       
824    def addGroupPQuota(self, group, printer) :
825        """Initializes a group print quota on a printer."""
826        uuid = self.genUUID()
827        fields = { "cn" : uuid,
828                   "objectClass" : ["pykotaObject", "pykotaGroupPQuota"],
829                   "pykotaGroupName" : group.Name,
830                   "pykotaPrinterName" : printer.Name,
831                   "pykotaDateLimit" : "None",
832                 } 
833        if self.info["groupquotabase"].lower() == "group" :
834            dn = "cn=%s,%s" % (uuid, group.ident)
835        else :   
836            dn = "cn=%s,%s" % (uuid, self.info["groupquotabase"])
837        self.doAdd(dn, fields)
838        return self.getGroupPQuota(group, printer)
839       
840    def writePrinterPrices(self, printer) :   
841        """Write the printer's prices back into the storage."""
842        fields = {
843                   "pykotaPricePerPage" : str(printer.PricePerPage),
844                   "pykotaPricePerJob" : str(printer.PricePerJob),
845                 }
846        self.doModify(printer.ident, fields)
847       
848    def writePrinterDescription(self, printer) :   
849        """Write the printer's description back into the storage."""
850        fields = {
851                   "description" : self.userCharsetToDatabase(printer.Description or ""),
852                 }
853        if fields["description"] :
854            self.doModify(printer.ident, fields)
855       
856    def writeUserOverCharge(self, user, factor) :
857        """Sets the user's overcharging coefficient."""
858        fields = {
859                   "pykotaOverCharge" : str(factor),
860                 }
861        self.doModify(user.ident, fields)
862       
863    def writeUserLimitBy(self, user, limitby) :   
864        """Sets the user's limiting factor."""
865        fields = {
866                   "pykotaLimitBy" : limitby,
867                 }
868        self.doModify(user.ident, fields)         
869       
870    def writeGroupLimitBy(self, group, limitby) :   
871        """Sets the group's limiting factor."""
872        fields = {
873                   "pykotaLimitBy" : limitby,
874                 }
875        self.doModify(group.ident, fields)         
876       
877    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
878        """Sets the date limit permanently for a user print quota."""
879        fields = {
880                   "pykotaDateLimit" : datelimit,
881                 }
882        return self.doModify(userpquota.ident, fields)
883           
884    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
885        """Sets the date limit permanently for a group print quota."""
886        fields = {
887                   "pykotaDateLimit" : datelimit,
888                 }
889        return self.doModify(grouppquota.ident, fields)
890       
891    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
892        """Increase page counters for a user print quota."""
893        fields = {
894                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
895                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
896                 }
897        return self.doModify(userpquota.ident, fields)         
898       
899    def writeUserPQuotaPagesCounters(self, userpquota, newpagecounter, newlifepagecounter) :   
900        """Sets the new page counters permanently for a user print quota."""
901        fields = {
902                   "pykotaPageCounter" : str(newpagecounter),
903                   "pykotaLifePageCounter" : str(newlifepagecounter),
904                   "pykotaDateLimit" : None,
905                   "pykotaWarnCount" : "0",
906                 } 
907        return self.doModify(userpquota.ident, fields)         
908       
909    def decreaseUserAccountBalance(self, user, amount) :   
910        """Decreases user's account balance from an amount."""
911        fields = {
912                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
913                 }
914        return self.doModify(user.idbalance, fields, flushcache=1)         
915       
916    def writeUserAccountBalance(self, user, newbalance, newlifetimepaid=None) :   
917        """Sets the new account balance and eventually new lifetime paid."""
918        fields = {
919                   "pykotaBalance" : str(newbalance),
920                 }
921        if newlifetimepaid is not None :
922            fields.update({ "pykotaLifeTimePaid" : str(newlifetimepaid) })
923        return self.doModify(user.idbalance, fields)         
924           
925    def writeNewPayment(self, user, amount) :       
926        """Adds a new payment to the payments history."""
927        payments = []
928        for payment in user.Payments :
929            payments.append("%s # %s" % (payment[0], str(payment[1])))
930        payments.append("%s # %s" % (str(DateTime.now()), str(amount)))
931        fields = {
932                   "pykotaPayments" : payments,
933                 }
934        return self.doModify(user.idbalance, fields)         
935       
936    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
937        """Sets the last job's size permanently."""
938        fields = {
939                   "pykotaJobSize" : str(jobsize),
940                   "pykotaJobPrice" : str(jobprice),
941                 }
942        self.doModify(lastjob.ident, fields)         
943       
944    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) :
945        """Adds a job in a printer's history."""
946        if (not self.disablehistory) or (not printer.LastJob.Exists) :
947            uuid = self.genUUID()
948            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
949        else :   
950            uuid = printer.LastJob.ident[3:].split(",")[0]
951            dn = printer.LastJob.ident
952        if self.privacy :   
953            # For legal reasons, we want to hide the title, filename and options
954            title = filename = options = "hidden"
955        fields = {
956                   "objectClass" : ["pykotaObject", "pykotaJob"],
957                   "cn" : uuid,
958                   "pykotaUserName" : user.Name,
959                   "pykotaPrinterName" : printer.Name,
960                   "pykotaJobId" : jobid,
961                   "pykotaPrinterPageCounter" : str(pagecounter),
962                   "pykotaAction" : action,
963                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
964                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
965                   "pykotaCopies" : str(copies), 
966                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
967                   "pykotaHostName" : str(clienthost), 
968                   "pykotaJobSizeBytes" : str(jobsizebytes),
969                   "pykotaMD5Sum" : str(jobmd5sum),
970                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
971                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
972                 }
973        if (not self.disablehistory) or (not printer.LastJob.Exists) :
974            if jobsize is not None :         
975                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
976            self.doAdd(dn, fields)
977        else :   
978            # here we explicitly want to reset jobsize to 'None' if needed
979            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
980            self.doModify(dn, fields)
981           
982        if printer.LastJob.Exists :
983            fields = {
984                       "pykotaLastJobIdent" : uuid,
985                     }
986            self.doModify(printer.LastJob.lastjobident, fields)         
987        else :   
988            lastjuuid = self.genUUID()
989            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
990            fields = {
991                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
992                       "cn" : lastjuuid,
993                       "pykotaPrinterName" : printer.Name,
994                       "pykotaLastJobIdent" : uuid,
995                     } 
996            self.doAdd(lastjdn, fields)         
997           
998    def writeUserPQuotaLimits(self, userpquota, softlimit, hardlimit) :
999        """Sets soft and hard limits for a user quota."""
1000        fields = { 
1001                   "pykotaSoftLimit" : str(softlimit),
1002                   "pykotaHardLimit" : str(hardlimit),
1003                   "pykotaDateLimit" : "None",
1004                   "pykotaWarnCount" : "0",
1005                 }
1006        self.doModify(userpquota.ident, fields)
1007       
1008    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1009        """Sets the warn counter value for a user quota."""
1010        fields = { 
1011                   "pykotaWarnCount" : str(warncount or 0),
1012                 }
1013        self.doModify(userpquota.ident, fields)
1014       
1015    def increaseUserPQuotaWarnCount(self, userpquota) :
1016        """Increases the warn counter value for a user quota."""
1017        fields = {
1018                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1019                 }
1020        return self.doModify(userpquota.ident, fields)         
1021       
1022    def writeGroupPQuotaLimits(self, grouppquota, softlimit, hardlimit) :
1023        """Sets soft and hard limits for a group quota on a specific printer."""
1024        fields = { 
1025                   "pykotaSoftLimit" : str(softlimit),
1026                   "pykotaHardLimit" : str(hardlimit),
1027                   "pykotaDateLimit" : "None",
1028                 }
1029        self.doModify(grouppquota.ident, fields)
1030           
1031    def writePrinterToGroup(self, pgroup, printer) :
1032        """Puts a printer into a printer group."""
1033        if printer.ident not in pgroup.uniqueMember :
1034            pgroup.uniqueMember.append(printer.ident)
1035            fields = {
1036                       "uniqueMember" : pgroup.uniqueMember
1037                     } 
1038            self.doModify(pgroup.ident, fields)         
1039           
1040    def removePrinterFromGroup(self, pgroup, printer) :
1041        """Removes a printer from a printer group."""
1042        try :
1043            pgroup.uniqueMember.remove(printer.ident)
1044        except ValueError :   
1045            pass
1046        else :   
1047            fields = {
1048                       "uniqueMember" : pgroup.uniqueMember,
1049                     } 
1050            self.doModify(pgroup.ident, fields)         
1051           
1052    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
1053        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1054        precond = "(objectClass=pykotaJob)"
1055        where = []
1056        if user is not None :
1057            where.append("(pykotaUserName=%s)" % user.Name)
1058        if printer is not None :
1059            where.append("(pykotaPrinterName=%s)" % printer.Name)
1060        if hostname is not None :
1061            where.append("(pykotaHostName=%s)" % hostname)
1062        if billingcode is not None :
1063            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1064        if where :   
1065            where = "(&%s)" % "".join([precond] + where)
1066        else :   
1067            where = precond
1068        jobs = []   
1069        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1070                                               "pykotaHostName", 
1071                                               "pykotaUserName", 
1072                                               "pykotaPrinterName", 
1073                                               "pykotaJobId", 
1074                                               "pykotaPrinterPageCounter", 
1075                                               "pykotaAction", 
1076                                               "pykotaJobSize", 
1077                                               "pykotaJobPrice", 
1078                                               "pykotaFileName", 
1079                                               "pykotaTitle", 
1080                                               "pykotaCopies", 
1081                                               "pykotaOptions", 
1082                                               "pykotaBillingCode", 
1083                                               "pykotaPages", 
1084                                               "pykotaMD5Sum", 
1085                                               "createTimestamp" ], 
1086                                      base=self.info["jobbase"])
1087        if result :
1088            for (ident, fields) in result :
1089                job = StorageJob(self)
1090                job.ident = ident
1091                job.JobId = fields.get("pykotaJobId")[0]
1092                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1093                try :
1094                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1095                except ValueError :   
1096                    job.JobSize = None
1097                try :   
1098                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1099                except ValueError :
1100                    job.JobPrice = None
1101                job.JobAction = fields.get("pykotaAction", [""])[0]
1102                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1103                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1104                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1105                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1106                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1107                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1108                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1109                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1110                job.JobPages = fields.get("pykotaPages", [""])[0]
1111                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1112                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1113                date = fields.get("createTimestamp", ["19700101000000"])[0]
1114                year = int(date[:4])
1115                month = int(date[4:6])
1116                day = int(date[6:8])
1117                hour = int(date[8:10])
1118                minute = int(date[10:12])
1119                second = int(date[12:14])
1120                job.JobDate = "%04i%02i%02i %02i:%02i:%02i" % (year, month, day, hour, minute, second)
1121                if ((start is None) and (end is None)) or \
1122                   ((start is None) and (job.JobDate <= end)) or \
1123                   ((end is None) and (job.JobDate >= start)) or \
1124                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1125                    job.UserName = fields.get("pykotaUserName")[0]
1126                    job.PrinterName = fields.get("pykotaPrinterName")[0]
1127                    job.Exists = 1
1128                    jobs.append(job)
1129            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1130            if limit :   
1131                jobs = jobs[:int(limit)]
1132        return jobs
1133       
1134    def deleteUser(self, user) :   
1135        """Completely deletes an user from the Quota Storage."""
1136        todelete = []   
1137        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % user.Name, base=self.info["jobbase"])
1138        for (ident, fields) in result :
1139            todelete.append(ident)
1140        if self.info["userquotabase"].lower() == "user" :
1141            base = self.info["userbase"]
1142        else :
1143            base = self.info["userquotabase"]
1144        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % user.Name, ["pykotaPrinterName", "pykotaUserName"], base=base)
1145        for (ident, fields) in result :
1146            # ensure the user print quota entry will be deleted
1147            todelete.append(ident)
1148           
1149            # if last job of current printer was printed by the user
1150            # to delete, we also need to delete the printer's last job entry.
1151            printername = fields["pykotaPrinterName"][0]
1152            printer = self.getPrinter(printername)
1153            if printer.LastJob.UserName == user.Name :
1154                todelete.append(printer.LastJob.lastjobident)
1155           
1156        for ident in todelete :   
1157            self.doDelete(ident)
1158           
1159        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1160        if result :
1161            fields = result[0][1]
1162            for k in fields.keys() :
1163                if k.startswith("pykota") :
1164                    del fields[k]
1165                elif k.lower() == "objectclass" :   
1166                    todelete = []
1167                    for i in range(len(fields[k])) :
1168                        if fields[k][i].startswith("pykota") : 
1169                            todelete.append(i)
1170                    todelete.sort()       
1171                    todelete.reverse()
1172                    for i in todelete :
1173                        del fields[k][i]
1174            if fields.get("objectClass") or fields.get("objectclass") :
1175                self.doModify(user.ident, fields, ignoreold=0)       
1176            else :   
1177                self.doDelete(user.ident)
1178        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % user.Name, ["pykotaUserName"], base=self.info["balancebase"])
1179        for (ident, fields) in result :
1180            self.doDelete(ident)
1181       
1182    def deleteGroup(self, group) :   
1183        """Completely deletes a group from the Quota Storage."""
1184        if self.info["groupquotabase"].lower() == "group" :
1185            base = self.info["groupbase"]
1186        else :
1187            base = self.info["groupquotabase"]
1188        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % group.Name, ["pykotaGroupName"], base=base)
1189        for (ident, fields) in result :
1190            self.doDelete(ident)
1191        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1192        if result :
1193            fields = result[0][1]
1194            for k in fields.keys() :
1195                if k.startswith("pykota") :
1196                    del fields[k]
1197                elif k.lower() == "objectclass" :   
1198                    todelete = []
1199                    for i in range(len(fields[k])) :
1200                        if fields[k][i].startswith("pykota") : 
1201                            todelete.append(i)
1202                    todelete.sort()       
1203                    todelete.reverse()
1204                    for i in todelete :
1205                        del fields[k][i]
1206            if fields.get("objectClass") or fields.get("objectclass") :
1207                self.doModify(group.ident, fields, ignoreold=0)       
1208            else :   
1209                self.doDelete(group.ident)
1210               
1211    def deletePrinter(self, printer) :   
1212        """Completely deletes a printer from the Quota Storage."""
1213        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % printer.Name, base=self.info["lastjobbase"])
1214        for (ident, fields) in result :
1215            self.doDelete(ident)
1216        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % printer.Name, base=self.info["jobbase"])
1217        for (ident, fields) in result :
1218            self.doDelete(ident)
1219        if self.info["groupquotabase"].lower() == "group" :
1220            base = self.info["groupbase"]
1221        else :
1222            base = self.info["groupquotabase"]
1223        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % printer.Name, base=base)
1224        for (ident, fields) in result :
1225            self.doDelete(ident)
1226        if self.info["userquotabase"].lower() == "user" :
1227            base = self.info["userbase"]
1228        else :
1229            base = self.info["userquotabase"]
1230        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % printer.Name, base=base)
1231        for (ident, fields) in result :
1232            self.doDelete(ident)
1233        for parent in self.getParentPrinters(printer) : 
1234            try :
1235                parent.uniqueMember.remove(printer.ident)
1236            except ValueError :   
1237                pass
1238            else :   
1239                fields = {
1240                           "uniqueMember" : parent.uniqueMember,
1241                         } 
1242                self.doModify(parent.ident, fields)         
1243        self.doDelete(printer.ident)   
1244       
1245    def deleteBillingCode(self, code) :
1246        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1247        self.doDelete(code.ident)
1248       
1249    def extractPrinters(self, extractonly={}) :
1250        """Extracts all printer records."""
1251        pname = extractonly.get("printername")
1252        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1253        if entries :
1254            result = [ ("dn", "printername", "priceperpage", "priceperjob", "description") ]
1255            for entry in entries :
1256                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description))
1257            return result 
1258       
1259    def extractUsers(self, extractonly={}) :
1260        """Extracts all user records."""
1261        uname = extractonly.get("username")
1262        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1263        if entries :
1264            result = [ ("dn", "username", "balance", "lifetimepaid", "limitby", "email") ]
1265            for entry in entries :
1266                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email))
1267            return result 
1268       
1269    def extractBillingcodes(self, extractonly={}) :
1270        """Extracts all billing codes records."""
1271        billingcode = extractonly.get("billingcode")
1272        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1273        if entries :
1274            result = [ ("dn", "billingcode", "balance", "pagecounter", "description") ]
1275            for entry in entries :
1276                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1277            return result 
1278       
1279    def extractGroups(self, extractonly={}) :
1280        """Extracts all group records."""
1281        gname = extractonly.get("groupname")
1282        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1283        if entries :
1284            result = [ ("dn", "groupname", "limitby", "balance", "lifetimepaid") ]
1285            for entry in entries :
1286                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid))
1287            return result 
1288       
1289    def extractPayments(self, extractonly={}) :
1290        """Extracts all payment records."""
1291        uname = extractonly.get("username")
1292        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1293        if entries :
1294            result = [ ("username", "amount", "date") ]
1295            for entry in entries :
1296                for (date, amount) in entry.Payments :
1297                    result.append((entry.Name, amount, date))
1298            return result       
1299       
1300    def extractUpquotas(self, extractonly={}) :
1301        """Extracts all userpquota records."""
1302        pname = extractonly.get("printername")
1303        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1304        if entries :
1305            result = [ ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1306            uname = extractonly.get("username")
1307            for entry in entries :
1308                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1309                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1310            return result
1311       
1312    def extractGpquotas(self, extractonly={}) :
1313        """Extracts all grouppquota records."""
1314        pname = extractonly.get("printername")
1315        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1316        if entries :
1317            result = [ ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1318            gname = extractonly.get("groupname")
1319            for entry in entries :
1320                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1321                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1322            return result
1323       
1324    def extractUmembers(self, extractonly={}) :
1325        """Extracts all user groups members."""
1326        gname = extractonly.get("groupname")
1327        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1328        if entries :
1329            result = [ ("groupname", "username", "groupdn", "userdn") ]
1330            uname = extractonly.get("username")
1331            for entry in entries :
1332                for member in entry.Members :
1333                    if (uname is None) or (member.Name == uname) :
1334                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1335            return result       
1336               
1337    def extractPmembers(self, extractonly={}) :
1338        """Extracts all printer groups members."""
1339        pname = extractonly.get("printername")
1340        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1341        if entries :
1342            result = [ ("pgroupname", "printername", "pgroupdn", "printerdn") ]
1343            pgname = extractonly.get("pgroupname")
1344            for entry in entries :
1345                for parent in self.getParentPrinters(entry) :
1346                    if (pgname is None) or (parent.Name == pgname) :
1347                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1348            return result       
1349       
1350    def extractHistory(self, extractonly={}) :
1351        """Extracts all jobhistory records."""
1352        uname = extractonly.get("username")
1353        if uname :
1354            user = self.getUser(uname)
1355        else :   
1356            user = None
1357        pname = extractonly.get("printername")
1358        if pname :
1359            printer = self.getPrinter(pname)
1360        else :   
1361            printer = None
1362        startdate = extractonly.get("start")
1363        enddate = extractonly.get("end")
1364        (startdate, enddate) = self.cleanDates(startdate, enddate)
1365        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), limit=None, start=startdate, end=enddate)
1366        if entries :
1367            result = [ ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode") ] 
1368            for entry in entries :
1369                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)) 
1370            return result
1371           
1372    def getBillingCodeFromBackend(self, label) :
1373        """Extracts billing code information given its label : returns first matching billing code."""
1374        code = StorageBillingCode(self, label)
1375        ulabel = self.userCharsetToDatabase(label)
1376        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % ulabel, ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], base=self.info["billingcodebase"])
1377        if result :
1378            fields = result[0][1]       # take only first matching code, ignore the rest
1379            code.ident = result[0][0]
1380            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1381            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1382            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1383            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1384            code.Exists = 1
1385        return code   
1386       
1387    def addBillingCode(self, label) :
1388        """Adds a billing code to the quota storage, returns it."""
1389        uuid = self.genUUID()
1390        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1391        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1392                   "cn" : uuid,
1393                   "pykotaBillingCode" : self.userCharsetToDatabase(label),
1394                   "pykotaPageCounter" : "0",
1395                   "pykotaBalance" : "0.0",
1396                 } 
1397        self.doAdd(dn, fields)
1398        return self.getBillingCode(label)
1399       
1400    def writeBillingCodeDescription(self, code) :
1401        """Sets the new description for a billing code."""
1402        fields = {
1403                   "description" : self.userCharsetToDatabase(code.Description or ""), 
1404                 }
1405        if fields["description"] :
1406            self.doModify(code.ident, fields)
1407           
1408    def getMatchingBillingCodes(self, billingcodepattern) :
1409        """Returns the list of all billing codes which match a certain pattern."""
1410        codes = []
1411        result = self.doSearch("(&(objectClass=pykotaBilling)%s)" % "".join(["(pykotaBillingCode=%s)" % self.userCharsetToDatabase(bcode) for bcode in billingcodepattern.split(",")]), \
1412                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1413                                base=self.info["billingcodebase"])
1414        if result :
1415            for (codeid, fields) in result :
1416                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1417                code = StorageBillingCode(self, codename)
1418                code.ident = codeid
1419                code.BillingCode = codename
1420                code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1421                code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1422                code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1423                code.Exists = 1
1424                codes.append(code)
1425                self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1426        return codes       
1427       
1428# def setBillingCodeValues(self, code, newbalance, newpagecounter) :   
1429# def consumeBillingCode(self, code, balance, pagecounter) :
Note: See TracBrowser for help on using the browser.