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

Revision 3555, 98.1 kB (checked in by jerome, 12 years ago)

Strange bug in payment's content.

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