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

Revision 3521, 97.7 kB (checked in by jerome, 12 years ago)

Removed some code specific to Python v2.2 and earlier.
Improved the work done by bse@… to fix #52.
IMPORTANT : the same optimisation is currently done for users only, not for
users groups, printers or billing codes. And more importantly the code
was not ported to the LDAP backend. I need more time to do all this.

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