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

Revision 2707, 87.4 kB (checked in by jerome, 18 years ago)

Improved user modification speed by around 15%.

  • 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 deletePrinter(self, printer) :   
1408        """Completely deletes a printer from the Quota Storage."""
1409        pname = self.userCharsetToDatabase(printer.Name)
1410        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % pname, base=self.info["lastjobbase"])
1411        for (ident, fields) in result :
1412            self.doDelete(ident)
1413        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % pname, base=self.info["jobbase"])
1414        for (ident, fields) in result :
1415            self.doDelete(ident)
1416        if self.info["groupquotabase"].lower() == "group" :
1417            base = self.info["groupbase"]
1418        else :
1419            base = self.info["groupquotabase"]
1420        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1421        for (ident, fields) in result :
1422            self.doDelete(ident)
1423        if self.info["userquotabase"].lower() == "user" :
1424            base = self.info["userbase"]
1425        else :
1426            base = self.info["userquotabase"]
1427        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1428        for (ident, fields) in result :
1429            self.doDelete(ident)
1430        for parent in self.getParentPrinters(printer) : 
1431            try :
1432                parent.uniqueMember.remove(printer.ident)
1433            except ValueError :   
1434                pass
1435            else :   
1436                fields = {
1437                           "uniqueMember" : parent.uniqueMember,
1438                         } 
1439                self.doModify(parent.ident, fields)         
1440        self.doDelete(printer.ident)   
1441       
1442    def deleteBillingCode(self, code) :
1443        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1444        self.doDelete(code.ident)
1445       
1446    def extractPrinters(self, extractonly={}) :
1447        """Extracts all printer records."""
1448        pname = extractonly.get("printername")
1449        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1450        if entries :
1451            result = [ ("dn", "printername", "priceperpage", "priceperjob", "description", "maxjobsize", "passthrough") ]
1452            for entry in entries :
1453                if entry.PassThrough in (1, "1", "t", "true", "T", "TRUE", "True") :
1454                    passthrough = "t"
1455                else :   
1456                    passthrough = "f"
1457                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description, entry.MaxJobSize, passthrough))
1458            return result 
1459       
1460    def extractUsers(self, extractonly={}) :
1461        """Extracts all user records."""
1462        uname = extractonly.get("username")
1463        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1464        if entries :
1465            result = [ ("dn", "username", "balance", "lifetimepaid", "limitby", "email") ]
1466            for entry in entries :
1467                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email))
1468            return result 
1469       
1470    def extractBillingcodes(self, extractonly={}) :
1471        """Extracts all billing codes records."""
1472        billingcode = extractonly.get("billingcode")
1473        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1474        if entries :
1475            result = [ ("dn", "billingcode", "balance", "pagecounter", "description") ]
1476            for entry in entries :
1477                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1478            return result 
1479       
1480    def extractGroups(self, extractonly={}) :
1481        """Extracts all group records."""
1482        gname = extractonly.get("groupname")
1483        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1484        if entries :
1485            result = [ ("dn", "groupname", "limitby", "balance", "lifetimepaid") ]
1486            for entry in entries :
1487                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid))
1488            return result 
1489       
1490    def extractPayments(self, extractonly={}) :
1491        """Extracts all payment records."""
1492        uname = extractonly.get("username")
1493        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1494        if entries :
1495            result = [ ("username", "amount", "date", "description") ]
1496            for entry in entries :
1497                for (date, amount, description) in entry.Payments :
1498                    result.append((entry.Name, amount, date, description))
1499            return result       
1500       
1501    def extractUpquotas(self, extractonly={}) :
1502        """Extracts all userpquota records."""
1503        pname = extractonly.get("printername")
1504        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1505        if entries :
1506            result = [ ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1507            uname = extractonly.get("username")
1508            for entry in entries :
1509                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1510                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1511            return result
1512       
1513    def extractGpquotas(self, extractonly={}) :
1514        """Extracts all grouppquota records."""
1515        pname = extractonly.get("printername")
1516        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1517        if entries :
1518            result = [ ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1519            gname = extractonly.get("groupname")
1520            for entry in entries :
1521                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1522                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1523            return result
1524       
1525    def extractUmembers(self, extractonly={}) :
1526        """Extracts all user groups members."""
1527        gname = extractonly.get("groupname")
1528        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1529        if entries :
1530            result = [ ("groupname", "username", "groupdn", "userdn") ]
1531            uname = extractonly.get("username")
1532            for entry in entries :
1533                for member in entry.Members :
1534                    if (uname is None) or (member.Name == uname) :
1535                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1536            return result       
1537               
1538    def extractPmembers(self, extractonly={}) :
1539        """Extracts all printer groups members."""
1540        pname = extractonly.get("printername")
1541        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1542        if entries :
1543            result = [ ("pgroupname", "printername", "pgroupdn", "printerdn") ]
1544            pgname = extractonly.get("pgroupname")
1545            for entry in entries :
1546                for parent in self.getParentPrinters(entry) :
1547                    if (pgname is None) or (parent.Name == pgname) :
1548                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1549            return result       
1550       
1551    def extractHistory(self, extractonly={}) :
1552        """Extracts all jobhistory records."""
1553        uname = extractonly.get("username")
1554        if uname :
1555            user = self.getUser(uname)
1556        else :   
1557            user = None
1558        pname = extractonly.get("printername")
1559        if pname :
1560            printer = self.getPrinter(pname)
1561        else :   
1562            printer = None
1563        startdate = extractonly.get("start")
1564        enddate = extractonly.get("end")
1565        (startdate, enddate) = self.cleanDates(startdate, enddate)
1566        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), limit=None, start=startdate, end=enddate)
1567        if entries :
1568            result = [ ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode", "precomputedjobsize", "precomputedjobprice") ] 
1569            for entry in entries :
1570                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)) 
1571            return result
1572           
1573    def getBillingCodeFromBackend(self, label) :
1574        """Extracts billing code information given its label : returns first matching billing code."""
1575        code = StorageBillingCode(self, label)
1576        ulabel = self.userCharsetToDatabase(label)
1577        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % \
1578                                  ulabel, \
1579                                  ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], \
1580                                  base=self.info["billingcodebase"])
1581        if result :
1582            fields = result[0][1]       # take only first matching code, ignore the rest
1583            code.ident = result[0][0]
1584            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1585            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1586            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1587            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1588            code.Exists = 1
1589        return code   
1590       
1591    def addBillingCode(self, label) :
1592        """Adds a billing code to the quota storage, returns it."""
1593        uuid = self.genUUID()
1594        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1595        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1596                   "cn" : uuid,
1597                   "pykotaBillingCode" : self.userCharsetToDatabase(label),
1598                   "pykotaPageCounter" : "0",
1599                   "pykotaBalance" : "0.0",
1600                 } 
1601        self.doAdd(dn, fields)
1602        return self.getBillingCode(label)
1603       
1604    def saveBillingCode(self, code) :
1605        """Sets the new description for a billing code."""
1606        fields = {
1607                   "description" : self.userCharsetToDatabase(code.Description or ""), 
1608                   "pykotaPageCounter" : str(code.PageCounter),
1609                   "pykotaBalance" : str(code.Balance),
1610                 }
1611        self.doModify(code.ident, fields)
1612           
1613    def getMatchingBillingCodes(self, billingcodepattern) :
1614        """Returns the list of all billing codes which match a certain pattern."""
1615        codes = []
1616        result = self.doSearch("objectClass=pykotaBilling", \
1617                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1618                                base=self.info["billingcodebase"])
1619        if result :
1620            patterns = billingcodepattern.split(",")
1621            for (codeid, fields) in result :
1622                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1623                if self.tool.matchString(codename, patterns) :
1624                    code = StorageBillingCode(self, codename)
1625                    code.ident = codeid
1626                    code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1627                    code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1628                    code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1629                    code.Exists = 1
1630                    codes.append(code)
1631                    self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1632        return codes       
1633       
1634    def consumeBillingCode(self, code, pagecounter, balance) :
1635        """Consumes from a billing code."""
1636        fields = {
1637                   "pykotaBalance" : { "operator" : "-", "value" : balance, "convert" : float },
1638                   "pykotaPageCounter" : { "operator" : "+", "value" : pagecounter, "convert" : int },
1639                 }
1640        return self.doModify(code.ident, fields)         
Note: See TracBrowser for help on using the browser.