root / pykota / branches / 1.26_fixes / pykota / storages / ldapstorage.py @ 3522

Revision 3522, 97.7 kB (checked in by jerome, 14 years ago)

Backport of the fix to #52.

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