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

Revision 2358, 71.9 kB (checked in by jerome, 19 years ago)

More work done on billing codes + LDAP.
Severity: minor.

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