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

Revision 3419, 98.5 kB (checked in by jerome, 16 years ago)

Improved debug output.

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