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

Revision 2735, 89.7 kB (checked in by jerome, 18 years ago)

Completely untested modification stuff...

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