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

Revision 2798, 95.0 kB (checked in by jerome, 18 years ago)

Fixed a problem with LDAP backend (copy&paste again !)

  • 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, 2005, 2006 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
36import base64
37import random
38
39from mx import DateTime
40
41from pykota.storage import PyKotaStorageError, BaseStorage, StorageObject, \
42                           StorageUser, StorageGroup, StoragePrinter, \
43                           StorageJob, StorageLastJob, StorageUserPQuota, \
44                           StorageGroupPQuota, StorageBillingCode
45
46try :
47    import ldap
48    import ldap.modlist
49except ImportError :   
50    raise PyKotaStorageError, "This python version (%s) doesn't seem to have the python-ldap module installed correctly." % sys.version.split()[0]
51else :   
52    try :
53        from ldap.cidict import cidict
54    except ImportError :   
55        import UserDict
56        sys.stderr.write("ERROR: PyKota requires a newer version of python-ldap. Workaround activated. Please upgrade python-ldap !\n")
57        class cidict(UserDict.UserDict) :
58            pass # Fake it all, and don't care for case insensitivity : users who need it will have to upgrade.
59   
60class Storage(BaseStorage) :
61    def __init__(self, pykotatool, host, dbname, user, passwd) :
62        """Opens the LDAP connection."""
63        self.savedtool = pykotatool
64        self.savedhost = host
65        self.saveddbname = dbname
66        self.saveduser = user
67        self.savedpasswd = passwd
68        self.secondStageInit()
69       
70    def secondStageInit(self) :   
71        """Second stage initialisation."""
72        BaseStorage.__init__(self, self.savedtool)
73        self.info = self.tool.config.getLDAPInfo()
74        message = ""
75        for tryit in range(3) :
76            try :
77                self.tool.logdebug("Trying to open database (host=%s, dbname=%s, user=%s)..." % (self.savedhost, self.saveddbname, self.saveduser))
78                self.database = ldap.initialize(self.savedhost) 
79                if self.info["ldaptls"] :
80                    # we want TLS
81                    ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.info["cacert"])
82                    self.database.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
83                    self.database.start_tls_s()
84                self.database.simple_bind_s(self.saveduser, self.savedpasswd)
85                self.basedn = self.saveddbname
86            except ldap.SERVER_DOWN :   
87                message = "LDAP backend for PyKota seems to be down !"
88                self.tool.printInfo("%s" % message, "error")
89                self.tool.printInfo("Trying again in 2 seconds...", "warn")
90                time.sleep(2)
91            except ldap.LDAPError :   
92                message = "Unable to connect to LDAP server %s as %s." % (self.savedhost, self.saveduser)
93                self.tool.printInfo("%s" % message, "error")
94                self.tool.printInfo("Trying again in 2 seconds...", "warn")
95                time.sleep(2)
96            else :   
97                self.useldapcache = self.tool.config.getLDAPCache()
98                if self.useldapcache :
99                    self.tool.logdebug("Low-Level LDAP Caching enabled.")
100                    self.ldapcache = {} # low-level cache specific to LDAP backend
101                self.closed = 0
102                self.tool.logdebug("Database opened (host=%s, dbname=%s, user=%s)" % (self.savedhost, self.saveddbname, self.saveduser))
103                return # All is fine here.
104        raise PyKotaStorageError, message         
105           
106    def close(self) :   
107        """Closes the database connection."""
108        if not self.closed :
109            self.database.unbind_s()
110            self.closed = 1
111            self.tool.logdebug("Database closed.")
112       
113    def genUUID(self) :   
114        """Generates an unique identifier.
115       
116           TODO : this one is not unique accross several print servers, but should be sufficient for testing.
117        """
118        return md5.md5("%s-%s" % (time.time(), random.random())).hexdigest()
119       
120    def normalizeFields(self, fields) :   
121        """Ensure all items are lists."""
122        for (k, v) in fields.items() :
123            if type(v) not in (types.TupleType, types.ListType) :
124                if not v :
125                    del fields[k]
126                else :   
127                    fields[k] = [ v ]
128        return fields       
129       
130    def beginTransaction(self) :   
131        """Starts a transaction."""
132        self.tool.logdebug("Transaction begins... WARNING : No transactions in LDAP !")
133       
134    def commitTransaction(self) :   
135        """Commits a transaction."""
136        self.tool.logdebug("Transaction committed. WARNING : No transactions in LDAP !")
137       
138    def rollbackTransaction(self) :     
139        """Rollbacks a transaction."""
140        self.tool.logdebug("Transaction aborted. WARNING : No transaction in LDAP !")
141       
142    def doSearch(self, key, fields=None, base="", scope=ldap.SCOPE_SUBTREE, flushcache=0) :
143        """Does an LDAP search query."""
144        message = ""
145        for tryit in range(3) :
146            try :
147                base = base or self.basedn
148                if self.useldapcache :
149                    # Here we overwrite the fields the app want, to try and
150                    # retrieve ALL user defined attributes ("*")
151                    # + the createTimestamp attribute, needed by job history
152                    #
153                    # This may not work with all LDAP servers
154                    # but works at least in OpenLDAP (2.1.25)
155                    # and iPlanet Directory Server (5.1 SP3)
156                    fields = ["*", "createTimestamp"]         
157                   
158                if self.useldapcache and (not flushcache) and (scope == ldap.SCOPE_BASE) and self.ldapcache.has_key(base) :
159                    entry = self.ldapcache[base]
160                    self.tool.logdebug("LDAP cache hit %s => %s" % (base, entry))
161                    result = [(base, entry)]
162                else :
163                    self.tool.logdebug("QUERY : Filter : %s, BaseDN : %s, Scope : %s, Attributes : %s" % (key, base, scope, fields))
164                    result = self.database.search_s(base, scope, key, fields)
165            except ldap.NO_SUCH_OBJECT, msg :       
166                raise PyKotaStorageError, (_("Search base %s doesn't seem to exist. Probable misconfiguration. Please double check /etc/pykota/pykota.conf : %s") % (base, msg))
167            except ldap.LDAPError, msg :   
168                message = (_("Search for %s(%s) from %s(scope=%s) returned no answer.") % (key, fields, base, scope)) + " : %s" % str(msg)
169                self.tool.printInfo("LDAP error : %s" % message, "error")
170                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
171                self.close()
172                self.secondStageInit()
173            else :     
174                self.tool.logdebug("QUERY : Result : %s" % result)
175                result = [ (dn, cidict(attrs)) for (dn, attrs) in result ]
176                if self.useldapcache :
177                    for (dn, attributes) in result :
178                        self.tool.logdebug("LDAP cache store %s => %s" % (dn, attributes))
179                        self.ldapcache[dn] = attributes
180                return result
181        raise PyKotaStorageError, message
182           
183    def doAdd(self, dn, fields) :
184        """Adds an entry in the LDAP directory."""
185        fields = self.normalizeFields(cidict(fields))
186        message = ""
187        for tryit in range(3) :
188            try :
189                self.tool.logdebug("QUERY : ADD(%s, %s)" % (dn, str(fields)))
190                entry = ldap.modlist.addModlist(fields)
191                self.tool.logdebug("%s" % entry)
192                self.database.add_s(dn, entry)
193            except ldap.ALREADY_EXISTS, msg :       
194                raise PyKotaStorageError, "Entry %s already exists : %s" % (dn, str(msg))
195            except ldap.LDAPError, msg :
196                message = (_("Problem adding LDAP entry (%s, %s)") % (dn, str(fields))) + " : %s" % str(msg)
197                self.tool.printInfo("LDAP error : %s" % message, "error")
198                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
199                self.close()
200                self.secondStageInit()
201            else :
202                if self.useldapcache :
203                    self.tool.logdebug("LDAP cache add %s => %s" % (dn, fields))
204                    self.ldapcache[dn] = fields
205                return dn
206        raise PyKotaStorageError, message
207           
208    def doDelete(self, dn) :
209        """Deletes an entry from the LDAP directory."""
210        message = ""
211        for tryit in range(3) :
212            try :
213                self.tool.logdebug("QUERY : Delete(%s)" % dn)
214                self.database.delete_s(dn)
215            except ldap.NO_SUCH_OBJECT :   
216                self.tool.printInfo("Entry %s was already missing before we deleted it. This **MAY** be normal." % dn, "info")
217            except ldap.LDAPError, msg :
218                message = (_("Problem deleting LDAP entry (%s)") % dn) + " : %s" % str(msg)
219                self.tool.printInfo("LDAP error : %s" % message, "error")
220                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
221                self.close()
222                self.secondStageInit()
223            else :   
224                if self.useldapcache :
225                    try :
226                        self.tool.logdebug("LDAP cache del %s" % dn)
227                        del self.ldapcache[dn]
228                    except KeyError :   
229                        pass
230                return       
231        raise PyKotaStorageError, message
232           
233    def doModify(self, dn, fields, ignoreold=1, flushcache=0) :
234        """Modifies an entry in the LDAP directory."""
235        fields = cidict(fields)
236        for tryit in range(3) :
237            try :
238                # TODO : take care of, and update LDAP specific cache
239                if self.useldapcache and not flushcache :
240                    if self.ldapcache.has_key(dn) :
241                        old = self.ldapcache[dn]
242                        self.tool.logdebug("LDAP cache hit %s => %s" % (dn, old))
243                        oldentry = {}
244                        for (k, v) in old.items() :
245                            if k != "createTimestamp" :
246                                oldentry[k] = v
247                    else :   
248                        self.tool.logdebug("LDAP cache miss %s" % dn)
249                        oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE)[0][1]
250                else :       
251                    oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE, flushcache=flushcache)[0][1]
252                for (k, v) in fields.items() :
253                    if type(v) == type({}) :
254                        try :
255                            oldvalue = v["convert"](oldentry.get(k, [0])[0])
256                        except ValueError :   
257                            self.tool.logdebug("Error converting %s with %s(%s)" % (oldentry.get(k), k, v))
258                            oldvalue = 0
259                        if v["operator"] == '+' :
260                            newvalue = oldvalue + v["value"]
261                        else :   
262                            newvalue = oldvalue - v["value"]
263                        fields[k] = str(newvalue)
264                fields = self.normalizeFields(fields)
265                self.tool.logdebug("QUERY : Modify(%s, %s ==> %s)" % (dn, oldentry, fields))
266                entry = ldap.modlist.modifyModlist(oldentry, fields, ignore_oldexistent=ignoreold)
267                modentry = []
268                for (mop, mtyp, mval) in entry :
269                    if mtyp and (mtyp.lower() != "createtimestamp") :
270                        modentry.append((mop, mtyp, mval))
271                self.tool.logdebug("MODIFY : %s ==> %s ==> %s" % (fields, entry, modentry))
272                if modentry :
273                    self.database.modify_s(dn, modentry)
274            except ldap.LDAPError, msg :
275                message = (_("Problem modifying LDAP entry (%s, %s)") % (dn, fields)) + " : %s" % str(msg)
276                self.tool.printInfo("LDAP error : %s" % message, "error")
277                self.tool.printInfo("LDAP connection will be closed and reopened.", "warn")
278                self.close()
279                self.secondStageInit()
280            else :
281                if self.useldapcache :
282                    cachedentry = self.ldapcache[dn]
283                    for (mop, mtyp, mval) in entry :
284                        if mop in (ldap.MOD_ADD, ldap.MOD_REPLACE) :
285                            cachedentry[mtyp] = mval
286                        else :
287                            try :
288                                del cachedentry[mtyp]
289                            except KeyError :   
290                                pass
291                    self.tool.logdebug("LDAP cache update %s => %s" % (dn, cachedentry))
292                return dn
293        raise PyKotaStorageError, message
294           
295    def filterNames(self, records, attribute, patterns=None) :
296        """Returns a list of 'attribute' from a list of records.
297       
298           Logs any missing attribute.
299        """   
300        result = []
301        for (dn, record) in records :
302            attrval = record.get(attribute, [None])[0]
303            if attrval is None :
304                self.tool.printInfo("Object %s has no %s attribute !" % (dn, attribute), "error")
305            else :
306                attrval = self.databaseToUserCharset(attrval)
307                if patterns :
308                    if (not isinstance(patterns, type([]))) and (not isinstance(patterns, type(()))) :
309                        patterns = [ patterns ]
310                    if self.tool.matchString(attrval, patterns) :   
311                        result.append(attrval)
312                else :   
313                    result.append(attrval)
314        return result       
315               
316    def getAllBillingCodes(self, billingcode=None) :   
317        """Extracts all billing codes or only the billing codes matching the optional parameter."""
318        ldapfilter = "objectClass=pykotaBilling"
319        result = self.doSearch(ldapfilter, ["pykotaBillingCode"], base=self.info["billingcodebase"])
320        if result :
321            return [self.databaseToUserCharset(bc) for bc in self.filterNames(result, "pykotaBillingCode", billingcode)]
322        else :   
323            return []
324       
325    def getAllPrintersNames(self, printername=None) :   
326        """Extracts all printer names or only the printers' names matching the optional parameter."""
327        ldapfilter = "objectClass=pykotaPrinter"
328        result = self.doSearch(ldapfilter, ["pykotaPrinterName"], base=self.info["printerbase"])
329        if result :
330            return self.filterNames(result, "pykotaPrinterName", printername)
331        else :   
332            return []
333       
334    def getAllUsersNames(self, username=None) :   
335        """Extracts all user names or only the users' names matching the optional parameter."""
336        ldapfilter = "objectClass=pykotaAccount"
337        result = self.doSearch(ldapfilter, ["pykotaUserName"], base=self.info["userbase"])
338        if result :
339            return self.filterNames(result, "pykotaUserName", username)
340        else :   
341            return []
342       
343    def getAllGroupsNames(self, groupname=None) :   
344        """Extracts all group names or only the groups' names matching the optional parameter."""
345        ldapfilter = "objectClass=pykotaGroup"
346        result = self.doSearch(ldapfilter, ["pykotaGroupName"], base=self.info["groupbase"])
347        if result :
348            return self.filterNames(result, "pykotaGroupName", groupname)
349        else :   
350            return []
351       
352    def getUserNbJobsFromHistory(self, user) :
353        """Returns the number of jobs the user has in history."""
354        result = self.doSearch("(&(pykotaUserName=%s)(objectClass=pykotaJob))" % self.userCharsetToDatabase(user.Name), None, base=self.info["jobbase"])
355        return len(result)
356       
357    def getUserFromBackend(self, username) :   
358        """Extracts user information given its name."""
359        user = StorageUser(self, username)
360        username = self.userCharsetToDatabase(username)
361        result = self.doSearch("(&(objectClass=pykotaAccount)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["userrdn"], username), ["pykotaUserName", "pykotaLimitBy", self.info["usermail"], "pykotaOverCharge", "description"], base=self.info["userbase"])
362        if result :
363            fields = result[0][1]
364            user.ident = result[0][0]
365            user.Description = self.databaseToUserCharset(fields.get("description", [None])[0])
366            user.Email = fields.get(self.info["usermail"], [None])[0]
367            user.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
368            user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
369            result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["balancerdn"], username), ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], base=self.info["balancebase"])
370            if not result :
371                raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
372            else :
373                fields = result[0][1]
374                user.idbalance = result[0][0]
375                user.AccountBalance = fields.get("pykotaBalance")
376                if user.AccountBalance is not None :
377                    if user.AccountBalance[0].upper() == "NONE" :
378                        user.AccountBalance = None
379                    else :   
380                        user.AccountBalance = float(user.AccountBalance[0])
381                user.AccountBalance = user.AccountBalance or 0.0       
382                user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
383                if user.LifeTimePaid is not None :
384                    if user.LifeTimePaid[0].upper() == "NONE" :
385                        user.LifeTimePaid = None
386                    else :   
387                        user.LifeTimePaid = float(user.LifeTimePaid[0])
388                user.LifeTimePaid = user.LifeTimePaid or 0.0       
389                user.Payments = []
390                for payment in fields.get("pykotaPayments", []) :
391                    try :
392                        (date, amount, description) = payment.split(" # ")
393                    except ValueError :
394                        # Payment with no description (old Payment)
395                        (date, amount) = payment.split(" # ")
396                        description = ""
397                    else :   
398                        description = self.databaseToUserCharset(base64.decodestring(description))
399                    user.Payments.append((date, float(amount), description))
400            user.Exists = 1
401        return user
402       
403    def getGroupFromBackend(self, groupname) :   
404        """Extracts group information given its name."""
405        group = StorageGroup(self, groupname)
406        groupname = self.userCharsetToDatabase(groupname)
407        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % (groupname, self.info["grouprdn"], groupname), ["pykotaGroupName", "pykotaLimitBy", "description"], base=self.info["groupbase"])
408        if result :
409            fields = result[0][1]
410            group.ident = result[0][0]
411            group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
412            group.Description = self.databaseToUserCharset(fields.get("description", [None])[0])
413            group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
414            group.AccountBalance = 0.0
415            group.LifeTimePaid = 0.0
416            for member in self.getGroupMembers(group) :
417                if member.Exists :
418                    group.AccountBalance += member.AccountBalance
419                    group.LifeTimePaid += member.LifeTimePaid
420            group.Exists = 1
421        return group
422       
423    def getPrinterFromBackend(self, printername) :       
424        """Extracts printer information given its name : returns first matching printer."""
425        printer = StoragePrinter(self, printername)
426        printername = self.userCharsetToDatabase(printername)
427        result = self.doSearch("(&(objectClass=pykotaPrinter)(|(pykotaPrinterName=%s)(%s=%s)))" \
428                      % (printername, self.info["printerrdn"], printername), \
429                        ["pykotaPrinterName", "pykotaPricePerPage", \
430                         "pykotaPricePerJob", "pykotaMaxJobSize", \
431                         "pykotaPassThrough", "uniqueMember", "description"], \
432                      base=self.info["printerbase"])
433        if result :
434            fields = result[0][1]       # take only first matching printer, ignore the rest
435            printer.ident = result[0][0]
436            printer.Name = fields.get("pykotaPrinterName", [self.databaseToUserCharset(printername)])[0] 
437            printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0])
438            printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0])
439            printer.MaxJobSize = int(fields.get("pykotaMaxJobSize", [0])[0])
440            printer.PassThrough = fields.get("pykotaPassThrough", [None])[0]
441            if printer.PassThrough in (1, "1", "t", "true", "TRUE", "True") :
442                printer.PassThrough = 1
443            else :
444                printer.PassThrough = 0
445            printer.uniqueMember = fields.get("uniqueMember", [])
446            printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
447            printer.Exists = 1
448        return printer   
449       
450    def getUserPQuotaFromBackend(self, user, printer) :       
451        """Extracts a user print quota."""
452        userpquota = StorageUserPQuota(self, user, printer)
453        if printer.Exists and user.Exists :
454            if self.info["userquotabase"].lower() == "user" :
455                base = user.ident
456            else :   
457                base = self.info["userquotabase"]
458            result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s)(pykotaPrinterName=%s))" % \
459                                      (self.userCharsetToDatabase(user.Name), self.userCharsetToDatabase(printer.Name)), \
460                                      ["pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount", "pykotaMaxJobSize"], \
461                                      base=base)
462            if result :
463                fields = result[0][1]
464                userpquota.ident = result[0][0]
465                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
466                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
467                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
468                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
469                if userpquota.SoftLimit is not None :
470                    if userpquota.SoftLimit[0].upper() == "NONE" :
471                        userpquota.SoftLimit = None
472                    else :   
473                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
474                userpquota.HardLimit = fields.get("pykotaHardLimit")
475                if userpquota.HardLimit is not None :
476                    if userpquota.HardLimit[0].upper() == "NONE" :
477                        userpquota.HardLimit = None
478                    elif userpquota.HardLimit is not None :   
479                        userpquota.HardLimit = int(userpquota.HardLimit[0])
480                userpquota.DateLimit = fields.get("pykotaDateLimit")
481                if userpquota.DateLimit is not None :
482                    if userpquota.DateLimit[0].upper() == "NONE" : 
483                        userpquota.DateLimit = None
484                    else :   
485                        userpquota.DateLimit = userpquota.DateLimit[0]
486                userpquota.MaxJobSize = fields.get("pykotaMaxJobSize")
487                if userpquota.MaxJobSize is not None :
488                    if userpquota.MaxJobSize[0].upper() == "NONE" :
489                        userpquota.MaxJobSize = None
490                    else :   
491                        userpquota.MaxJobSize = int(userpquota.MaxJobSize[0])
492                userpquota.Exists = 1
493        return userpquota
494       
495    def getGroupPQuotaFromBackend(self, group, printer) :       
496        """Extracts a group print quota."""
497        grouppquota = StorageGroupPQuota(self, group, printer)
498        if group.Exists :
499            if self.info["groupquotabase"].lower() == "group" :
500                base = group.ident
501            else :   
502                base = self.info["groupquotabase"]
503            result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s)(pykotaPrinterName=%s))" % \
504                                      (self.userCharsetToDatabase(group.Name), self.userCharsetToDatabase(printer.Name)), \
505                                      ["pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaMaxJobSize"], \
506                                      base=base)
507            if result :
508                fields = result[0][1]
509                grouppquota.ident = result[0][0]
510                grouppquota.SoftLimit = fields.get("pykotaSoftLimit")
511                if grouppquota.SoftLimit is not None :
512                    if grouppquota.SoftLimit[0].upper() == "NONE" :
513                        grouppquota.SoftLimit = None
514                    else :   
515                        grouppquota.SoftLimit = int(grouppquota.SoftLimit[0])
516                grouppquota.HardLimit = fields.get("pykotaHardLimit")
517                if grouppquota.HardLimit is not None :
518                    if grouppquota.HardLimit[0].upper() == "NONE" :
519                        grouppquota.HardLimit = None
520                    else :   
521                        grouppquota.HardLimit = int(grouppquota.HardLimit[0])
522                grouppquota.DateLimit = fields.get("pykotaDateLimit")
523                if grouppquota.DateLimit is not None :
524                    if grouppquota.DateLimit[0].upper() == "NONE" : 
525                        grouppquota.DateLimit = None
526                    else :   
527                        grouppquota.DateLimit = grouppquota.DateLimit[0]
528                grouppquota.MaxJobSize = fields.get("pykotaMaxJobSize")
529                if grouppquota.MaxJobSize is not None :
530                    if grouppquota.MaxJobSize[0].upper() == "NONE" :
531                        grouppquota.MaxJobSize = None
532                    else :   
533                        grouppquota.MaxJobSize = int(grouppquota.MaxJobSize[0])
534                grouppquota.PageCounter = 0
535                grouppquota.LifePageCounter = 0
536                usernamesfilter = "".join(["(pykotaUserName=%s)" % self.userCharsetToDatabase(member.Name) for member in self.getGroupMembers(group)])
537                if usernamesfilter :
538                    usernamesfilter = "(|%s)" % usernamesfilter
539                if self.info["userquotabase"].lower() == "user" :
540                    base = self.info["userbase"]
541                else :
542                    base = self.info["userquotabase"]
543                result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)%s)" % \
544                                          (self.userCharsetToDatabase(printer.Name), usernamesfilter), \
545                                          ["pykotaPageCounter", "pykotaLifePageCounter"], base=base)
546                if result :
547                    for userpquota in result :   
548                        grouppquota.PageCounter += int(userpquota[1].get("pykotaPageCounter", [0])[0] or 0)
549                        grouppquota.LifePageCounter += int(userpquota[1].get("pykotaLifePageCounter", [0])[0] or 0)
550                grouppquota.Exists = 1
551        return grouppquota
552       
553    def getPrinterLastJobFromBackend(self, printer) :       
554        """Extracts a printer's last job information."""
555        lastjob = StorageLastJob(self, printer)
556        pname = self.userCharsetToDatabase(printer.Name)
557        result = self.doSearch("(&(objectClass=pykotaLastjob)(|(pykotaPrinterName=%s)(%s=%s)))" % \
558                                  (pname, self.info["printerrdn"], pname), \
559                                  ["pykotaLastJobIdent"], \
560                                  base=self.info["lastjobbase"])
561        if result :
562            lastjob.lastjobident = result[0][0]
563            lastjobident = result[0][1]["pykotaLastJobIdent"][0]
564            result = None
565            try :
566                result = self.doSearch("objectClass=pykotaJob", [ "pykotaJobSizeBytes", 
567                                                                  "pykotaHostName", 
568                                                                  "pykotaUserName", 
569                                                                  "pykotaPrinterName", 
570                                                                  "pykotaJobId", 
571                                                                  "pykotaPrinterPageCounter", 
572                                                                  "pykotaJobSize", 
573                                                                  "pykotaAction", 
574                                                                  "pykotaJobPrice", 
575                                                                  "pykotaFileName", 
576                                                                  "pykotaTitle", 
577                                                                  "pykotaCopies", 
578                                                                  "pykotaOptions", 
579                                                                  "pykotaBillingCode", 
580                                                                  "pykotaPages", 
581                                                                  "pykotaMD5Sum", 
582                                                                  "pykotaPrecomputedJobSize",
583                                                                  "pykotaPrecomputedJobPrice",
584                                                                  "createTimestamp" ], 
585                                                                base="cn=%s,%s" % (lastjobident, self.info["jobbase"]), scope=ldap.SCOPE_BASE)
586            except PyKotaStorageError :   
587                pass # Last job entry exists, but job probably doesn't exist anymore.
588            if result :
589                fields = result[0][1]
590                lastjob.ident = result[0][0]
591                lastjob.JobId = fields.get("pykotaJobId")[0]
592                lastjob.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
593                lastjob.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0])
594                try :
595                    lastjob.JobSize = int(fields.get("pykotaJobSize", [0])[0])
596                except ValueError :   
597                    lastjob.JobSize = None
598                try :   
599                    lastjob.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
600                except ValueError :   
601                    lastjob.JobPrice = None
602                lastjob.JobAction = fields.get("pykotaAction", [""])[0]
603                lastjob.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
604                lastjob.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
605                lastjob.JobCopies = int(fields.get("pykotaCopies", [0])[0])
606                lastjob.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
607                lastjob.JobHostName = fields.get("pykotaHostName", [""])[0]
608                lastjob.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
609                lastjob.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
610                lastjob.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
611                lastjob.JobPages = fields.get("pykotaPages", [""])[0]
612                try :
613                    lastjob.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
614                except ValueError :   
615                    lastjob.PrecomputedJobSize = None
616                try :   
617                    lastjob.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
618                except ValueError :   
619                    lastjob.PrecomputedJobPrice = None
620                if lastjob.JobTitle == lastjob.JobFileName == lastjob.JobOptions == "hidden" :
621                    (lastjob.JobTitle, lastjob.JobFileName, lastjob.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
622                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
623                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
624                lastjob.JobDate = mxtime.strftime("%Y%m%d %H:%M:%S")
625                lastjob.Exists = 1
626        return lastjob
627       
628    def getGroupMembersFromBackend(self, group) :       
629        """Returns the group's members list."""
630        groupmembers = []
631        gname = self.userCharsetToDatabase(group.Name)
632        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % \
633                                  (gname, self.info["grouprdn"], gname), \
634                                  [self.info["groupmembers"]], \
635                                  base=self.info["groupbase"])
636        if result :
637            for username in result[0][1].get(self.info["groupmembers"], []) :
638                groupmembers.append(self.getUser(self.databaseToUserCharset(username)))
639        return groupmembers       
640       
641    def getUserGroupsFromBackend(self, user) :       
642        """Returns the user's groups list."""
643        groups = []
644        uname = self.userCharsetToDatabase(user.Name)
645        result = self.doSearch("(&(objectClass=pykotaGroup)(%s=%s))" % \
646                                  (self.info["groupmembers"], uname), \
647                                  [self.info["grouprdn"], "pykotaGroupName", "pykotaLimitBy"], \
648                                  base=self.info["groupbase"])
649        if result :
650            for (groupid, fields) in result :
651                groupname = self.databaseToUserCharset((fields.get("pykotaGroupName", [None]) or fields.get(self.info["grouprdn"], [None]))[0])
652                group = self.getFromCache("GROUPS", groupname)
653                if group is None :
654                    group = StorageGroup(self, groupname)
655                    group.ident = groupid
656                    group.LimitBy = fields.get("pykotaLimitBy")
657                    if group.LimitBy is not None :
658                        group.LimitBy = group.LimitBy[0]
659                    else :   
660                        group.LimitBy = "quota"
661                    group.AccountBalance = 0.0
662                    group.LifeTimePaid = 0.0
663                    for member in self.getGroupMembers(group) :
664                        if member.Exists :
665                            group.AccountBalance += member.AccountBalance
666                            group.LifeTimePaid += member.LifeTimePaid
667                    group.Exists = 1
668                    self.cacheEntry("GROUPS", group.Name, group)
669                groups.append(group)
670        return groups       
671       
672    def getParentPrintersFromBackend(self, printer) :   
673        """Get all the printer groups this printer is a member of."""
674        pgroups = []
675        result = self.doSearch("(&(objectClass=pykotaPrinter)(uniqueMember=%s))" % \
676                                  printer.ident, \
677                                  ["pykotaPrinterName"], \
678                                  base=self.info["printerbase"])
679        if result :
680            for (printerid, fields) in result :
681                if printerid != printer.ident : # In case of integrity violation.
682                    parentprinter = self.getPrinter(self.databaseToUserCharset(fields.get("pykotaPrinterName")[0]))
683                    if parentprinter.Exists :
684                        pgroups.append(parentprinter)
685        return pgroups
686       
687    def getMatchingPrinters(self, printerpattern) :
688        """Returns the list of all printers for which name matches a certain pattern."""
689        printers = []
690        # see comment at the same place in pgstorage.py
691        result = self.doSearch("objectClass=pykotaPrinter", \
692                                  ["pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerJob", "pykotaMaxJobSize", "pykotaPassThrough", "uniqueMember", "description"], \
693                                  base=self.info["printerbase"])
694        if result :
695            patterns = printerpattern.split(",")
696            try :
697                patdict = {}.fromkeys(patterns)
698            except AttributeError :   
699                # Python v2.2 or earlier
700                patdict = {}
701                for p in patterns :
702                    patdict[p] = None
703            for (printerid, fields) in result :
704                printername = self.databaseToUserCharset(fields.get("pykotaPrinterName", [""])[0] or fields.get(self.info["printerrdn"], [""])[0])
705                if patdict.has_key(printername) or self.tool.matchString(printername, patterns) :
706                    printer = StoragePrinter(self, printername)
707                    printer.ident = printerid
708                    printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0] or 0.0)
709                    printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0] or 0.0)
710                    printer.MaxJobSize = int(fields.get("pykotaMaxJobSize", [0])[0])
711                    printer.PassThrough = fields.get("pykotaPassThrough", [None])[0]
712                    if printer.PassThrough in (1, "1", "t", "true", "TRUE", "True") :
713                        printer.PassThrough = 1
714                    else :
715                        printer.PassThrough = 0
716                    printer.uniqueMember = fields.get("uniqueMember", [])
717                    printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
718                    printer.Exists = 1
719                    printers.append(printer)
720                    self.cacheEntry("PRINTERS", printer.Name, printer)
721        return printers       
722       
723    def getMatchingUsers(self, userpattern) :
724        """Returns the list of all users for which name matches a certain pattern."""
725        users = []
726        # see comment at the same place in pgstorage.py
727        result = self.doSearch("objectClass=pykotaAccount", \
728                                  ["pykotaUserName", "pykotaLimitBy", self.info["usermail"], "pykotaOverCharge", "description"], \
729                                  base=self.info["userbase"])
730        if result :
731            patterns = userpattern.split(",")
732            try :
733                patdict = {}.fromkeys(patterns)
734            except AttributeError :   
735                # Python v2.2 or earlier
736                patdict = {}
737                for p in patterns :
738                    patdict[p] = None
739            for (userid, fields) in result :
740                username = self.databaseToUserCharset(fields.get("pykotaUserName", [""])[0] or fields.get(self.info["userrdn"], [""])[0])
741                if patdict.has_key(username) or self.tool.matchString(username, patterns) :
742                    user = StorageUser(self, username)
743                    user.ident = userid
744                    user.Email = fields.get(self.info["usermail"], [None])[0]
745                    user.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
746                    user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
747                    user.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
748                    uname = self.userCharsetToDatabase(username)
749                    result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % \
750                                              (uname, self.info["balancerdn"], uname), \
751                                              ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], \
752                                              base=self.info["balancebase"])
753                    if not result :
754                        raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
755                    else :
756                        fields = result[0][1]
757                        user.idbalance = result[0][0]
758                        user.AccountBalance = fields.get("pykotaBalance")
759                        if user.AccountBalance is not None :
760                            if user.AccountBalance[0].upper() == "NONE" :
761                                user.AccountBalance = None
762                            else :   
763                                user.AccountBalance = float(user.AccountBalance[0])
764                        user.AccountBalance = user.AccountBalance or 0.0       
765                        user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
766                        if user.LifeTimePaid is not None :
767                            if user.LifeTimePaid[0].upper() == "NONE" :
768                                user.LifeTimePaid = None
769                            else :   
770                                user.LifeTimePaid = float(user.LifeTimePaid[0])
771                        user.LifeTimePaid = user.LifeTimePaid or 0.0       
772                        user.Payments = []
773                        for payment in fields.get("pykotaPayments", []) :
774                            try :
775                                (date, amount, description) = payment.split(" # ")
776                            except ValueError :
777                                # Payment with no description (old Payment)
778                                (date, amount) = payment.split(" # ")
779                                description = ""
780                            else :   
781                                description = self.databaseToUserCharset(base64.decodestring(description))
782                            user.Payments.append((date, float(amount), description))
783                    user.Exists = 1
784                    users.append(user)
785                    self.cacheEntry("USERS", user.Name, user)
786        return users       
787       
788    def getMatchingGroups(self, grouppattern) :
789        """Returns the list of all groups for which name matches a certain pattern."""
790        groups = []
791        # see comment at the same place in pgstorage.py
792        result = self.doSearch("objectClass=pykotaGroup", \
793                                  ["pykotaGroupName", "pykotaLimitBy", "description"], \
794                                  base=self.info["groupbase"])
795        if result :
796            patterns = grouppattern.split(",")
797            try :
798                patdict = {}.fromkeys(patterns)
799            except AttributeError :   
800                # Python v2.2 or earlier
801                patdict = {}
802                for p in patterns :
803                    patdict[p] = None
804            for (groupid, fields) in result :
805                groupname = self.databaseToUserCharset(fields.get("pykotaGroupName", [""])[0] or fields.get(self.info["grouprdn"], [""])[0])
806                if patdict.has_key(groupname) or self.tool.matchString(groupname, patterns) :
807                    group = StorageGroup(self, groupname)
808                    group.ident = groupid
809                    group.Name = fields.get("pykotaGroupName", [self.databaseToUserCharset(groupname)])[0] 
810                    group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
811                    group.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
812                    group.AccountBalance = 0.0
813                    group.LifeTimePaid = 0.0
814                    for member in self.getGroupMembers(group) :
815                        if member.Exists :
816                            group.AccountBalance += member.AccountBalance
817                            group.LifeTimePaid += member.LifeTimePaid
818                    group.Exists = 1
819                    groups.append(group)
820                    self.cacheEntry("GROUPS", group.Name, group)
821        return groups
822       
823    def getPrinterUsersAndQuotas(self, printer, names=["*"]) :       
824        """Returns the list of users who uses a given printer, along with their quotas."""
825        usersandquotas = []
826        pname = self.userCharsetToDatabase(printer.Name)
827        names = [self.userCharsetToDatabase(n) for n in names]
828        if self.info["userquotabase"].lower() == "user" :
829           base = self.info["userbase"]
830        else :
831           base = self.info["userquotabase"]
832        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % \
833                                  (pname, "".join(["(pykotaUserName=%s)" % uname for uname in names])), \
834                                  ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], \
835                                  base=base)
836        if result :
837            for (userquotaid, fields) in result :
838                user = self.getUser(self.databaseToUserCharset(fields.get("pykotaUserName")[0]))
839                userpquota = StorageUserPQuota(self, user, printer)
840                userpquota.ident = userquotaid
841                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
842                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
843                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
844                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
845                if userpquota.SoftLimit is not None :
846                    if userpquota.SoftLimit[0].upper() == "NONE" :
847                        userpquota.SoftLimit = None
848                    else :   
849                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
850                userpquota.HardLimit = fields.get("pykotaHardLimit")
851                if userpquota.HardLimit is not None :
852                    if userpquota.HardLimit[0].upper() == "NONE" :
853                        userpquota.HardLimit = None
854                    elif userpquota.HardLimit is not None :   
855                        userpquota.HardLimit = int(userpquota.HardLimit[0])
856                userpquota.DateLimit = fields.get("pykotaDateLimit")
857                if userpquota.DateLimit is not None :
858                    if userpquota.DateLimit[0].upper() == "NONE" : 
859                        userpquota.DateLimit = None
860                    else :   
861                        userpquota.DateLimit = userpquota.DateLimit[0]
862                userpquota.Exists = 1
863                usersandquotas.append((user, userpquota))
864                self.cacheEntry("USERPQUOTAS", "%s@%s" % (user.Name, printer.Name), userpquota)
865        usersandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
866        return usersandquotas
867               
868    def getPrinterGroupsAndQuotas(self, printer, names=["*"]) :       
869        """Returns the list of groups which uses a given printer, along with their quotas."""
870        groupsandquotas = []
871        pname = self.userCharsetToDatabase(printer.Name)
872        names = [self.userCharsetToDatabase(n) for n in names]
873        if self.info["groupquotabase"].lower() == "group" :
874           base = self.info["groupbase"]
875        else :
876           base = self.info["groupquotabase"]
877        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s)(|%s))" % \
878                                  (pname, "".join(["(pykotaGroupName=%s)" % gname for gname in names])), \
879                                  ["pykotaGroupName"], \
880                                  base=base)
881        if result :
882            for (groupquotaid, fields) in result :
883                group = self.getGroup(self.databaseToUserCharset(fields.get("pykotaGroupName")[0]))
884                grouppquota = self.getGroupPQuota(group, printer)
885                groupsandquotas.append((group, grouppquota))
886        groupsandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
887        return groupsandquotas
888       
889    def addPrinter(self, printer) :
890        """Adds a printer to the quota storage, returns the old value if it already exists."""
891        oldentry = self.getPrinter(printer.Name)
892        if oldentry.Exists :
893            return oldentry # we return the existing entry
894        printername = self.userCharsetToDatabase(printer.Name)
895        fields = { self.info["printerrdn"] : printername,
896                   "objectClass" : ["pykotaObject", "pykotaPrinter"],
897                   "cn" : printername,
898                   "pykotaPrinterName" : printername,
899                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
900                   "pykotaMaxJobSize" : str(printer.MaxJobSize or 0),
901                   "description" : self.userCharsetToDatabase(printer.Description or ""),
902                   "pykotaPricePerPage" : str(printer.PricePerPage or 0.0),
903                   "pykotaPricePerJob" : str(printer.PricePerJob or 0.0),
904                 } 
905        dn = "%s=%s,%s" % (self.info["printerrdn"], printername, self.info["printerbase"])
906        self.doAdd(dn, fields)
907        printer.isDirty = False
908        return None # the entry created doesn't need further modification
909       
910    def addUser(self, user) :       
911        """Adds a user to the quota storage, returns the old value if it already exists."""
912        oldentry = self.getUser(user.Name)
913        if oldentry.Exists :
914            return oldentry # we return the existing entry
915        uname = self.userCharsetToDatabase(user.Name)
916        newfields = {
917                       "pykotaUserName" : uname,
918                       "pykotaLimitBy" : (user.LimitBy or "quota"),
919                       "pykotaOverCharge" : str(user.OverCharge),
920                       "description" : self.userCharsetToDatabase(user.Description or "")
921                    }   
922                       
923        if user.Email :
924            newfields.update({self.info["usermail"]: user.Email})
925        mustadd = 1
926        if self.info["newuser"].lower() != 'below' :
927            try :
928                (where, action) = [s.strip() for s in self.info["newuser"].split(",")]
929            except ValueError :
930                (where, action) = (self.info["newuser"].strip(), "fail")
931            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
932                                      (where, self.info["userrdn"], uname), \
933                                      None, \
934                                      base=self.info["userbase"])
935            if result :
936                (dn, fields) = result[0]
937                oc = fields.get("objectClass", fields.get("objectclass", []))
938                oc.extend(["pykotaAccount", "pykotaAccountBalance"])
939                fields.update(newfields)
940                fields.update({ "pykotaBalance" : str(user.AccountBalance or 0.0),
941                                "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), })   
942                self.doModify(dn, fields)
943                mustadd = 0
944            else :
945                message = _("Unable to find an existing objectClass %s entry with %s=%s to attach pykotaAccount objectClass") % (where, self.info["userrdn"], user.Name)
946                if action.lower() == "warn" :   
947                    self.tool.printInfo(_("%s. A new entry will be created instead.") % message, "warn")
948                else : # 'fail' or incorrect setting
949                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
950               
951        if mustadd :
952            if self.info["userbase"] == self.info["balancebase"] :           
953                fields = { self.info["userrdn"] : uname,
954                           "objectClass" : ["pykotaObject", "pykotaAccount", "pykotaAccountBalance"],
955                           "cn" : uname,
956                           "pykotaBalance" : str(user.AccountBalance or 0.0),
957                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
958                         } 
959            else :             
960                fields = { self.info["userrdn"] : uname,
961                           "objectClass" : ["pykotaObject", "pykotaAccount"],
962                           "cn" : uname,
963                         } 
964            fields.update(newfields)         
965            dn = "%s=%s,%s" % (self.info["userrdn"], uname, self.info["userbase"])
966            self.doAdd(dn, fields)
967            if self.info["userbase"] != self.info["balancebase"] :           
968                fields = { self.info["balancerdn"] : uname,
969                           "objectClass" : ["pykotaObject", "pykotaAccountBalance"],
970                           "cn" : uname,
971                           "pykotaBalance" : str(user.AccountBalance or 0.0),
972                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
973                         } 
974                dn = "%s=%s,%s" % (self.info["balancerdn"], uname, self.info["balancebase"])
975                self.doAdd(dn, fields)
976        user.idbalance = dn
977        if user.PaymentsBacklog :
978            for (value, comment) in user.PaymentsBacklog :
979                self.writeNewPayment(user, value, comment)
980            user.PaymentsBacklog = []
981        user.isDirty = False
982        return None # the entry created doesn't need further modification
983       
984    def addGroup(self, group) :       
985        """Adds a group to the quota storage, returns the old value if it already exists."""
986        oldentry = self.getGroup(group.Name)
987        if oldentry.Exists :
988            return oldentry # we return the existing entry
989        gname = self.userCharsetToDatabase(group.Name)
990        newfields = { 
991                      "pykotaGroupName" : gname,
992                      "pykotaLimitBy" : (group.LimitBy or "quota"),
993                      "description" : self.userCharsetToDatabase(group.Description or "")
994                    } 
995        mustadd = 1
996        if self.info["newgroup"].lower() != 'below' :
997            try :
998                (where, action) = [s.strip() for s in self.info["newgroup"].split(",")]
999            except ValueError :
1000                (where, action) = (self.info["newgroup"].strip(), "fail")
1001            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % \
1002                                      (where, self.info["grouprdn"], gname), \
1003                                      None, \
1004                                      base=self.info["groupbase"])
1005            if result :
1006                (dn, fields) = result[0]
1007                oc = fields.get("objectClass", fields.get("objectclass", []))
1008                oc.extend(["pykotaGroup"])
1009                fields.update(newfields)
1010                self.doModify(dn, fields)
1011                mustadd = 0
1012            else :
1013                message = _("Unable to find an existing entry to attach pykotaGroup objectclass %s") % group.Name
1014                if action.lower() == "warn" :   
1015                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
1016                else : # 'fail' or incorrect setting
1017                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
1018               
1019        if mustadd :
1020            fields = { self.info["grouprdn"] : gname,
1021                       "objectClass" : ["pykotaObject", "pykotaGroup"],
1022                       "cn" : gname,
1023                     } 
1024            fields.update(newfields)         
1025            dn = "%s=%s,%s" % (self.info["grouprdn"], gname, self.info["groupbase"])
1026            self.doAdd(dn, fields)
1027        group.isDirty = False
1028        return None # the entry created doesn't need further modification
1029       
1030    def addUserToGroup(self, user, group) :   
1031        """Adds an user to a group."""
1032        if user.Name not in [u.Name for u in self.getGroupMembers(group)] :
1033            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1034            if result :
1035                fields = result[0][1]
1036                if not fields.has_key(self.info["groupmembers"]) :
1037                    fields[self.info["groupmembers"]] = []
1038                fields[self.info["groupmembers"]].append(self.userCharsetToDatabase(user.Name))
1039                self.doModify(group.ident, fields)
1040                group.Members.append(user)
1041               
1042    def delUserFromGroup(self, user, group) :   
1043        """Removes an user from a group."""
1044        if user.Name in [u.Name for u in self.getGroupMembers(group)] :
1045            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)
1046            if result :
1047                fields = result[0][1]
1048                if not fields.has_key(self.info["groupmembers"]) :
1049                    fields[self.info["groupmembers"]] = []
1050                try :   
1051                    fields[self.info["groupmembers"]].remove(self.userCharsetToDatabase(user.Name))
1052                except ValueError :
1053                    pass # TODO : Strange, shouldn't it be there ?
1054                else :
1055                    self.doModify(group.ident, fields)
1056                    group.Members.remove(user)
1057               
1058    def addUserPQuota(self, upq) :
1059        """Initializes a user print quota on a printer."""
1060        # first check if an entry already exists
1061        oldentry = self.getUserPQuota(upq.User, upq.Printer)
1062        if oldentry.Exists :
1063            return oldentry # we return the existing entry
1064        uuid = self.genUUID()
1065        uname = self.userCharsetToDatabase(upq.User.Name)
1066        pname = self.userCharsetToDatabase(upq.Printer.Name)
1067        fields = { "cn" : uuid,
1068                   "objectClass" : ["pykotaObject", "pykotaUserPQuota"],
1069                   "pykotaUserName" : uname,
1070                   "pykotaPrinterName" : pname,
1071                   "pykotaSoftLimit" : str(upq.SoftLimit),
1072                   "pykotaHardLimit" : str(upq.HardLimit),
1073                   "pykotaDateLimit" : str(upq.DateLimit),
1074                   "pykotaPageCounter" : str(upq.PageCounter or 0),
1075                   "pykotaLifePageCounter" : str(upq.LifePageCounter or 0),
1076                   "pykotaWarnCount" : str(upq.WarnCount or 0),
1077                   "pykotaMaxJobSize" : str(upq.MaxJobSize or 0),
1078                 } 
1079        if self.info["userquotabase"].lower() == "user" :
1080            dn = "cn=%s,%s" % (uuid, upq.User.ident)
1081        else :   
1082            dn = "cn=%s,%s" % (uuid, self.info["userquotabase"])
1083        self.doAdd(dn, fields)
1084        upq.isDirty = False
1085        return None # the entry created doesn't need further modification
1086       
1087    def addGroupPQuota(self, gpq) :
1088        """Initializes a group print quota on a printer."""
1089        oldentry = self.getGroupPQuota(gpq.Group, gpq.Printer)
1090        if oldentry.Exists :
1091            return oldentry # we return the existing entry
1092        uuid = self.genUUID()
1093        gname = self.userCharsetToDatabase(gpq.Group.Name)
1094        pname = self.userCharsetToDatabase(gpq.Printer.Name)
1095        fields = { "cn" : uuid,
1096                   "objectClass" : ["pykotaObject", "pykotaGroupPQuota"],
1097                   "pykotaGroupName" : gname,
1098                   "pykotaPrinterName" : pname,
1099                   "pykotaDateLimit" : "None",
1100                 } 
1101        if self.info["groupquotabase"].lower() == "group" :
1102            dn = "cn=%s,%s" % (uuid, gpq.Group.ident)
1103        else :   
1104            dn = "cn=%s,%s" % (uuid, self.info["groupquotabase"])
1105        self.doAdd(dn, fields)
1106        gpq.isDirty = False
1107        return None # the entry created doesn't need further modification
1108       
1109    def savePrinter(self, printer) :   
1110        """Saves the printer to the database in a single operation."""
1111        fields = {
1112                   "pykotaPassThrough" : (printer.PassThrough and "t") or "f",
1113                   "pykotaMaxJobSize" : str(printer.MaxJobSize or 0),
1114                   "description" : self.userCharsetToDatabase(printer.Description or ""),
1115                   "pykotaPricePerPage" : str(printer.PricePerPage or 0.0),
1116                   "pykotaPricePerJob" : str(printer.PricePerJob or 0.0),
1117                 }
1118        self.doModify(printer.ident, fields)
1119       
1120    def saveUser(self, user) :
1121        """Saves the user to the database in a single operation."""
1122        newfields = {
1123                       "pykotaLimitBy" : (user.LimitBy or "quota"),
1124                       "pykotaOverCharge" : str(user.OverCharge),
1125                       "description" : self.userCharsetToDatabase(user.Description or ""), 
1126                    }   
1127        if user.Email :
1128            newfields.update({self.info["usermail"]: user.Email})
1129        self.doModify(user.ident, newfields)
1130       
1131        newfields = { "pykotaBalance" : str(user.AccountBalance or 0.0),
1132                      "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
1133                    }
1134        self.doModify(user.idbalance, newfields)
1135       
1136    def saveGroup(self, group) :
1137        """Saves the group to the database in a single operation."""
1138        newfields = {
1139                       "pykotaLimitBy" : (group.LimitBy or "quota"),
1140                       "description" : self.userCharsetToDatabase(group.Description or ""), 
1141                    }   
1142        self.doModify(group.ident, newfields)
1143       
1144    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
1145        """Sets the date limit permanently for a user print quota."""
1146        fields = {
1147                   "pykotaDateLimit" : datelimit,
1148                 }
1149        return self.doModify(userpquota.ident, fields)
1150           
1151    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
1152        """Sets the date limit permanently for a group print quota."""
1153        fields = {
1154                   "pykotaDateLimit" : datelimit,
1155                 }
1156        return self.doModify(grouppquota.ident, fields)
1157       
1158    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
1159        """Increase page counters for a user print quota."""
1160        fields = {
1161                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1162                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1163                 }
1164        return self.doModify(userpquota.ident, fields)         
1165       
1166    def decreaseUserAccountBalance(self, user, amount) :   
1167        """Decreases user's account balance from an amount."""
1168        fields = {
1169                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
1170                 }
1171        return self.doModify(user.idbalance, fields, flushcache=1)         
1172       
1173    def writeNewPayment(self, user, amount, comment="") :
1174        """Adds a new payment to the payments history."""
1175        payments = []
1176        for payment in user.Payments :
1177            payments.append("%s # %s # %s" % (payment[0], str(payment[1]), base64.encodestring(self.userCharsetToDatabase(payment[2])).strip()))
1178        payments.append("%s # %s # %s" % (str(DateTime.now()), str(amount), base64.encodestring(self.userCharsetToDatabase(comment)).strip()))
1179        fields = {
1180                   "pykotaPayments" : payments,
1181                 }
1182        return self.doModify(user.idbalance, fields)         
1183       
1184    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
1185        """Sets the last job's size permanently."""
1186        fields = {
1187                   "pykotaJobSize" : str(jobsize),
1188                   "pykotaJobPrice" : str(jobprice),
1189                 }
1190        self.doModify(lastjob.ident, fields)         
1191       
1192    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, precomputedsize=None, precomputedprice=None) :
1193        """Adds a job in a printer's history."""
1194        uname = self.userCharsetToDatabase(user.Name)
1195        pname = self.userCharsetToDatabase(printer.Name)
1196        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1197            uuid = self.genUUID()
1198            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
1199        else :   
1200            uuid = printer.LastJob.ident[3:].split(",")[0]
1201            dn = printer.LastJob.ident
1202        if self.privacy :   
1203            # For legal reasons, we want to hide the title, filename and options
1204            title = filename = options = "hidden"
1205        fields = {
1206                   "objectClass" : ["pykotaObject", "pykotaJob"],
1207                   "cn" : uuid,
1208                   "pykotaUserName" : uname,
1209                   "pykotaPrinterName" : pname,
1210                   "pykotaJobId" : jobid,
1211                   "pykotaPrinterPageCounter" : str(pagecounter),
1212                   "pykotaAction" : action,
1213                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
1214                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
1215                   "pykotaCopies" : str(copies), 
1216                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
1217                   "pykotaHostName" : str(clienthost), 
1218                   "pykotaJobSizeBytes" : str(jobsizebytes),
1219                   "pykotaMD5Sum" : str(jobmd5sum),
1220                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
1221                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
1222                   "pykotaPrecomputedJobSize" : str(precomputedsize),
1223                   "pykotaPrecomputedJobPrice" : str(precomputedprice),
1224                 }
1225        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1226            if jobsize is not None :         
1227                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1228            self.doAdd(dn, fields)
1229        else :   
1230            # here we explicitly want to reset jobsize to 'None' if needed
1231            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1232            self.doModify(dn, fields)
1233           
1234        if printer.LastJob.Exists :
1235            fields = {
1236                       "pykotaLastJobIdent" : uuid,
1237                     }
1238            self.doModify(printer.LastJob.lastjobident, fields)         
1239        else :   
1240            lastjuuid = self.genUUID()
1241            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
1242            fields = {
1243                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
1244                       "cn" : lastjuuid,
1245                       "pykotaPrinterName" : pname,
1246                       "pykotaLastJobIdent" : uuid,
1247                     } 
1248            self.doAdd(lastjdn, fields)         
1249           
1250    def saveUserPQuota(self, userpquota) :
1251        """Saves an user print quota entry."""
1252        fields = { 
1253                   "pykotaSoftLimit" : str(userpquota.SoftLimit),
1254                   "pykotaHardLimit" : str(userpquota.HardLimit),
1255                   "pykotaDateLimit" : str(userpquota.DateLimit),
1256                   "pykotaWarnCount" : str(userpquota.WarnCount or 0),
1257                   "pykotaPageCounter" : str(userpquota.PageCounter or 0),
1258                   "pykotaLifePageCounter" : str(userpquota.LifePageCounter or 0),
1259                   "pykotaMaxJobSize" : str(userpquota.MaxJobSize or 0),
1260                 }
1261        self.doModify(userpquota.ident, fields)
1262       
1263    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1264        """Sets the warn counter value for a user quota."""
1265        fields = { 
1266                   "pykotaWarnCount" : str(warncount or 0),
1267                 }
1268        self.doModify(userpquota.ident, fields)
1269       
1270    def increaseUserPQuotaWarnCount(self, userpquota) :
1271        """Increases the warn counter value for a user quota."""
1272        fields = {
1273                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1274                 }
1275        return self.doModify(userpquota.ident, fields)         
1276       
1277    def saveGroupPQuota(self, grouppquota) :
1278        """Saves a group print quota entry."""
1279        fields = { 
1280                   "pykotaSoftLimit" : str(grouppquota.SoftLimit),
1281                   "pykotaHardLimit" : str(grouppquota.HardLimit),
1282                   "pykotaDateLimit" : str(grouppquota.DateLimit),
1283                   "pykotaMaxJobSize" : str(grouppquota.MaxJobSize or 0),
1284                 }
1285        self.doModify(grouppquota.ident, fields)
1286           
1287    def writePrinterToGroup(self, pgroup, printer) :
1288        """Puts a printer into a printer group."""
1289        if printer.ident not in pgroup.uniqueMember :
1290            pgroup.uniqueMember.append(printer.ident)
1291            fields = {
1292                       "uniqueMember" : pgroup.uniqueMember
1293                     } 
1294            self.doModify(pgroup.ident, fields)         
1295           
1296    def removePrinterFromGroup(self, pgroup, printer) :
1297        """Removes a printer from a printer group."""
1298        try :
1299            pgroup.uniqueMember.remove(printer.ident)
1300        except ValueError :   
1301            pass
1302        else :   
1303            fields = {
1304                       "uniqueMember" : pgroup.uniqueMember,
1305                     } 
1306            self.doModify(pgroup.ident, fields)         
1307           
1308    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
1309        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1310        precond = "(objectClass=pykotaJob)"
1311        where = []
1312        if user is not None :
1313            where.append("(pykotaUserName=%s)" % self.userCharsetToDatabase(user.Name))
1314        if printer is not None :
1315            where.append("(pykotaPrinterName=%s)" % self.userCharsetToDatabase(printer.Name))
1316        if hostname is not None :
1317            where.append("(pykotaHostName=%s)" % hostname)
1318        if billingcode is not None :
1319            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1320        if where :   
1321            where = "(&%s)" % "".join([precond] + where)
1322        else :   
1323            where = precond
1324        jobs = []   
1325        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1326                                               "pykotaHostName", 
1327                                               "pykotaUserName", 
1328                                               "pykotaPrinterName", 
1329                                               "pykotaJobId", 
1330                                               "pykotaPrinterPageCounter", 
1331                                               "pykotaAction", 
1332                                               "pykotaJobSize", 
1333                                               "pykotaJobPrice", 
1334                                               "pykotaFileName", 
1335                                               "pykotaTitle", 
1336                                               "pykotaCopies", 
1337                                               "pykotaOptions", 
1338                                               "pykotaBillingCode", 
1339                                               "pykotaPages", 
1340                                               "pykotaMD5Sum", 
1341                                               "pykotaPrecomputedJobSize",
1342                                               "pykotaPrecomputedJobPrice",
1343                                               "createTimestamp" ], 
1344                                      base=self.info["jobbase"])
1345        if result :
1346            for (ident, fields) in result :
1347                job = StorageJob(self)
1348                job.ident = ident
1349                job.JobId = fields.get("pykotaJobId")[0]
1350                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1351                try :
1352                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1353                except ValueError :   
1354                    job.JobSize = None
1355                try :   
1356                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1357                except ValueError :
1358                    job.JobPrice = None
1359                job.JobAction = fields.get("pykotaAction", [""])[0]
1360                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1361                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1362                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1363                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1364                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1365                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1366                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1367                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1368                job.JobPages = fields.get("pykotaPages", [""])[0]
1369                try :
1370                    job.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
1371                except ValueError :   
1372                    job.PrecomputedJobSize = None
1373                try :   
1374                    job.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
1375                except ValueError :
1376                    job.PrecomputedJobPrice = None
1377                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1378                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1379                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
1380                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
1381                job.JobDate = mxtime.strftime("%Y%m%d %H:%M:%S")
1382                if ((start is None) and (end is None)) or \
1383                   ((start is None) and (job.JobDate <= end)) or \
1384                   ((end is None) and (job.JobDate >= start)) or \
1385                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1386                    job.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
1387                    job.PrinterName = self.databaseToUserCharset(fields.get("pykotaPrinterName")[0])
1388                    job.Exists = 1
1389                    jobs.append(job)
1390            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1391            if limit :   
1392                jobs = jobs[:int(limit)]
1393        return jobs
1394       
1395    def deleteUser(self, user) :   
1396        """Completely deletes an user from the Quota Storage."""
1397        uname = self.userCharsetToDatabase(user.Name)
1398        todelete = []   
1399        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % uname, base=self.info["jobbase"])
1400        for (ident, fields) in result :
1401            todelete.append(ident)
1402        if self.info["userquotabase"].lower() == "user" :
1403            base = self.info["userbase"]
1404        else :
1405            base = self.info["userquotabase"]
1406        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % uname, \
1407                                  ["pykotaPrinterName", "pykotaUserName"], \
1408                                  base=base)
1409        for (ident, fields) in result :
1410            # ensure the user print quota entry will be deleted
1411            todelete.append(ident)
1412           
1413            # if last job of current printer was printed by the user
1414            # to delete, we also need to delete the printer's last job entry.
1415            printer = self.getPrinter(self.databaseToUserCharset(fields["pykotaPrinterName"][0]))
1416            if printer.LastJob.UserName == user.Name :
1417                todelete.append(printer.LastJob.lastjobident)
1418           
1419        for ident in todelete :   
1420            self.doDelete(ident)
1421           
1422        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1423        if result :
1424            fields = result[0][1]
1425            for k in fields.keys() :
1426                if k.startswith("pykota") :
1427                    del fields[k]
1428                elif k.lower() == "objectclass" :   
1429                    todelete = []
1430                    for i in range(len(fields[k])) :
1431                        if fields[k][i].startswith("pykota") : 
1432                            todelete.append(i)
1433                    todelete.sort()       
1434                    todelete.reverse()
1435                    for i in todelete :
1436                        del fields[k][i]
1437            if fields.get("objectClass") or fields.get("objectclass") :
1438                self.doModify(user.ident, fields, ignoreold=0)       
1439            else :   
1440                self.doDelete(user.ident)
1441        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % \
1442                                   uname, \
1443                                   ["pykotaUserName"], \
1444                                   base=self.info["balancebase"])
1445        for (ident, fields) in result :
1446            self.doDelete(ident)
1447       
1448    def deleteGroup(self, group) :   
1449        """Completely deletes a group from the Quota Storage."""
1450        gname = self.userCharsetToDatabase(group.Name)
1451        if self.info["groupquotabase"].lower() == "group" :
1452            base = self.info["groupbase"]
1453        else :
1454            base = self.info["groupquotabase"]
1455        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % \
1456                                  gname, \
1457                                  ["pykotaGroupName"], \
1458                                  base=base)
1459        for (ident, fields) in result :
1460            self.doDelete(ident)
1461        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1462        if result :
1463            fields = result[0][1]
1464            for k in fields.keys() :
1465                if k.startswith("pykota") :
1466                    del fields[k]
1467                elif k.lower() == "objectclass" :   
1468                    todelete = []
1469                    for i in range(len(fields[k])) :
1470                        if fields[k][i].startswith("pykota") : 
1471                            todelete.append(i)
1472                    todelete.sort()       
1473                    todelete.reverse()
1474                    for i in todelete :
1475                        del fields[k][i]
1476            if fields.get("objectClass") or fields.get("objectclass") :
1477                self.doModify(group.ident, fields, ignoreold=0)       
1478            else :   
1479                self.doDelete(group.ident)
1480               
1481    def deleteManyBillingCodes(self, billingcodes) :
1482        """Deletes many billing codes."""
1483        for bcode in billingcodes :
1484            bcode.delete()
1485       
1486    def deleteManyUsers(self, users) :       
1487        """Deletes many users."""
1488        for user in users :
1489            user.delete()
1490           
1491    def deleteManyGroups(self, groups) :       
1492        """Deletes many groups."""
1493        for group in groups :
1494            group.delete()
1495       
1496    def deleteManyPrinters(self, printers) :       
1497        """Deletes many printers."""
1498        for printer in printers :
1499            printer.delete()
1500       
1501    def deleteManyUserPQuotas(self, printers, users) :       
1502        """Deletes many user print quota entries."""
1503        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1504        for printer in printers :
1505            for user in users :
1506                upq = self.getUserPQuota(user, printer)
1507                if upq.Exists :
1508                    upq.delete()
1509           
1510    def deleteManyGroupPQuotas(self, printers, groups) :
1511        """Deletes many group print quota entries."""
1512        # TODO : grab all with a single (possibly VERY huge) filter if possible (might depend on the LDAP server !)
1513        for printer in printers :
1514            for group in groups :
1515                gpq = self.getGroupPQuota(group, printer)
1516                if gpq.Exists :
1517                    gpq.delete()
1518               
1519    def deleteUserPQuota(self, upquota) :   
1520        """Completely deletes an user print quota entry from the database."""
1521        uname = self.userCharsetToDatabase(upquota.User.Name)
1522        pname = self.userCharsetToDatabase(upquota.Printer.Name)
1523        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s)(pykotaPrinterName=%s))" \
1524                                   % (uname, pname), \
1525                                   base=self.info["jobbase"])
1526        for (ident, fields) in result :
1527            self.doDelete(ident)
1528        if upquota.Printer.LastJob.UserName == upquota.User.Name :
1529            self.doDelete(upquota.Printer.LastJob.lastjobident)
1530        self.doDelete(upquota.ident)
1531       
1532    def deleteGroupPQuota(self, gpquota) :   
1533        """Completely deletes a group print quota entry from the database."""
1534        self.doDelete(gpquota.ident)
1535               
1536    def deletePrinter(self, printer) :   
1537        """Completely deletes a printer from the Quota Storage."""
1538        pname = self.userCharsetToDatabase(printer.Name)
1539        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % pname, base=self.info["lastjobbase"])
1540        for (ident, fields) in result :
1541            self.doDelete(ident)
1542        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % pname, base=self.info["jobbase"])
1543        for (ident, fields) in result :
1544            self.doDelete(ident)
1545        if self.info["groupquotabase"].lower() == "group" :
1546            base = self.info["groupbase"]
1547        else :
1548            base = self.info["groupquotabase"]
1549        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1550        for (ident, fields) in result :
1551            self.doDelete(ident)
1552        if self.info["userquotabase"].lower() == "user" :
1553            base = self.info["userbase"]
1554        else :
1555            base = self.info["userquotabase"]
1556        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1557        for (ident, fields) in result :
1558            self.doDelete(ident)
1559        for parent in self.getParentPrinters(printer) : 
1560            try :
1561                parent.uniqueMember.remove(printer.ident)
1562            except ValueError :   
1563                pass
1564            else :   
1565                fields = {
1566                           "uniqueMember" : parent.uniqueMember,
1567                         } 
1568                self.doModify(parent.ident, fields)         
1569        self.doDelete(printer.ident)   
1570       
1571    def deleteBillingCode(self, code) :
1572        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1573        self.doDelete(code.ident)
1574       
1575    def extractPrinters(self, extractonly={}) :
1576        """Extracts all printer records."""
1577        pname = extractonly.get("printername")
1578        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1579        if entries :
1580            result = [ ("dn", "printername", "priceperpage", "priceperjob", "description", "maxjobsize", "passthrough") ]
1581            for entry in entries :
1582                if entry.PassThrough in (1, "1", "t", "true", "T", "TRUE", "True") :
1583                    passthrough = "t"
1584                else :   
1585                    passthrough = "f"
1586                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description, entry.MaxJobSize, passthrough))
1587            return result 
1588       
1589    def extractUsers(self, extractonly={}) :
1590        """Extracts all user records."""
1591        uname = extractonly.get("username")
1592        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1593        if entries :
1594            result = [ ("dn", "username", "balance", "lifetimepaid", "limitby", "email", "description") ]
1595            for entry in entries :
1596                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email, entry.Description))
1597            return result 
1598       
1599    def extractBillingcodes(self, extractonly={}) :
1600        """Extracts all billing codes records."""
1601        billingcode = extractonly.get("billingcode")
1602        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1603        if entries :
1604            result = [ ("dn", "billingcode", "balance", "pagecounter", "description") ]
1605            for entry in entries :
1606                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1607            return result 
1608       
1609    def extractGroups(self, extractonly={}) :
1610        """Extracts all group records."""
1611        gname = extractonly.get("groupname")
1612        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1613        if entries :
1614            result = [ ("dn", "groupname", "limitby", "balance", "lifetimepaid", "description") ]
1615            for entry in entries :
1616                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid, entry.Description))
1617            return result 
1618       
1619    def extractPayments(self, extractonly={}) :
1620        """Extracts all payment records."""
1621        uname = extractonly.get("username")
1622        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1623        if entries :
1624            result = [ ("username", "amount", "date", "description") ]
1625            for entry in entries :
1626                for (date, amount, description) in entry.Payments :
1627                    result.append((entry.Name, amount, date, description))
1628            return result       
1629       
1630    def extractUpquotas(self, extractonly={}) :
1631        """Extracts all userpquota records."""
1632        pname = extractonly.get("printername")
1633        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1634        if entries :
1635            result = [ ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1636            uname = extractonly.get("username")
1637            for entry in entries :
1638                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1639                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1640            return result
1641       
1642    def extractGpquotas(self, extractonly={}) :
1643        """Extracts all grouppquota records."""
1644        pname = extractonly.get("printername")
1645        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1646        if entries :
1647            result = [ ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1648            gname = extractonly.get("groupname")
1649            for entry in entries :
1650                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1651                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1652            return result
1653       
1654    def extractUmembers(self, extractonly={}) :
1655        """Extracts all user groups members."""
1656        gname = extractonly.get("groupname")
1657        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1658        if entries :
1659            result = [ ("groupname", "username", "groupdn", "userdn") ]
1660            uname = extractonly.get("username")
1661            for entry in entries :
1662                for member in entry.Members :
1663                    if (uname is None) or (member.Name == uname) :
1664                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1665            return result       
1666               
1667    def extractPmembers(self, extractonly={}) :
1668        """Extracts all printer groups members."""
1669        pname = extractonly.get("printername")
1670        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1671        if entries :
1672            result = [ ("pgroupname", "printername", "pgroupdn", "printerdn") ]
1673            pgname = extractonly.get("pgroupname")
1674            for entry in entries :
1675                for parent in self.getParentPrinters(entry) :
1676                    if (pgname is None) or (parent.Name == pgname) :
1677                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1678            return result       
1679       
1680    def extractHistory(self, extractonly={}) :
1681        """Extracts all jobhistory records."""
1682        uname = extractonly.get("username")
1683        if uname :
1684            user = self.getUser(uname)
1685        else :   
1686            user = None
1687        pname = extractonly.get("printername")
1688        if pname :
1689            printer = self.getPrinter(pname)
1690        else :   
1691            printer = None
1692        startdate = extractonly.get("start")
1693        enddate = extractonly.get("end")
1694        (startdate, enddate) = self.cleanDates(startdate, enddate)
1695        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), limit=None, start=startdate, end=enddate)
1696        if entries :
1697            result = [ ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode", "precomputedjobsize", "precomputedjobprice") ] 
1698            for entry in entries :
1699                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, entry.PrecomputedJobSize, entry.PrecomputedJobPrice)) 
1700            return result
1701           
1702    def getBillingCodeFromBackend(self, label) :
1703        """Extracts billing code information given its label : returns first matching billing code."""
1704        code = StorageBillingCode(self, label)
1705        ulabel = self.userCharsetToDatabase(label)
1706        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % \
1707                                  ulabel, \
1708                                  ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], \
1709                                  base=self.info["billingcodebase"])
1710        if result :
1711            fields = result[0][1]       # take only first matching code, ignore the rest
1712            code.ident = result[0][0]
1713            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1714            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1715            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1716            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1717            code.Exists = 1
1718        return code   
1719       
1720    def addBillingCode(self, bcode) :
1721        """Adds a billing code to the quota storage, returns it."""
1722        oldentry = self.getBillingCode(bcode.BillingCode)
1723        if oldentry.Exists :
1724            return oldentry # we return the existing entry
1725        uuid = self.genUUID()
1726        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1727        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1728                   "cn" : uuid,
1729                   "pykotaBillingCode" : self.userCharsetToDatabase(bcode.BillingCode),
1730                   "pykotaPageCounter" : str(bcode.PageCounter or 0),
1731                   "pykotaBalance" : str(bcode.Balance or 0.0),
1732                   "description" : self.userCharsetToDatabase(bcode.Description or ""), 
1733                 } 
1734        self.doAdd(dn, fields)
1735        bcode.isDirty = False
1736        return None # the entry created doesn't need further modification
1737       
1738    def saveBillingCode(self, bcode) :
1739        """Sets the new description for a billing code."""
1740        fields = {
1741                   "description" : self.userCharsetToDatabase(bcode.Description or ""), 
1742                   "pykotaPageCounter" : str(bcode.PageCounter or 0),
1743                   "pykotaBalance" : str(bcode.Balance or 0.0),
1744                 }
1745        self.doModify(bcode.ident, fields)
1746           
1747    def getMatchingBillingCodes(self, billingcodepattern) :
1748        """Returns the list of all billing codes which match a certain pattern."""
1749        codes = []
1750        result = self.doSearch("objectClass=pykotaBilling", \
1751                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1752                                base=self.info["billingcodebase"])
1753        if result :
1754            patterns = billingcodepattern.split(",")
1755            try :
1756                patdict = {}.fromkeys(patterns)
1757            except AttributeError :   
1758                # Python v2.2 or earlier
1759                patdict = {}
1760                for p in patterns :
1761                    patdict[p] = None
1762            for (codeid, fields) in result :
1763                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1764                if patdict.has_key(codename) or self.tool.matchString(codename, patterns) :
1765                    code = StorageBillingCode(self, codename)
1766                    code.ident = codeid
1767                    code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1768                    code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1769                    code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1770                    code.Exists = 1
1771                    codes.append(code)
1772                    self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1773        return codes       
1774       
1775    def consumeBillingCode(self, bcode, pagecounter, balance) :
1776        """Consumes from a billing code."""
1777        fields = {
1778                   "pykotaBalance" : { "operator" : "-", "value" : balance, "convert" : float },
1779                   "pykotaPageCounter" : { "operator" : "+", "value" : pagecounter, "convert" : int },
1780                 }
1781        return self.doModify(bcode.ident, fields)         
Note: See TracBrowser for help on using the browser.