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

Revision 2217, 69.5 kB (checked in by jerome, 19 years ago)

The job-billing info is now saved into the database if present.

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