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

Revision 2375, 73.7 kB (checked in by jerome, 19 years ago)

More work done on LDAP + billing codes.

  • 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(printer.Description or ""),
849                 }
850        if fields["description"] :
851            self.doModify(printer.ident, fields)
852       
853    def writeUserOverCharge(self, user, factor) :
854        """Sets the user's overcharging coefficient."""
855        fields = {
856                   "pykotaOverCharge" : str(factor),
857                 }
858        self.doModify(user.ident, fields)
859       
860    def writeUserLimitBy(self, user, limitby) :   
861        """Sets the user's limiting factor."""
862        fields = {
863                   "pykotaLimitBy" : limitby,
864                 }
865        self.doModify(user.ident, fields)         
866       
867    def writeGroupLimitBy(self, group, limitby) :   
868        """Sets the group's limiting factor."""
869        fields = {
870                   "pykotaLimitBy" : limitby,
871                 }
872        self.doModify(group.ident, fields)         
873       
874    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
875        """Sets the date limit permanently for a user print quota."""
876        fields = {
877                   "pykotaDateLimit" : datelimit,
878                 }
879        return self.doModify(userpquota.ident, fields)
880           
881    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
882        """Sets the date limit permanently for a group print quota."""
883        fields = {
884                   "pykotaDateLimit" : datelimit,
885                 }
886        return self.doModify(grouppquota.ident, fields)
887       
888    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
889        """Increase page counters for a user print quota."""
890        fields = {
891                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
892                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
893                 }
894        return self.doModify(userpquota.ident, fields)         
895       
896    def writeUserPQuotaPagesCounters(self, userpquota, newpagecounter, newlifepagecounter) :   
897        """Sets the new page counters permanently for a user print quota."""
898        fields = {
899                   "pykotaPageCounter" : str(newpagecounter),
900                   "pykotaLifePageCounter" : str(newlifepagecounter),
901                   "pykotaDateLimit" : None,
902                   "pykotaWarnCount" : "0",
903                 } 
904        return self.doModify(userpquota.ident, fields)         
905       
906    def decreaseUserAccountBalance(self, user, amount) :   
907        """Decreases user's account balance from an amount."""
908        fields = {
909                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
910                 }
911        return self.doModify(user.idbalance, fields, flushcache=1)         
912       
913    def writeUserAccountBalance(self, user, newbalance, newlifetimepaid=None) :   
914        """Sets the new account balance and eventually new lifetime paid."""
915        fields = {
916                   "pykotaBalance" : str(newbalance),
917                 }
918        if newlifetimepaid is not None :
919            fields.update({ "pykotaLifeTimePaid" : str(newlifetimepaid) })
920        return self.doModify(user.idbalance, fields)         
921           
922    def writeNewPayment(self, user, amount) :       
923        """Adds a new payment to the payments history."""
924        payments = []
925        for payment in user.Payments :
926            payments.append("%s # %s" % (payment[0], str(payment[1])))
927        payments.append("%s # %s" % (str(DateTime.now()), str(amount)))
928        fields = {
929                   "pykotaPayments" : payments,
930                 }
931        return self.doModify(user.idbalance, fields)         
932       
933    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
934        """Sets the last job's size permanently."""
935        fields = {
936                   "pykotaJobSize" : str(jobsize),
937                   "pykotaJobPrice" : str(jobprice),
938                 }
939        self.doModify(lastjob.ident, fields)         
940       
941    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) :
942        """Adds a job in a printer's history."""
943        if (not self.disablehistory) or (not printer.LastJob.Exists) :
944            uuid = self.genUUID()
945            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
946        else :   
947            uuid = printer.LastJob.ident[3:].split(",")[0]
948            dn = printer.LastJob.ident
949        if self.privacy :   
950            # For legal reasons, we want to hide the title, filename and options
951            title = filename = options = "hidden"
952        fields = {
953                   "objectClass" : ["pykotaObject", "pykotaJob"],
954                   "cn" : uuid,
955                   "pykotaUserName" : user.Name,
956                   "pykotaPrinterName" : printer.Name,
957                   "pykotaJobId" : jobid,
958                   "pykotaPrinterPageCounter" : str(pagecounter),
959                   "pykotaAction" : action,
960                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
961                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
962                   "pykotaCopies" : str(copies), 
963                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
964                   "pykotaHostName" : str(clienthost), 
965                   "pykotaJobSizeBytes" : str(jobsizebytes),
966                   "pykotaMD5Sum" : str(jobmd5sum),
967                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
968                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
969                 }
970        if (not self.disablehistory) or (not printer.LastJob.Exists) :
971            if jobsize is not None :         
972                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
973            self.doAdd(dn, fields)
974        else :   
975            # here we explicitly want to reset jobsize to 'None' if needed
976            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
977            self.doModify(dn, fields)
978           
979        if printer.LastJob.Exists :
980            fields = {
981                       "pykotaLastJobIdent" : uuid,
982                     }
983            self.doModify(printer.LastJob.lastjobident, fields)         
984        else :   
985            lastjuuid = self.genUUID()
986            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
987            fields = {
988                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
989                       "cn" : lastjuuid,
990                       "pykotaPrinterName" : printer.Name,
991                       "pykotaLastJobIdent" : uuid,
992                     } 
993            self.doAdd(lastjdn, fields)         
994           
995    def writeUserPQuotaLimits(self, userpquota, softlimit, hardlimit) :
996        """Sets soft and hard limits for a user quota."""
997        fields = { 
998                   "pykotaSoftLimit" : str(softlimit),
999                   "pykotaHardLimit" : str(hardlimit),
1000                   "pykotaDateLimit" : "None",
1001                   "pykotaWarnCount" : "0",
1002                 }
1003        self.doModify(userpquota.ident, fields)
1004       
1005    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1006        """Sets the warn counter value for a user quota."""
1007        fields = { 
1008                   "pykotaWarnCount" : str(warncount or 0),
1009                 }
1010        self.doModify(userpquota.ident, fields)
1011       
1012    def increaseUserPQuotaWarnCount(self, userpquota) :
1013        """Increases the warn counter value for a user quota."""
1014        fields = {
1015                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1016                 }
1017        return self.doModify(userpquota.ident, fields)         
1018       
1019    def writeGroupPQuotaLimits(self, grouppquota, softlimit, hardlimit) :
1020        """Sets soft and hard limits for a group quota on a specific printer."""
1021        fields = { 
1022                   "pykotaSoftLimit" : str(softlimit),
1023                   "pykotaHardLimit" : str(hardlimit),
1024                   "pykotaDateLimit" : "None",
1025                 }
1026        self.doModify(grouppquota.ident, fields)
1027           
1028    def writePrinterToGroup(self, pgroup, printer) :
1029        """Puts a printer into a printer group."""
1030        if printer.ident not in pgroup.uniqueMember :
1031            pgroup.uniqueMember.append(printer.ident)
1032            fields = {
1033                       "uniqueMember" : pgroup.uniqueMember
1034                     } 
1035            self.doModify(pgroup.ident, fields)         
1036           
1037    def removePrinterFromGroup(self, pgroup, printer) :
1038        """Removes a printer from a printer group."""
1039        try :
1040            pgroup.uniqueMember.remove(printer.ident)
1041        except ValueError :   
1042            pass
1043        else :   
1044            fields = {
1045                       "uniqueMember" : pgroup.uniqueMember,
1046                     } 
1047            self.doModify(pgroup.ident, fields)         
1048           
1049    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
1050        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1051        precond = "(objectClass=pykotaJob)"
1052        where = []
1053        if user is not None :
1054            where.append("(pykotaUserName=%s)" % user.Name)
1055        if printer is not None :
1056            where.append("(pykotaPrinterName=%s)" % printer.Name)
1057        if hostname is not None :
1058            where.append("(pykotaHostName=%s)" % hostname)
1059        if billingcode is not None :
1060            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1061        if where :   
1062            where = "(&%s)" % "".join([precond] + where)
1063        else :   
1064            where = precond
1065        jobs = []   
1066        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1067                                               "pykotaHostName", 
1068                                               "pykotaUserName", 
1069                                               "pykotaPrinterName", 
1070                                               "pykotaJobId", 
1071                                               "pykotaPrinterPageCounter", 
1072                                               "pykotaAction", 
1073                                               "pykotaJobSize", 
1074                                               "pykotaJobPrice", 
1075                                               "pykotaFileName", 
1076                                               "pykotaTitle", 
1077                                               "pykotaCopies", 
1078                                               "pykotaOptions", 
1079                                               "pykotaBillingCode", 
1080                                               "pykotaPages", 
1081                                               "pykotaMD5Sum", 
1082                                               "createTimestamp" ], 
1083                                      base=self.info["jobbase"])
1084        if result :
1085            for (ident, fields) in result :
1086                job = StorageJob(self)
1087                job.ident = ident
1088                job.JobId = fields.get("pykotaJobId")[0]
1089                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1090                try :
1091                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1092                except ValueError :   
1093                    job.JobSize = None
1094                try :   
1095                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1096                except ValueError :
1097                    job.JobPrice = None
1098                job.JobAction = fields.get("pykotaAction", [""])[0]
1099                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1100                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1101                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1102                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1103                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1104                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1105                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1106                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1107                job.JobPages = fields.get("pykotaPages", [""])[0]
1108                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1109                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1110                date = fields.get("createTimestamp", ["19700101000000"])[0]
1111                year = int(date[:4])
1112                month = int(date[4:6])
1113                day = int(date[6:8])
1114                hour = int(date[8:10])
1115                minute = int(date[10:12])
1116                second = int(date[12:14])
1117                job.JobDate = "%04i%02i%02i %02i:%02i:%02i" % (year, month, day, hour, minute, second)
1118                if ((start is None) and (end is None)) or \
1119                   ((start is None) and (job.JobDate <= end)) or \
1120                   ((end is None) and (job.JobDate >= start)) or \
1121                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1122                    job.UserName = fields.get("pykotaUserName")[0]
1123                    job.PrinterName = fields.get("pykotaPrinterName")[0]
1124                    job.Exists = 1
1125                    jobs.append(job)
1126            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1127            if limit :   
1128                jobs = jobs[:int(limit)]
1129        return jobs
1130       
1131    def deleteUser(self, user) :   
1132        """Completely deletes an user from the Quota Storage."""
1133        todelete = []   
1134        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % user.Name, base=self.info["jobbase"])
1135        for (ident, fields) in result :
1136            todelete.append(ident)
1137        if self.info["userquotabase"].lower() == "user" :
1138            base = self.info["userbase"]
1139        else :
1140            base = self.info["userquotabase"]
1141        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % user.Name, ["pykotaPrinterName", "pykotaUserName"], base=base)
1142        for (ident, fields) in result :
1143            # ensure the user print quota entry will be deleted
1144            todelete.append(ident)
1145           
1146            # if last job of current printer was printed by the user
1147            # to delete, we also need to delete the printer's last job entry.
1148            printername = fields["pykotaPrinterName"][0]
1149            printer = self.getPrinter(printername)
1150            if printer.LastJob.UserName == user.Name :
1151                todelete.append(printer.LastJob.lastjobident)
1152           
1153        for ident in todelete :   
1154            self.doDelete(ident)
1155           
1156        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1157        if result :
1158            fields = result[0][1]
1159            for k in fields.keys() :
1160                if k.startswith("pykota") :
1161                    del fields[k]
1162                elif k.lower() == "objectclass" :   
1163                    todelete = []
1164                    for i in range(len(fields[k])) :
1165                        if fields[k][i].startswith("pykota") : 
1166                            todelete.append(i)
1167                    todelete.sort()       
1168                    todelete.reverse()
1169                    for i in todelete :
1170                        del fields[k][i]
1171            if fields.get("objectClass") or fields.get("objectclass") :
1172                self.doModify(user.ident, fields, ignoreold=0)       
1173            else :   
1174                self.doDelete(user.ident)
1175        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % user.Name, ["pykotaUserName"], base=self.info["balancebase"])
1176        for (ident, fields) in result :
1177            self.doDelete(ident)
1178       
1179    def deleteGroup(self, group) :   
1180        """Completely deletes a group from the Quota Storage."""
1181        if self.info["groupquotabase"].lower() == "group" :
1182            base = self.info["groupbase"]
1183        else :
1184            base = self.info["groupquotabase"]
1185        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % group.Name, ["pykotaGroupName"], base=base)
1186        for (ident, fields) in result :
1187            self.doDelete(ident)
1188        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1189        if result :
1190            fields = result[0][1]
1191            for k in fields.keys() :
1192                if k.startswith("pykota") :
1193                    del fields[k]
1194                elif k.lower() == "objectclass" :   
1195                    todelete = []
1196                    for i in range(len(fields[k])) :
1197                        if fields[k][i].startswith("pykota") : 
1198                            todelete.append(i)
1199                    todelete.sort()       
1200                    todelete.reverse()
1201                    for i in todelete :
1202                        del fields[k][i]
1203            if fields.get("objectClass") or fields.get("objectclass") :
1204                self.doModify(group.ident, fields, ignoreold=0)       
1205            else :   
1206                self.doDelete(group.ident)
1207               
1208    def deletePrinter(self, printer) :   
1209        """Completely deletes a printer from the Quota Storage."""
1210        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % printer.Name, base=self.info["lastjobbase"])
1211        for (ident, fields) in result :
1212            self.doDelete(ident)
1213        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % printer.Name, base=self.info["jobbase"])
1214        for (ident, fields) in result :
1215            self.doDelete(ident)
1216        if self.info["groupquotabase"].lower() == "group" :
1217            base = self.info["groupbase"]
1218        else :
1219            base = self.info["groupquotabase"]
1220        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % printer.Name, base=base)
1221        for (ident, fields) in result :
1222            self.doDelete(ident)
1223        if self.info["userquotabase"].lower() == "user" :
1224            base = self.info["userbase"]
1225        else :
1226            base = self.info["userquotabase"]
1227        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % printer.Name, base=base)
1228        for (ident, fields) in result :
1229            self.doDelete(ident)
1230        for parent in self.getParentPrinters(printer) : 
1231            try :
1232                parent.uniqueMember.remove(printer.ident)
1233            except ValueError :   
1234                pass
1235            else :   
1236                fields = {
1237                           "uniqueMember" : parent.uniqueMember,
1238                         } 
1239                self.doModify(parent.ident, fields)         
1240        self.doDelete(printer.ident)   
1241       
1242    def deleteBillingCode(self, code) :
1243        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1244        self.doDelete(code.ident)
1245       
1246    def extractPrinters(self, extractonly={}) :
1247        """Extracts all printer records."""
1248        pname = extractonly.get("printername")
1249        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1250        if entries :
1251            result = [ ("dn", "printername", "priceperpage", "priceperjob", "description") ]
1252            for entry in entries :
1253                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description))
1254            return result 
1255       
1256    def extractUsers(self, extractonly={}) :
1257        """Extracts all user records."""
1258        uname = extractonly.get("username")
1259        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1260        if entries :
1261            result = [ ("dn", "username", "balance", "lifetimepaid", "limitby", "email") ]
1262            for entry in entries :
1263                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email))
1264            return result 
1265       
1266    def extractBillingcodes(self, extractonly={}) :
1267        """Extracts all billing codes records."""
1268        billingcode = extractonly.get("billingcode")
1269        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1270        if entries :
1271            result = [ ("dn", "billingcode", "balance", "pagecounter", "description") ]
1272            for entry in entries :
1273                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1274            return result 
1275       
1276    def extractGroups(self, extractonly={}) :
1277        """Extracts all group records."""
1278        gname = extractonly.get("groupname")
1279        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1280        if entries :
1281            result = [ ("dn", "groupname", "limitby", "balance", "lifetimepaid") ]
1282            for entry in entries :
1283                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid))
1284            return result 
1285       
1286    def extractPayments(self, extractonly={}) :
1287        """Extracts all payment records."""
1288        uname = extractonly.get("username")
1289        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1290        if entries :
1291            result = [ ("username", "amount", "date") ]
1292            for entry in entries :
1293                for (date, amount) in entry.Payments :
1294                    result.append((entry.Name, amount, date))
1295            return result       
1296       
1297    def extractUpquotas(self, extractonly={}) :
1298        """Extracts all userpquota records."""
1299        pname = extractonly.get("printername")
1300        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1301        if entries :
1302            result = [ ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1303            uname = extractonly.get("username")
1304            for entry in entries :
1305                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1306                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1307            return result
1308       
1309    def extractGpquotas(self, extractonly={}) :
1310        """Extracts all grouppquota records."""
1311        pname = extractonly.get("printername")
1312        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1313        if entries :
1314            result = [ ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1315            gname = extractonly.get("groupname")
1316            for entry in entries :
1317                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1318                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1319            return result
1320       
1321    def extractUmembers(self, extractonly={}) :
1322        """Extracts all user groups members."""
1323        gname = extractonly.get("groupname")
1324        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1325        if entries :
1326            result = [ ("groupname", "username", "groupdn", "userdn") ]
1327            uname = extractonly.get("username")
1328            for entry in entries :
1329                for member in entry.Members :
1330                    if (uname is None) or (member.Name == uname) :
1331                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1332            return result       
1333               
1334    def extractPmembers(self, extractonly={}) :
1335        """Extracts all printer groups members."""
1336        pname = extractonly.get("printername")
1337        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1338        if entries :
1339            result = [ ("pgroupname", "printername", "pgroupdn", "printerdn") ]
1340            pgname = extractonly.get("pgroupname")
1341            for entry in entries :
1342                for parent in self.getParentPrinters(entry) :
1343                    if (pgname is None) or (parent.Name == pgname) :
1344                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1345            return result       
1346       
1347    def extractHistory(self, extractonly={}) :
1348        """Extracts all jobhistory records."""
1349        uname = extractonly.get("username")
1350        if uname :
1351            user = self.getUser(uname)
1352        else :   
1353            user = None
1354        pname = extractonly.get("printername")
1355        if pname :
1356            printer = self.getPrinter(pname)
1357        else :   
1358            printer = None
1359        startdate = extractonly.get("start")
1360        enddate = extractonly.get("end")
1361        (startdate, enddate) = self.cleanDates(startdate, enddate)
1362        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), limit=None, start=startdate, end=enddate)
1363        if entries :
1364            result = [ ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode") ] 
1365            for entry in entries :
1366                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)) 
1367            return result
1368           
1369    def getBillingCodeFromBackend(self, label) :
1370        """Extracts billing code information given its label : returns first matching billing code."""
1371        code = StorageBillingCode(self, label)
1372        ulabel = self.userCharsetToDatabase(label)
1373        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % ulabel, ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], base=self.info["billingcodebase"])
1374        if result :
1375            fields = result[0][1]       # take only first matching code, ignore the rest
1376            code.ident = result[0][0]
1377            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1378            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1379            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1380            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1381            code.Exists = 1
1382        return code   
1383       
1384    def addBillingCode(self, label) :
1385        """Adds a billing code to the quota storage, returns it."""
1386        uuid = self.genUUID()
1387        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1388        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1389                   "cn" : uuid,
1390                   "pykotaBillingCode" : self.userCharsetToDatabase(label),
1391                   "pykotaPageCounter" : "0",
1392                   "pykotaBalance" : "0.0",
1393                 } 
1394        self.doAdd(dn, fields)
1395        return self.getBillingCode(label)
1396       
1397    def writeBillingCodeDescription(self, code) :
1398        """Sets the new description for a billing code."""
1399        fields = {
1400                   "description" : self.userCharsetToDatabase(code.Description or ""), 
1401                 }
1402        if fields["description"] :
1403            self.doModify(code.ident, fields)
1404           
1405# def getMatchingBillingCodes(self, billingcodepattern) :
1406# def writeBillingCodeDescription(self, code) :
1407# def setBillingCodeValues(self, code, newbalance, newpagecounter) :   
1408# def consumeBillingCode(self, code, balance, pagecounter) :
Note: See TracBrowser for help on using the browser.