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

Revision 2418, 76.3 kB (checked in by jerome, 19 years ago)

Allow PyKota admins and users to use different database backends
or locations depending on their permissions.
Severity : minor, but with great possibilities :)

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