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

Revision 2722, 90.1 kB (checked in by jerome, 18 years ago)

Added userful helpers to retrieve all interesting datas with a single query
(LDAP will use more queries, as usual)

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