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

Revision 2386, 76.2 kB (checked in by jerome, 19 years ago)

dumpykota can now dump billing codes from LDAP too.

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