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

Revision 2718, 88.3 kB (checked in by jerome, 18 years ago)

Fixed deletion code for print quota entries.

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