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

Revision 3549, 97.8 kB (checked in by jerome, 14 years ago)

Removed support for the MaxJobSize? attribute for users group print quota
entries : I couldn't see a real use for this at the moment, and it would
complexify the code. This support might reappear later however. Added full
support for the MaxJobSize? attribute for user print quota entries,
editable with edpykota's new --maxjobsize command line switch. Changed
the internal handling of the MaxJobSize? attribute for printers :
internally 0 used to mean unlimited, it now allows one to forbid
printing onto a particular printer. The database upgrade script (only
for PostgreSQL) takes care of this.
IMPORTANT : the database schema changes. A database upgrade script is
provided for PostgreSQL only. The LDAP schema doesn't change to not
break any existing LDAP directory, so the pykotaMaxJobSize attribute is
still allowed on group print quota entries, but never used.
Seems to work as expected, for a change :-)
Fixes #15.

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