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

Revision 2706, 87.7 kB (checked in by jerome, 18 years ago)

pkusers now mostly works. Removing an user from a group with
LDAP is not yet done though. Also no test was done with
LDAP yet.
filldb now really creates users (it uses pkusers instead
of edpykota)

  • 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    def saveGroup(self, group) :
1051        """Saves the group to the database in a single operation."""
1052        newfields = {
1053                       "pykotaLimitBy" : (group.LimitBy or "quota"),
1054                    }   
1055        if group.Description is not None : 
1056            newfields.update({"description": self.userCharsetToDatabase(group.Description)})
1057        self.doModify(group.ident, newfields)
1058       
1059    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
1060        """Sets the date limit permanently for a user print quota."""
1061        fields = {
1062                   "pykotaDateLimit" : datelimit,
1063                 }
1064        return self.doModify(userpquota.ident, fields)
1065           
1066    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
1067        """Sets the date limit permanently for a group print quota."""
1068        fields = {
1069                   "pykotaDateLimit" : datelimit,
1070                 }
1071        return self.doModify(grouppquota.ident, fields)
1072       
1073    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
1074        """Increase page counters for a user print quota."""
1075        fields = {
1076                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1077                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1078                 }
1079        return self.doModify(userpquota.ident, fields)         
1080       
1081    def writeUserPQuotaPagesCounters(self, userpquota, newpagecounter, newlifepagecounter) :   
1082        """Sets the new page counters permanently for a user print quota."""
1083        fields = {
1084                   "pykotaPageCounter" : str(newpagecounter),
1085                   "pykotaLifePageCounter" : str(newlifepagecounter),
1086                   "pykotaDateLimit" : None,
1087                   "pykotaWarnCount" : "0",
1088                 } 
1089        return self.doModify(userpquota.ident, fields)         
1090       
1091    def decreaseUserAccountBalance(self, user, amount) :   
1092        """Decreases user's account balance from an amount."""
1093        fields = {
1094                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
1095                 }
1096        return self.doModify(user.idbalance, fields, flushcache=1)         
1097       
1098    def writeUserAccountBalance(self, user, newbalance, newlifetimepaid=None) :   
1099        """Sets the new account balance and eventually new lifetime paid."""
1100        fields = {
1101                   "pykotaBalance" : str(newbalance),
1102                 }
1103        if newlifetimepaid is not None :
1104            fields.update({ "pykotaLifeTimePaid" : str(newlifetimepaid) })
1105        return self.doModify(user.idbalance, fields)         
1106           
1107    def writeNewPayment(self, user, amount, comment="") :
1108        """Adds a new payment to the payments history."""
1109        payments = []
1110        for payment in user.Payments :
1111            payments.append("%s # %s # %s" % (payment[0], str(payment[1]), base64.encodestring(self.userCharsetToDatabase(payment[2])).strip()))
1112        payments.append("%s # %s # %s" % (str(DateTime.now()), str(amount), base64.encodestring(self.userCharsetToDatabase(comment)).strip()))
1113        fields = {
1114                   "pykotaPayments" : payments,
1115                 }
1116        return self.doModify(user.idbalance, fields)         
1117       
1118    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
1119        """Sets the last job's size permanently."""
1120        fields = {
1121                   "pykotaJobSize" : str(jobsize),
1122                   "pykotaJobPrice" : str(jobprice),
1123                 }
1124        self.doModify(lastjob.ident, fields)         
1125       
1126    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) :
1127        """Adds a job in a printer's history."""
1128        uname = self.userCharsetToDatabase(user.Name)
1129        pname = self.userCharsetToDatabase(printer.Name)
1130        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1131            uuid = self.genUUID()
1132            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
1133        else :   
1134            uuid = printer.LastJob.ident[3:].split(",")[0]
1135            dn = printer.LastJob.ident
1136        if self.privacy :   
1137            # For legal reasons, we want to hide the title, filename and options
1138            title = filename = options = "hidden"
1139        fields = {
1140                   "objectClass" : ["pykotaObject", "pykotaJob"],
1141                   "cn" : uuid,
1142                   "pykotaUserName" : uname,
1143                   "pykotaPrinterName" : pname,
1144                   "pykotaJobId" : jobid,
1145                   "pykotaPrinterPageCounter" : str(pagecounter),
1146                   "pykotaAction" : action,
1147                   "pykotaFileName" : ((filename is None) and "None") or self.userCharsetToDatabase(filename), 
1148                   "pykotaTitle" : ((title is None) and "None") or self.userCharsetToDatabase(title), 
1149                   "pykotaCopies" : str(copies), 
1150                   "pykotaOptions" : ((options is None) and "None") or self.userCharsetToDatabase(options), 
1151                   "pykotaHostName" : str(clienthost), 
1152                   "pykotaJobSizeBytes" : str(jobsizebytes),
1153                   "pykotaMD5Sum" : str(jobmd5sum),
1154                   "pykotaPages" : jobpages,            # don't add this attribute if it is not set, so no string conversion
1155                   "pykotaBillingCode" : self.userCharsetToDatabase(jobbilling), # don't add this attribute if it is not set, so no string conversion
1156                   "pykotaPrecomputedJobSize" : str(precomputedsize),
1157                   "pykotaPrecomputedJobPrice" : str(precomputedprice),
1158                 }
1159        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1160            if jobsize is not None :         
1161                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1162            self.doAdd(dn, fields)
1163        else :   
1164            # here we explicitly want to reset jobsize to 'None' if needed
1165            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1166            self.doModify(dn, fields)
1167           
1168        if printer.LastJob.Exists :
1169            fields = {
1170                       "pykotaLastJobIdent" : uuid,
1171                     }
1172            self.doModify(printer.LastJob.lastjobident, fields)         
1173        else :   
1174            lastjuuid = self.genUUID()
1175            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
1176            fields = {
1177                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
1178                       "cn" : lastjuuid,
1179                       "pykotaPrinterName" : pname,
1180                       "pykotaLastJobIdent" : uuid,
1181                     } 
1182            self.doAdd(lastjdn, fields)         
1183           
1184    def writeUserPQuotaLimits(self, userpquota, softlimit, hardlimit) :
1185        """Sets soft and hard limits for a user quota."""
1186        fields = { 
1187                   "pykotaSoftLimit" : str(softlimit),
1188                   "pykotaHardLimit" : str(hardlimit),
1189                   "pykotaDateLimit" : "None",
1190                   "pykotaWarnCount" : "0",
1191                 }
1192        self.doModify(userpquota.ident, fields)
1193       
1194    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
1195        """Sets the warn counter value for a user quota."""
1196        fields = { 
1197                   "pykotaWarnCount" : str(warncount or 0),
1198                 }
1199        self.doModify(userpquota.ident, fields)
1200       
1201    def increaseUserPQuotaWarnCount(self, userpquota) :
1202        """Increases the warn counter value for a user quota."""
1203        fields = {
1204                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
1205                 }
1206        return self.doModify(userpquota.ident, fields)         
1207       
1208    def writeGroupPQuotaLimits(self, grouppquota, softlimit, hardlimit) :
1209        """Sets soft and hard limits for a group quota on a specific printer."""
1210        fields = { 
1211                   "pykotaSoftLimit" : str(softlimit),
1212                   "pykotaHardLimit" : str(hardlimit),
1213                   "pykotaDateLimit" : "None",
1214                 }
1215        self.doModify(grouppquota.ident, fields)
1216           
1217    def writePrinterToGroup(self, pgroup, printer) :
1218        """Puts a printer into a printer group."""
1219        if printer.ident not in pgroup.uniqueMember :
1220            pgroup.uniqueMember.append(printer.ident)
1221            fields = {
1222                       "uniqueMember" : pgroup.uniqueMember
1223                     } 
1224            self.doModify(pgroup.ident, fields)         
1225           
1226    def removePrinterFromGroup(self, pgroup, printer) :
1227        """Removes a printer from a printer group."""
1228        try :
1229            pgroup.uniqueMember.remove(printer.ident)
1230        except ValueError :   
1231            pass
1232        else :   
1233            fields = {
1234                       "uniqueMember" : pgroup.uniqueMember,
1235                     } 
1236            self.doModify(pgroup.ident, fields)         
1237           
1238    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
1239        """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
1240        precond = "(objectClass=pykotaJob)"
1241        where = []
1242        if user is not None :
1243            where.append("(pykotaUserName=%s)" % self.userCharsetToDatabase(user.Name))
1244        if printer is not None :
1245            where.append("(pykotaPrinterName=%s)" % self.userCharsetToDatabase(printer.Name))
1246        if hostname is not None :
1247            where.append("(pykotaHostName=%s)" % hostname)
1248        if billingcode is not None :
1249            where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
1250        if where :   
1251            where = "(&%s)" % "".join([precond] + where)
1252        else :   
1253            where = precond
1254        jobs = []   
1255        result = self.doSearch(where, fields=[ "pykotaJobSizeBytes", 
1256                                               "pykotaHostName", 
1257                                               "pykotaUserName", 
1258                                               "pykotaPrinterName", 
1259                                               "pykotaJobId", 
1260                                               "pykotaPrinterPageCounter", 
1261                                               "pykotaAction", 
1262                                               "pykotaJobSize", 
1263                                               "pykotaJobPrice", 
1264                                               "pykotaFileName", 
1265                                               "pykotaTitle", 
1266                                               "pykotaCopies", 
1267                                               "pykotaOptions", 
1268                                               "pykotaBillingCode", 
1269                                               "pykotaPages", 
1270                                               "pykotaMD5Sum", 
1271                                               "pykotaPrecomputedJobSize",
1272                                               "pykotaPrecomputedJobPrice",
1273                                               "createTimestamp" ], 
1274                                      base=self.info["jobbase"])
1275        if result :
1276            for (ident, fields) in result :
1277                job = StorageJob(self)
1278                job.ident = ident
1279                job.JobId = fields.get("pykotaJobId")[0]
1280                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1281                try :
1282                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1283                except ValueError :   
1284                    job.JobSize = None
1285                try :   
1286                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1287                except ValueError :
1288                    job.JobPrice = None
1289                job.JobAction = fields.get("pykotaAction", [""])[0]
1290                job.JobFileName = self.databaseToUserCharset(fields.get("pykotaFileName", [""])[0]) 
1291                job.JobTitle = self.databaseToUserCharset(fields.get("pykotaTitle", [""])[0]) 
1292                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1293                job.JobOptions = self.databaseToUserCharset(fields.get("pykotaOptions", [""])[0]) 
1294                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1295                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1296                job.JobBillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [None])[0])
1297                job.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
1298                job.JobPages = fields.get("pykotaPages", [""])[0]
1299                try :
1300                    job.PrecomputedJobSize = int(fields.get("pykotaPrecomputedJobSize", [0])[0])
1301                except ValueError :   
1302                    job.PrecomputedJobSize = None
1303                try :   
1304                    job.PrecomputedJobPrice = float(fields.get("pykotaPrecomputedJobPrice", [0.0])[0])
1305                except ValueError :
1306                    job.PrecomputedJobPrice = None
1307                if job.JobTitle == job.JobFileName == job.JobOptions == "hidden" :
1308                    (job.JobTitle, job.JobFileName, job.JobOptions) = (_("Hidden because of privacy concerns"),) * 3
1309                date = fields.get("createTimestamp", ["19700101000000Z"])[0] # It's in UTC !
1310                mxtime = DateTime.strptime(date[:14], "%Y%m%d%H%M%S").localtime()
1311                job.JobDate = mxtime.strftime("%Y%m%d %H:%M:%S")
1312                if ((start is None) and (end is None)) or \
1313                   ((start is None) and (job.JobDate <= end)) or \
1314                   ((end is None) and (job.JobDate >= start)) or \
1315                   ((job.JobDate >= start) and (job.JobDate <= end)) :
1316                    job.UserName = self.databaseToUserCharset(fields.get("pykotaUserName")[0])
1317                    job.PrinterName = self.databaseToUserCharset(fields.get("pykotaPrinterName")[0])
1318                    job.Exists = 1
1319                    jobs.append(job)
1320            jobs.sort(lambda x, y : cmp(y.JobDate, x.JobDate))       
1321            if limit :   
1322                jobs = jobs[:int(limit)]
1323        return jobs
1324       
1325    def deleteUser(self, user) :   
1326        """Completely deletes an user from the Quota Storage."""
1327        uname = self.userCharsetToDatabase(user.Name)
1328        todelete = []   
1329        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % uname, base=self.info["jobbase"])
1330        for (ident, fields) in result :
1331            todelete.append(ident)
1332        if self.info["userquotabase"].lower() == "user" :
1333            base = self.info["userbase"]
1334        else :
1335            base = self.info["userquotabase"]
1336        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % uname, \
1337                                  ["pykotaPrinterName", "pykotaUserName"], \
1338                                  base=base)
1339        for (ident, fields) in result :
1340            # ensure the user print quota entry will be deleted
1341            todelete.append(ident)
1342           
1343            # if last job of current printer was printed by the user
1344            # to delete, we also need to delete the printer's last job entry.
1345            printer = self.getPrinter(self.databaseToUserCharset(fields["pykotaPrinterName"][0]))
1346            if printer.LastJob.UserName == user.Name :
1347                todelete.append(printer.LastJob.lastjobident)
1348           
1349        for ident in todelete :   
1350            self.doDelete(ident)
1351           
1352        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1353        if result :
1354            fields = result[0][1]
1355            for k in fields.keys() :
1356                if k.startswith("pykota") :
1357                    del fields[k]
1358                elif k.lower() == "objectclass" :   
1359                    todelete = []
1360                    for i in range(len(fields[k])) :
1361                        if fields[k][i].startswith("pykota") : 
1362                            todelete.append(i)
1363                    todelete.sort()       
1364                    todelete.reverse()
1365                    for i in todelete :
1366                        del fields[k][i]
1367            if fields.get("objectClass") or fields.get("objectclass") :
1368                self.doModify(user.ident, fields, ignoreold=0)       
1369            else :   
1370                self.doDelete(user.ident)
1371        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % \
1372                                   uname, \
1373                                   ["pykotaUserName"], \
1374                                   base=self.info["balancebase"])
1375        for (ident, fields) in result :
1376            self.doDelete(ident)
1377       
1378    def deleteGroup(self, group) :   
1379        """Completely deletes a group from the Quota Storage."""
1380        gname = self.userCharsetToDatabase(group.Name)
1381        if self.info["groupquotabase"].lower() == "group" :
1382            base = self.info["groupbase"]
1383        else :
1384            base = self.info["groupquotabase"]
1385        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % \
1386                                  gname, \
1387                                  ["pykotaGroupName"], \
1388                                  base=base)
1389        for (ident, fields) in result :
1390            self.doDelete(ident)
1391        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1392        if result :
1393            fields = result[0][1]
1394            for k in fields.keys() :
1395                if k.startswith("pykota") :
1396                    del fields[k]
1397                elif k.lower() == "objectclass" :   
1398                    todelete = []
1399                    for i in range(len(fields[k])) :
1400                        if fields[k][i].startswith("pykota") : 
1401                            todelete.append(i)
1402                    todelete.sort()       
1403                    todelete.reverse()
1404                    for i in todelete :
1405                        del fields[k][i]
1406            if fields.get("objectClass") or fields.get("objectclass") :
1407                self.doModify(group.ident, fields, ignoreold=0)       
1408            else :   
1409                self.doDelete(group.ident)
1410               
1411    def deletePrinter(self, printer) :   
1412        """Completely deletes a printer from the Quota Storage."""
1413        pname = self.userCharsetToDatabase(printer.Name)
1414        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % pname, base=self.info["lastjobbase"])
1415        for (ident, fields) in result :
1416            self.doDelete(ident)
1417        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % pname, base=self.info["jobbase"])
1418        for (ident, fields) in result :
1419            self.doDelete(ident)
1420        if self.info["groupquotabase"].lower() == "group" :
1421            base = self.info["groupbase"]
1422        else :
1423            base = self.info["groupquotabase"]
1424        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1425        for (ident, fields) in result :
1426            self.doDelete(ident)
1427        if self.info["userquotabase"].lower() == "user" :
1428            base = self.info["userbase"]
1429        else :
1430            base = self.info["userquotabase"]
1431        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % pname, base=base)
1432        for (ident, fields) in result :
1433            self.doDelete(ident)
1434        for parent in self.getParentPrinters(printer) : 
1435            try :
1436                parent.uniqueMember.remove(printer.ident)
1437            except ValueError :   
1438                pass
1439            else :   
1440                fields = {
1441                           "uniqueMember" : parent.uniqueMember,
1442                         } 
1443                self.doModify(parent.ident, fields)         
1444        self.doDelete(printer.ident)   
1445       
1446    def deleteBillingCode(self, code) :
1447        """Deletes a billing code from the Quota Storage (no entries are deleted from the history)"""
1448        self.doDelete(code.ident)
1449       
1450    def extractPrinters(self, extractonly={}) :
1451        """Extracts all printer records."""
1452        pname = extractonly.get("printername")
1453        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1454        if entries :
1455            result = [ ("dn", "printername", "priceperpage", "priceperjob", "description", "maxjobsize", "passthrough") ]
1456            for entry in entries :
1457                if entry.PassThrough in (1, "1", "t", "true", "T", "TRUE", "True") :
1458                    passthrough = "t"
1459                else :   
1460                    passthrough = "f"
1461                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description, entry.MaxJobSize, passthrough))
1462            return result 
1463       
1464    def extractUsers(self, extractonly={}) :
1465        """Extracts all user records."""
1466        uname = extractonly.get("username")
1467        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1468        if entries :
1469            result = [ ("dn", "username", "balance", "lifetimepaid", "limitby", "email") ]
1470            for entry in entries :
1471                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy, entry.Email))
1472            return result 
1473       
1474    def extractBillingcodes(self, extractonly={}) :
1475        """Extracts all billing codes records."""
1476        billingcode = extractonly.get("billingcode")
1477        entries = [b for b in [self.getBillingCode(label) for label in self.getAllBillingCodes(billingcode)] if b.Exists]
1478        if entries :
1479            result = [ ("dn", "billingcode", "balance", "pagecounter", "description") ]
1480            for entry in entries :
1481                result.append((entry.ident, entry.BillingCode, entry.Balance, entry.PageCounter, entry.Description))
1482            return result 
1483       
1484    def extractGroups(self, extractonly={}) :
1485        """Extracts all group records."""
1486        gname = extractonly.get("groupname")
1487        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1488        if entries :
1489            result = [ ("dn", "groupname", "limitby", "balance", "lifetimepaid") ]
1490            for entry in entries :
1491                result.append((entry.ident, entry.Name, entry.LimitBy, entry.AccountBalance, entry.LifeTimePaid))
1492            return result 
1493       
1494    def extractPayments(self, extractonly={}) :
1495        """Extracts all payment records."""
1496        uname = extractonly.get("username")
1497        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames(uname)] if u.Exists]
1498        if entries :
1499            result = [ ("username", "amount", "date", "description") ]
1500            for entry in entries :
1501                for (date, amount, description) in entry.Payments :
1502                    result.append((entry.Name, amount, date, description))
1503            return result       
1504       
1505    def extractUpquotas(self, extractonly={}) :
1506        """Extracts all userpquota records."""
1507        pname = extractonly.get("printername")
1508        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1509        if entries :
1510            result = [ ("username", "printername", "dn", "userdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1511            uname = extractonly.get("username")
1512            for entry in entries :
1513                for (user, userpquota) in self.getPrinterUsersAndQuotas(entry, names=[uname or "*"]) :
1514                    result.append((user.Name, entry.Name, userpquota.ident, user.ident, entry.ident, userpquota.LifePageCounter, userpquota.PageCounter, userpquota.SoftLimit, userpquota.HardLimit, userpquota.DateLimit))
1515            return result
1516       
1517    def extractGpquotas(self, extractonly={}) :
1518        """Extracts all grouppquota records."""
1519        pname = extractonly.get("printername")
1520        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1521        if entries :
1522            result = [ ("groupname", "printername", "dn", "groupdn", "printerdn", "lifepagecounter", "pagecounter", "softlimit", "hardlimit", "datelimit") ]
1523            gname = extractonly.get("groupname")
1524            for entry in entries :
1525                for (group, grouppquota) in self.getPrinterGroupsAndQuotas(entry, names=[gname or "*"]) :
1526                    result.append((group.Name, entry.Name, grouppquota.ident, group.ident, entry.ident, grouppquota.LifePageCounter, grouppquota.PageCounter, grouppquota.SoftLimit, grouppquota.HardLimit, grouppquota.DateLimit))
1527            return result
1528       
1529    def extractUmembers(self, extractonly={}) :
1530        """Extracts all user groups members."""
1531        gname = extractonly.get("groupname")
1532        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames(gname)] if g.Exists]
1533        if entries :
1534            result = [ ("groupname", "username", "groupdn", "userdn") ]
1535            uname = extractonly.get("username")
1536            for entry in entries :
1537                for member in entry.Members :
1538                    if (uname is None) or (member.Name == uname) :
1539                        result.append((entry.Name, member.Name, entry.ident, member.ident))
1540            return result       
1541               
1542    def extractPmembers(self, extractonly={}) :
1543        """Extracts all printer groups members."""
1544        pname = extractonly.get("printername")
1545        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames(pname)] if p.Exists]
1546        if entries :
1547            result = [ ("pgroupname", "printername", "pgroupdn", "printerdn") ]
1548            pgname = extractonly.get("pgroupname")
1549            for entry in entries :
1550                for parent in self.getParentPrinters(entry) :
1551                    if (pgname is None) or (parent.Name == pgname) :
1552                        result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1553            return result       
1554       
1555    def extractHistory(self, extractonly={}) :
1556        """Extracts all jobhistory records."""
1557        uname = extractonly.get("username")
1558        if uname :
1559            user = self.getUser(uname)
1560        else :   
1561            user = None
1562        pname = extractonly.get("printername")
1563        if pname :
1564            printer = self.getPrinter(pname)
1565        else :   
1566            printer = None
1567        startdate = extractonly.get("start")
1568        enddate = extractonly.get("end")
1569        (startdate, enddate) = self.cleanDates(startdate, enddate)
1570        entries = self.retrieveHistory(user, printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), limit=None, start=startdate, end=enddate)
1571        if entries :
1572            result = [ ("username", "printername", "dn", "jobid", "pagecounter", "jobsize", "action", "jobdate", "filename", "title", "copies", "options", "jobprice", "hostname", "jobsizebytes", "md5sum", "pages", "billingcode", "precomputedjobsize", "precomputedjobprice") ] 
1573            for entry in entries :
1574                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)) 
1575            return result
1576           
1577    def getBillingCodeFromBackend(self, label) :
1578        """Extracts billing code information given its label : returns first matching billing code."""
1579        code = StorageBillingCode(self, label)
1580        ulabel = self.userCharsetToDatabase(label)
1581        result = self.doSearch("(&(objectClass=pykotaBilling)(pykotaBillingCode=%s))" % \
1582                                  ulabel, \
1583                                  ["pykotaBillingCode", "pykotaBalance", "pykotaPageCounter", "description"], \
1584                                  base=self.info["billingcodebase"])
1585        if result :
1586            fields = result[0][1]       # take only first matching code, ignore the rest
1587            code.ident = result[0][0]
1588            code.BillingCode = self.databaseToUserCharset(fields.get("pykotaBillingCode", [ulabel])[0])
1589            code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1590            code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1591            code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1592            code.Exists = 1
1593        return code   
1594       
1595    def addBillingCode(self, label) :
1596        """Adds a billing code to the quota storage, returns it."""
1597        uuid = self.genUUID()
1598        dn = "cn=%s,%s" % (uuid, self.info["billingcodebase"])
1599        fields = { "objectClass" : ["pykotaObject", "pykotaBilling"],
1600                   "cn" : uuid,
1601                   "pykotaBillingCode" : self.userCharsetToDatabase(label),
1602                   "pykotaPageCounter" : "0",
1603                   "pykotaBalance" : "0.0",
1604                 } 
1605        self.doAdd(dn, fields)
1606        return self.getBillingCode(label)
1607       
1608    def saveBillingCode(self, code) :
1609        """Sets the new description for a billing code."""
1610        fields = {
1611                   "description" : self.userCharsetToDatabase(code.Description or ""), 
1612                   "pykotaPageCounter" : str(code.PageCounter),
1613                   "pykotaBalance" : str(code.Balance),
1614                 }
1615        self.doModify(code.ident, fields)
1616           
1617    def getMatchingBillingCodes(self, billingcodepattern) :
1618        """Returns the list of all billing codes which match a certain pattern."""
1619        codes = []
1620        result = self.doSearch("objectClass=pykotaBilling", \
1621                                ["pykotaBillingCode", "description", "pykotaPageCounter", "pykotaBalance"], \
1622                                base=self.info["billingcodebase"])
1623        if result :
1624            patterns = billingcodepattern.split(",")
1625            for (codeid, fields) in result :
1626                codename = self.databaseToUserCharset(fields.get("pykotaBillingCode", [""])[0])
1627                if self.tool.matchString(codename, patterns) :
1628                    code = StorageBillingCode(self, codename)
1629                    code.ident = codeid
1630                    code.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
1631                    code.Balance = float(fields.get("pykotaBalance", [0.0])[0])
1632                    code.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
1633                    code.Exists = 1
1634                    codes.append(code)
1635                    self.cacheEntry("BILLINGCODES", code.BillingCode, code)
1636        return codes       
1637       
1638    def consumeBillingCode(self, code, pagecounter, balance) :
1639        """Consumes from a billing code."""
1640        fields = {
1641                   "pykotaBalance" : { "operator" : "-", "value" : balance, "convert" : float },
1642                   "pykotaPageCounter" : { "operator" : "+", "value" : pagecounter, "convert" : int },
1643                 }
1644        return self.doModify(code.ident, fields)         
Note: See TracBrowser for help on using the browser.