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

Revision 1761, 65.5 kB (checked in by jalet, 20 years ago)

Should now correctly deal with charsets both when storing into databases and when
retrieving datas. Works with both PostgreSQL and LDAP.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota : Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23# $Log$
24# Revision 1.78  2004/10/02 05:48:56  jalet
25# Should now correctly deal with charsets both when storing into databases and when
26# retrieving datas. Works with both PostgreSQL and LDAP.
27#
28# Revision 1.77  2004/09/28 14:29:00  jalet
29# dumpykota for LDAP backend is almost there.
30#
31# Revision 1.76  2004/09/28 09:11:56  jalet
32# Fix for accented chars in print job's title, filename, and options
33#
34# Revision 1.75  2004/09/24 20:21:50  jalet
35# Fixed pykotaAccountBalance object location during creation
36#
37# Revision 1.74  2004/09/02 10:09:30  jalet
38# Fixed bug in LDAP user deletion code which didn't correctly delete the user's
39# pykotaLastJob entries.
40#
41# Revision 1.73  2004/07/17 20:37:27  jalet
42# Missing file... Am I really stupid ?
43#
44# Revision 1.72  2004/07/01 19:56:43  jalet
45# Better dispatching of error messages
46#
47# Revision 1.71  2004/07/01 17:45:49  jalet
48# Added code to handle the description field for printers
49#
50# Revision 1.70  2004/06/10 20:50:25  jalet
51# Better log message
52#
53# Revision 1.69  2004/06/05 22:18:04  jalet
54# Now catches some exceptions earlier.
55# storage.py and ldapstorage.py : removed old comments
56#
57# Revision 1.68  2004/06/05 22:03:50  jalet
58# Payments history is now stored in database
59#
60# Revision 1.67  2004/06/03 23:14:10  jalet
61# Now stores the job's size in bytes in the database.
62# Preliminary work on payments storage : database schemas are OK now,
63# but no code to store payments yet.
64# Removed schema picture, not relevant anymore.
65#
66# Revision 1.66  2004/05/28 20:56:45  jalet
67# Extended syntax for LDAP specific newuser and newgroup directives. Untested.
68#
69# Revision 1.65  2004/05/27 12:52:12  jalet
70# More useful error message in case of misconfiguration of an LDAP  search base
71# in pykota.conf
72#
73# Revision 1.64  2004/05/26 14:50:01  jalet
74# First try at saving the job-originating-hostname in the database
75#
76# Revision 1.63  2004/05/06 12:37:46  jalet
77# pkpgcounter : comments
78# pkprinters : when --add is used, existing printers are now skipped.
79#
80# Revision 1.62  2004/03/05 14:31:58  jalet
81# Improvement on strange history entries
82#
83# Revision 1.61  2004/03/05 13:19:53  jalet
84# Code safer wrt entries created in other tools
85#
86# Revision 1.60  2004/03/02 14:39:02  jalet
87# Final fix for printers searching
88#
89# Revision 1.59  2004/03/02 14:35:46  jalet
90# Missing test when searching printer objects when these objects were manually
91# created and don't contain the pykotaPrinterName attribute
92#
93# Revision 1.58  2004/02/27 09:30:33  jalet
94# datelimit wasn't reset when modifying soft and hard limits with the LDAP backend
95#
96# Revision 1.57  2004/02/26 14:18:07  jalet
97# Should fix the remaining bugs wrt printers groups and users groups.
98#
99# Revision 1.56  2004/02/25 16:52:39  jalet
100# Small fix wrt empty user groups
101#
102# Revision 1.55  2004/02/23 22:53:21  jalet
103# Don't retrieve data when it's not needed, to avoid database queries
104#
105# Revision 1.54  2004/02/20 16:38:39  jalet
106# ldapcache directive marked as experimental
107#
108# Revision 1.53  2004/02/20 14:42:21  jalet
109# Experimental ldapcache directive added
110#
111# Revision 1.52  2004/02/17 23:41:48  jalet
112# Preliminary work on low-level LDAP specific cache.
113#
114# Revision 1.51  2004/02/04 13:24:41  jalet
115# pkprinters can now remove printers from printers groups.
116#
117# Revision 1.50  2004/02/04 11:17:00  jalet
118# pkprinters command line tool added.
119#
120# Revision 1.49  2004/01/29 22:35:45  jalet
121# Small fix from Matt.
122#
123# Revision 1.48  2004/01/12 14:35:02  jalet
124# Printing history added to CGI script.
125#
126# Revision 1.47  2004/01/10 09:44:02  jalet
127# Fixed potential accuracy problem if a user printed on several printers at
128# the very same time.
129#
130# Revision 1.46  2004/01/08 16:33:27  jalet
131# Additionnal check to not create a circular printers group.
132#
133# Revision 1.45  2004/01/08 16:24:49  jalet
134# edpykota now supports adding printers to printer groups.
135#
136# Revision 1.44  2004/01/08 14:10:33  jalet
137# Copyright year changed.
138#
139# Revision 1.43  2004/01/06 14:24:59  jalet
140# Printer groups should be cached now, if caching is enabled.
141#
142# Revision 1.42  2003/12/29 14:12:48  uid67467
143# Tries to workaround possible integrity violations when retrieving printer groups
144#
145# Revision 1.41  2003/12/27 16:49:25  uid67467
146# Should be ok now.
147#
148# Revision 1.40  2003/11/29 22:02:14  jalet
149# Don't try to retrieve the user print quota information if current printer
150# doesn't exist.
151#
152# Revision 1.39  2003/11/26 23:35:32  jalet
153# Added a bit of code to support the setting of the user's email address
154# which was ignored during writes for now.
155#
156# Revision 1.38  2003/11/24 09:54:06  jalet
157# Small fix for LDAP when pykotaOptions attribute wasn't present.
158#
159# Revision 1.37  2003/11/23 19:01:37  jalet
160# Job price added to history
161#
162# Revision 1.36  2003/11/21 14:28:46  jalet
163# More complete job history.
164#
165# Revision 1.35  2003/11/12 13:06:37  jalet
166# Bug fix wrt no user/group name command line argument to edpykota
167#
168# Revision 1.34  2003/10/24 08:37:55  jalet
169# More complete messages in case of LDAP failure.
170# LDAP database connection is now unbound on exit too.
171#
172# Revision 1.33  2003/10/08 07:01:20  jalet
173# Job history can be disabled.
174# Some typos in README.
175# More messages in setup script.
176#
177# Revision 1.32  2003/10/07 14:23:25  jalet
178# More work on cache
179#
180# Revision 1.31  2003/10/07 09:07:30  jalet
181# Character encoding added to please latest version of Python
182#
183# Revision 1.30  2003/10/06 14:42:36  jalet
184# LDAP group access will be slower when cache is disabled, but at least code
185# is consistent with the rest of the caching mechanis, but at least code
186# is consistent with the rest of the caching mechanism
187#
188# Revision 1.29  2003/10/06 13:12:28  jalet
189# More work on caching
190#
191# Revision 1.28  2003/10/03 12:27:02  jalet
192# Several optimizations, especially with LDAP backend
193#
194# Revision 1.27  2003/10/03 08:57:55  jalet
195# Caching mechanism now caches all that's cacheable.
196#
197# Revision 1.26  2003/10/02 20:23:18  jalet
198# Storage caching mechanism added.
199#
200# Revision 1.25  2003/08/20 15:56:24  jalet
201# Better user and group deletion
202#
203# Revision 1.24  2003/07/29 20:55:17  jalet
204# 1.14 is out !
205#
206# Revision 1.23  2003/07/29 19:52:32  jalet
207# Forgot to read the email field from LDAP
208#
209# Revision 1.22  2003/07/29 09:54:03  jalet
210# Added configurable LDAP mail attribute support
211#
212# Revision 1.21  2003/07/28 09:11:12  jalet
213# PyKota now tries to add its attributes intelligently in existing LDAP
214# directories.
215#
216# Revision 1.20  2003/07/25 10:41:30  jalet
217# Better documentation.
218# pykotme now displays the current user's account balance.
219# Some test changed in ldap module.
220#
221# Revision 1.19  2003/07/14 14:18:16  jalet
222# Wrong documentation strings
223#
224# Revision 1.18  2003/07/11 14:23:13  jalet
225# When adding an user only adds one object containing both the user and
226# its account balance instead of two objects.
227#
228# Revision 1.17  2003/07/07 12:51:07  jalet
229# Small fix
230#
231# Revision 1.16  2003/07/07 12:11:13  jalet
232# Small fix
233#
234# Revision 1.15  2003/07/07 11:49:24  jalet
235# Lots of small fixes with the help of PyChecker
236#
237# Revision 1.14  2003/07/07 08:33:18  jalet
238# Bug fix due to a typo in LDAP code
239#
240# Revision 1.13  2003/07/05 07:46:50  jalet
241# The previous bug fix was incomplete.
242#
243# Revision 1.12  2003/06/30 13:54:21  jalet
244# Sorts by user / group name
245#
246# Revision 1.11  2003/06/25 14:10:01  jalet
247# Hey, it may work (edpykota --reset excepted) !
248#
249# Revision 1.10  2003/06/16 21:55:15  jalet
250# More work on LDAP, again. Problem detected.
251#
252# Revision 1.9  2003/06/16 11:59:09  jalet
253# More work on LDAP
254#
255# Revision 1.8  2003/06/15 22:26:52  jalet
256# More work on LDAP
257#
258# Revision 1.7  2003/06/14 22:44:21  jalet
259# More work on LDAP storage backend.
260#
261# Revision 1.6  2003/06/13 19:07:57  jalet
262# Two big bugs fixed, time to release something ;-)
263#
264# Revision 1.5  2003/06/10 16:37:54  jalet
265# Deletion of the second user which is not needed anymore.
266# Added a debug configuration field in /etc/pykota.conf
267# All queries can now be sent to the logger in debug mode, this will
268# greatly help improve performance when time for this will come.
269#
270# Revision 1.4  2003/06/10 10:45:32  jalet
271# Not implemented methods now raise an exception when called.
272#
273# Revision 1.3  2003/06/06 20:49:15  jalet
274# Very latest schema. UNTESTED.
275#
276# Revision 1.2  2003/06/06 14:21:08  jalet
277# New LDAP schema.
278# Small bug fixes.
279#
280# Revision 1.1  2003/06/05 11:19:13  jalet
281# More good work on LDAP storage.
282#
283#
284#
285
286#
287# My IANA assigned number, for
288# "Conseil Internet & Logiciels Libres, J�me Alet"
289# is 16868. Use this as a base to create the LDAP schema.
290#
291
292import os
293import types
294import time
295import md5
296from mx import DateTime
297
298from pykota.storage import PyKotaStorageError,BaseStorage,StorageObject,StorageUser,StorageGroup,StoragePrinter,StorageJob,StorageLastJob,StorageUserPQuota,StorageGroupPQuota
299
300try :
301    import ldap
302    import ldap.modlist
303except ImportError :   
304    import sys
305    # TODO : to translate or not to translate ?
306    raise PyKotaStorageError, "This python version (%s) doesn't seem to have the python-ldap module installed correctly." % sys.version.split()[0]
307   
308class Storage(BaseStorage) :
309    def __init__(self, pykotatool, host, dbname, user, passwd) :
310        """Opens the LDAP connection."""
311        BaseStorage.__init__(self, pykotatool)
312        self.info = pykotatool.config.getLDAPInfo()
313        try :
314            self.database = ldap.initialize(host) 
315            self.database.simple_bind_s(user, passwd)
316            self.basedn = dbname
317        except ldap.SERVER_DOWN :   
318            raise PyKotaStorageError, "LDAP backend for PyKota seems to be down !" # TODO : translate
319        except ldap.LDAPError :   
320            raise PyKotaStorageError, "Unable to connect to LDAP server %s as %s." % (host, user) # TODO : translate
321        else :   
322            self.useldapcache = pykotatool.config.getLDAPCache()
323            if self.useldapcache :
324                self.tool.logdebug("Low-Level LDAP Caching enabled.")
325                self.ldapcache = {} # low-level cache specific to LDAP backend
326            self.closed = 0
327            self.tool.logdebug("Database opened (host=%s, dbname=%s, user=%s)" % (host, dbname, user))
328           
329    def close(self) :   
330        """Closes the database connection."""
331        if not self.closed :
332            self.database.unbind_s()
333            self.closed = 1
334            self.tool.logdebug("Database closed.")
335       
336    def genUUID(self) :   
337        """Generates an unique identifier.
338       
339           TODO : this one is not unique accross several print servers, but should be sufficient for testing.
340        """
341        return md5.md5("%s" % time.time()).hexdigest()
342       
343    def normalizeFields(self, fields) :   
344        """Ensure all items are lists."""
345        for (k, v) in fields.items() :
346            if type(v) not in (types.TupleType, types.ListType) :
347                if not v :
348                    del fields[k]
349                else :   
350                    fields[k] = [ v ]
351        return fields       
352       
353    def beginTransaction(self) :   
354        """Starts a transaction."""
355        self.tool.logdebug("Transaction begins... WARNING : No transactions in LDAP !")
356       
357    def commitTransaction(self) :   
358        """Commits a transaction."""
359        self.tool.logdebug("Transaction committed. WARNING : No transactions in LDAP !")
360       
361    def rollbackTransaction(self) :     
362        """Rollbacks a transaction."""
363        self.tool.logdebug("Transaction aborted. WARNING : No transaction in LDAP !")
364       
365    def doSearch(self, key, fields=None, base="", scope=ldap.SCOPE_SUBTREE, flushcache=0) :
366        """Does an LDAP search query."""
367        try :
368            base = base or self.basedn
369            if self.useldapcache :
370                # Here we overwrite the fields the app want, to try and
371                # retrieve ALL user defined attributes ("*")
372                # + the createTimestamp attribute, needed by job history
373                #
374                # This may not work with all LDAP servers
375                # but works at least in OpenLDAP (2.1.25)
376                # and iPlanet Directory Server (5.1 SP3)
377                fields = ["*", "createTimestamp"]         
378               
379            if self.useldapcache and (not flushcache) and (scope == ldap.SCOPE_BASE) and self.ldapcache.has_key(base) :
380                entry = self.ldapcache[base]
381                self.tool.logdebug("LDAP cache hit %s => %s" % (base, entry))
382                result = [(base, entry)]
383            else :
384                self.tool.logdebug("QUERY : Filter : %s, BaseDN : %s, Scope : %s, Attributes : %s" % (key, base, scope, fields))
385                result = self.database.search_s(base, scope, key, fields)
386        except ldap.NO_SUCH_OBJECT, msg :       
387            raise PyKotaStorageError, (_("Search base %s doesn't seem to exist. Probable misconfiguration. Please double check /etc/pykota/pykota.conf : %s") % (base, msg))
388        except ldap.LDAPError, msg :   
389            raise PyKotaStorageError, (_("Search for %s(%s) from %s(scope=%s) returned no answer.") % (key, fields, base, scope)) + " : %s" % str(msg)
390        else :     
391            self.tool.logdebug("QUERY : Result : %s" % result)
392            if self.useldapcache :
393                for (dn, attributes) in result :
394                    self.tool.logdebug("LDAP cache store %s => %s" % (dn, attributes))
395                    self.ldapcache[dn] = attributes
396            return result
397           
398    def doAdd(self, dn, fields) :
399        """Adds an entry in the LDAP directory."""
400        # TODO : store into LDAP specific cache
401        fields = self.normalizeFields(fields)
402        try :
403            self.tool.logdebug("QUERY : ADD(%s, %s)" % (dn, str(fields)))
404            entry = ldap.modlist.addModlist(fields)
405            self.tool.logdebug("%s" % entry)
406            self.database.add_s(dn, entry)
407        except ldap.LDAPError, msg :
408            raise PyKotaStorageError, (_("Problem adding LDAP entry (%s, %s)") % (dn, str(fields))) + " : %s" % str(msg)
409        else :
410            if self.useldapcache :
411                self.tool.logdebug("LDAP cache add %s => %s" % (dn, fields))
412                self.ldapcache[dn] = fields
413            return dn
414           
415    def doDelete(self, dn) :
416        """Deletes an entry from the LDAP directory."""
417        # TODO : delete from LDAP specific cache
418        try :
419            self.tool.logdebug("QUERY : Delete(%s)" % dn)
420            self.database.delete_s(dn)
421        except ldap.LDAPError, msg :
422            raise PyKotaStorageError, (_("Problem deleting LDAP entry (%s)") % dn) + " : %s" % str(msg)
423        else :   
424            if self.useldapcache :
425                try :
426                    self.tool.logdebug("LDAP cache del %s" % dn)
427                    del self.ldapcache[dn]
428                except KeyError :   
429                    pass
430           
431    def doModify(self, dn, fields, ignoreold=1, flushcache=0) :
432        """Modifies an entry in the LDAP directory."""
433        try :
434            # TODO : take care of, and update LDAP specific cache
435            if self.useldapcache and not flushcache :
436                if self.ldapcache.has_key(dn) :
437                    old = self.ldapcache[dn]
438                    self.tool.logdebug("LDAP cache hit %s => %s" % (dn, old))
439                    oldentry = {}
440                    for (k, v) in old.items() :
441                        if k != "createTimestamp" :
442                            oldentry[k] = v
443                else :   
444                    self.tool.logdebug("LDAP cache miss %s" % dn)
445                    oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE)[0][1]
446            else :       
447                oldentry = self.doSearch("objectClass=*", base=dn, scope=ldap.SCOPE_BASE, flushcache=flushcache)[0][1]
448            for (k, v) in fields.items() :
449                if type(v) == type({}) :
450                    try :
451                        oldvalue = v["convert"](oldentry.get(k, [0])[0])
452                    except ValueError :   
453                        self.tool.logdebug("Error converting %s with %s(%s)" % (oldentry.get(k), k, v))
454                        oldvalue = 0
455                    if v["operator"] == '+' :
456                        newvalue = oldvalue + v["value"]
457                    else :   
458                        newvalue = oldvalue - v["value"]
459                    fields[k] = str(newvalue)
460            fields = self.normalizeFields(fields)
461            self.tool.logdebug("QUERY : Modify(%s, %s ==> %s)" % (dn, oldentry, fields))
462            entry = ldap.modlist.modifyModlist(oldentry, fields, ignore_oldexistent=ignoreold)
463            modentry = []
464            for (mop, mtyp, mval) in entry :
465                if mtyp != "createTimestamp" :
466                    modentry.append((mop, mtyp, mval))
467            self.tool.logdebug("MODIFY : %s ==> %s ==> %s" % (fields, entry, modentry))
468            if modentry :
469                self.database.modify_s(dn, modentry)
470        except ldap.LDAPError, msg :
471            raise PyKotaStorageError, (_("Problem modifying LDAP entry (%s, %s)") % (dn, fields)) + " : %s" % str(msg)
472        else :
473            if self.useldapcache :
474                cachedentry = self.ldapcache[dn]
475                for (mop, mtyp, mval) in entry :
476                    if mop in (ldap.MOD_ADD, ldap.MOD_REPLACE) :
477                        cachedentry[mtyp] = mval
478                    else :
479                        try :
480                            del cachedentry[mtyp]
481                        except KeyError :   
482                            pass
483                self.tool.logdebug("LDAP cache update %s => %s" % (dn, cachedentry))
484            return dn
485           
486    def getAllPrintersNames(self) :   
487        """Extracts all printer names."""
488        printernames = []
489        result = self.doSearch("objectClass=pykotaPrinter", ["pykotaPrinterName"], base=self.info["printerbase"])
490        if result :
491            printernames = [record[1]["pykotaPrinterName"][0] for record in result]
492        return printernames
493       
494    def getAllUsersNames(self) :   
495        """Extracts all user names."""
496        usernames = []
497        result = self.doSearch("objectClass=pykotaAccount", ["pykotaUserName"], base=self.info["userbase"])
498        if result :
499            usernames = [record[1]["pykotaUserName"][0] for record in result]
500        return usernames
501       
502    def getAllGroupsNames(self) :   
503        """Extracts all group names."""
504        groupnames = []
505        result = self.doSearch("objectClass=pykotaGroup", ["pykotaGroupName"], base=self.info["groupbase"])
506        if result :
507            groupnames = [record[1]["pykotaGroupName"][0] for record in result]
508        return groupnames
509       
510    def getUserFromBackend(self, username) :   
511        """Extracts user information given its name."""
512        user = StorageUser(self, username)
513        result = self.doSearch("(&(objectClass=pykotaAccount)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["userrdn"], username), ["pykotaUserName", "pykotaLimitBy", self.info["usermail"]], base=self.info["userbase"])
514        if result :
515            fields = result[0][1]
516            user.ident = result[0][0]
517            user.Name = fields.get("pykotaUserName", [username])[0] 
518            user.Email = fields.get(self.info["usermail"])
519            if user.Email is not None :
520                user.Email = user.Email[0]
521            user.LimitBy = fields.get("pykotaLimitBy")
522            if user.LimitBy is not None :
523                user.LimitBy = user.LimitBy[0]
524            result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["balancerdn"], username), ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], base=self.info["balancebase"])
525            if not result :
526                raise PyKotaStorageError, _("No pykotaAccountBalance object found for user %s. Did you create LDAP entries manually ?") % username
527            else :
528                fields = result[0][1]
529                user.idbalance = result[0][0]
530                user.AccountBalance = fields.get("pykotaBalance")
531                if user.AccountBalance is not None :
532                    if user.AccountBalance[0].upper() == "NONE" :
533                        user.AccountBalance = None
534                    else :   
535                        user.AccountBalance = float(user.AccountBalance[0])
536                user.AccountBalance = user.AccountBalance or 0.0       
537                user.LifeTimePaid = fields.get("pykotaLifeTimePaid")
538                if user.LifeTimePaid is not None :
539                    if user.LifeTimePaid[0].upper() == "NONE" :
540                        user.LifeTimePaid = None
541                    else :   
542                        user.LifeTimePaid = float(user.LifeTimePaid[0])
543                user.LifeTimePaid = user.LifeTimePaid or 0.0       
544                user.Payments = []
545                for payment in fields.get("pykotaPayments", []) :
546                    (date, amount) = payment.split(" # ")
547                    user.Payments.append((date, amount))
548            user.Exists = 1
549        return user
550       
551    def getGroupFromBackend(self, groupname) :   
552        """Extracts group information given its name."""
553        group = StorageGroup(self, groupname)
554        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % (groupname, self.info["grouprdn"], groupname), ["pykotaGroupName", "pykotaLimitBy"], base=self.info["groupbase"])
555        if result :
556            fields = result[0][1]
557            group.ident = result[0][0]
558            group.Name = fields.get("pykotaGroupName", [groupname])[0] 
559            group.LimitBy = fields.get("pykotaLimitBy")
560            if group.LimitBy is not None :
561                group.LimitBy = group.LimitBy[0]
562            group.AccountBalance = 0.0
563            group.LifeTimePaid = 0.0
564            for member in self.getGroupMembers(group) :
565                if member.Exists :
566                    group.AccountBalance += member.AccountBalance
567                    group.LifeTimePaid += member.LifeTimePaid
568            group.Exists = 1
569        return group
570       
571    def getPrinterFromBackend(self, printername) :       
572        """Extracts printer information given its name : returns first matching printer."""
573        printer = StoragePrinter(self, printername)
574        result = self.doSearch("(&(objectClass=pykotaPrinter)(|(pykotaPrinterName=%s)(%s=%s)))" % (printername, self.info["printerrdn"], printername), ["pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerJob", "uniqueMember", "description"], base=self.info["printerbase"])
575        if result :
576            fields = result[0][1]       # take only first matching printer, ignore the rest
577            printer.ident = result[0][0]
578            printer.Name = fields.get("pykotaPrinterName", [printername])[0] 
579            printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0] or 0.0)
580            printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0] or 0.0)
581            printer.uniqueMember = fields.get("uniqueMember", [])
582            printer.Description = fields.get("description", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
583            printer.Exists = 1
584        return printer   
585       
586    def getUserPQuotaFromBackend(self, user, printer) :       
587        """Extracts a user print quota."""
588        userpquota = StorageUserPQuota(self, user, printer)
589        if printer.Exists and user.Exists :
590            result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s)(pykotaPrinterName=%s))" % (user.Name, printer.Name), ["pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit"], base=self.info["userquotabase"])
591            if result :
592                fields = result[0][1]
593                userpquota.ident = result[0][0]
594                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0] or 0)
595                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0] or 0)
596                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
597                if userpquota.SoftLimit is not None :
598                    if userpquota.SoftLimit[0].upper() == "NONE" :
599                        userpquota.SoftLimit = None
600                    else :   
601                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
602                userpquota.HardLimit = fields.get("pykotaHardLimit")
603                if userpquota.HardLimit is not None :
604                    if userpquota.HardLimit[0].upper() == "NONE" :
605                        userpquota.HardLimit = None
606                    elif userpquota.HardLimit is not None :   
607                        userpquota.HardLimit = int(userpquota.HardLimit[0])
608                userpquota.DateLimit = fields.get("pykotaDateLimit")
609                if userpquota.DateLimit is not None :
610                    if userpquota.DateLimit[0].upper() == "NONE" : 
611                        userpquota.DateLimit = None
612                    else :   
613                        userpquota.DateLimit = userpquota.DateLimit[0]
614                userpquota.Exists = 1
615        return userpquota
616       
617    def getGroupPQuotaFromBackend(self, group, printer) :       
618        """Extracts a group print quota."""
619        grouppquota = StorageGroupPQuota(self, group, printer)
620        if group.Exists :
621            result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s)(pykotaPrinterName=%s))" % (group.Name, printer.Name), ["pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit"], base=self.info["groupquotabase"])
622            if result :
623                fields = result[0][1]
624                grouppquota.ident = result[0][0]
625                grouppquota.SoftLimit = fields.get("pykotaSoftLimit")
626                if grouppquota.SoftLimit is not None :
627                    if grouppquota.SoftLimit[0].upper() == "NONE" :
628                        grouppquota.SoftLimit = None
629                    else :   
630                        grouppquota.SoftLimit = int(grouppquota.SoftLimit[0])
631                grouppquota.HardLimit = fields.get("pykotaHardLimit")
632                if grouppquota.HardLimit is not None :
633                    if grouppquota.HardLimit[0].upper() == "NONE" :
634                        grouppquota.HardLimit = None
635                    else :   
636                        grouppquota.HardLimit = int(grouppquota.HardLimit[0])
637                grouppquota.DateLimit = fields.get("pykotaDateLimit")
638                if grouppquota.DateLimit is not None :
639                    if grouppquota.DateLimit[0].upper() == "NONE" : 
640                        grouppquota.DateLimit = None
641                    else :   
642                        grouppquota.DateLimit = grouppquota.DateLimit[0]
643                grouppquota.PageCounter = 0
644                grouppquota.LifePageCounter = 0
645                usernamesfilter = "".join(["(pykotaUserName=%s)" % member.Name for member in self.getGroupMembers(group)])
646                if usernamesfilter :
647                    usernamesfilter = "(|%s)" % usernamesfilter
648                result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)%s)" % (printer.Name, usernamesfilter), ["pykotaPageCounter", "pykotaLifePageCounter"], base=self.info["userquotabase"])
649                if result :
650                    for userpquota in result :   
651                        grouppquota.PageCounter += int(userpquota[1].get("pykotaPageCounter", [0])[0] or 0)
652                        grouppquota.LifePageCounter += int(userpquota[1].get("pykotaLifePageCounter", [0])[0] or 0)
653                grouppquota.Exists = 1
654        return grouppquota
655       
656    def getPrinterLastJobFromBackend(self, printer) :       
657        """Extracts a printer's last job information."""
658        lastjob = StorageLastJob(self, printer)
659        result = self.doSearch("(&(objectClass=pykotaLastjob)(|(pykotaPrinterName=%s)(%s=%s)))" % (printer.Name, self.info["printerrdn"], printer.Name), ["pykotaLastJobIdent"], base=self.info["lastjobbase"])
660        if result :
661            lastjob.lastjobident = result[0][0]
662            lastjobident = result[0][1]["pykotaLastJobIdent"][0]
663            result = None
664            try :
665                result = self.doSearch("objectClass=pykotaJob", ["pykotaJobSizeBytes", "pykotaHostName", "pykotaUserName", "pykotaJobId", "pykotaPrinterPageCounter", "pykotaJobSize", "pykotaAction", "pykotaJobPrice", "pykotaFileName", "pykotaTitle", "pykotaCopies", "pykotaOptions", "createTimestamp"], base="cn=%s,%s" % (lastjobident, self.info["jobbase"]), scope=ldap.SCOPE_BASE)
666            except PyKotaStorageError :   
667                pass # Last job entry exists, but job probably doesn't exist anymore.
668            if result :
669                fields = result[0][1]
670                lastjob.ident = result[0][0]
671                lastjob.JobId = fields.get("pykotaJobId")[0]
672                lastjob.UserName = fields.get("pykotaUserName")[0]
673                lastjob.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
674                try :
675                    lastjob.JobSize = int(fields.get("pykotaJobSize", [0])[0])
676                except ValueError :   
677                    lastjob.JobSize = None
678                try :   
679                    lastjob.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
680                except ValueError :   
681                    lastjob.JobPrice = None
682                lastjob.JobAction = fields.get("pykotaAction", [""])[0]
683                lastjob.JobFileName = fields.get("pykotaFileName", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
684                lastjob.JobTitle = fields.get("pykotaTitle", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
685                lastjob.JobCopies = int(fields.get("pykotaCopies", [0])[0])
686                lastjob.JobOptions = fields.get("pykotaOptions", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
687                lastjob.JobHostName = fields.get("pykotaHostName", [""])[0]
688                lastjob.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
689                date = fields.get("createTimestamp", ["19700101000000"])[0]
690                year = int(date[:4])
691                month = int(date[4:6])
692                day = int(date[6:8])
693                hour = int(date[8:10])
694                minute = int(date[10:12])
695                second = int(date[12:14])
696                lastjob.JobDate = "%04i-%02i-%02i %02i:%02i:%02i" % (year, month, day, hour, minute, second)
697                lastjob.Exists = 1
698        return lastjob
699       
700    def getGroupMembersFromBackend(self, group) :       
701        """Returns the group's members list."""
702        groupmembers = []
703        result = self.doSearch("(&(objectClass=pykotaGroup)(|(pykotaGroupName=%s)(%s=%s)))" % (group.Name, self.info["grouprdn"], group.Name), [self.info["groupmembers"]], base=self.info["groupbase"])
704        if result :
705            for username in result[0][1].get(self.info["groupmembers"], []) :
706                groupmembers.append(self.getUser(username))
707        return groupmembers       
708       
709    def getUserGroupsFromBackend(self, user) :       
710        """Returns the user's groups list."""
711        groups = []
712        result = self.doSearch("(&(objectClass=pykotaGroup)(%s=%s))" % (self.info["groupmembers"], user.Name), [self.info["grouprdn"], "pykotaGroupName", "pykotaLimitBy"], base=self.info["groupbase"])
713        if result :
714            for (groupid, fields) in result :
715                groupname = (fields.get("pykotaGroupName", [None]) or fields.get(self.info["grouprdn"], [None]))[0]
716                group = self.getFromCache("GROUPS", groupname)
717                if group is None :
718                    group = StorageGroup(self, groupname)
719                    group.ident = groupid
720                    group.LimitBy = fields.get("pykotaLimitBy")
721                    if group.LimitBy is not None :
722                        group.LimitBy = group.LimitBy[0]
723                    group.AccountBalance = 0.0
724                    group.LifeTimePaid = 0.0
725                    for member in self.getGroupMembers(group) :
726                        if member.Exists :
727                            group.AccountBalance += member.AccountBalance
728                            group.LifeTimePaid += member.LifeTimePaid
729                    group.Exists = 1
730                    self.cacheEntry("GROUPS", group.Name, group)
731                groups.append(group)
732        return groups       
733       
734    def getParentPrintersFromBackend(self, printer) :   
735        """Get all the printer groups this printer is a member of."""
736        pgroups = []
737        result = self.doSearch("(&(objectClass=pykotaPrinter)(uniqueMember=%s))" % printer.ident, ["pykotaPrinterName"], base=self.info["printerbase"])
738        if result :
739            for (printerid, fields) in result :
740                if printerid != printer.ident : # In case of integrity violation.
741                    parentprinter = self.getPrinter(fields.get("pykotaPrinterName")[0])
742                    if parentprinter.Exists :
743                        pgroups.append(parentprinter)
744        return pgroups
745       
746    def getMatchingPrinters(self, printerpattern) :
747        """Returns the list of all printers for which name matches a certain pattern."""
748        printers = []
749        # see comment at the same place in pgstorage.py
750        result = self.doSearch("(&(objectClass=pykotaPrinter)(|%s))" % "".join(["(pykotaPrinterName=%s)(%s=%s)" % (pname, self.info["printerrdn"], pname) for pname in printerpattern.split(",")]), ["pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerJob", "uniqueMember", "description"], base=self.info["printerbase"])
751        if result :
752            for (printerid, fields) in result :
753                printername = fields.get("pykotaPrinterName", [""])[0] or fields.get(self.info["printerrdn"], [""])[0]
754                printer = StoragePrinter(self, printername)
755                printer.ident = printerid
756                printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0] or 0.0)
757                printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0] or 0.0)
758                printer.uniqueMember = fields.get("uniqueMember", [])
759                printer.Description = fields.get("description", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
760                printer.Exists = 1
761                printers.append(printer)
762                self.cacheEntry("PRINTERS", printer.Name, printer)
763        return printers       
764       
765    def getPrinterUsersAndQuotas(self, printer, names=["*"]) :       
766        """Returns the list of users who uses a given printer, along with their quotas."""
767        usersandquotas = []
768        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % (printer.Name, "".join(["(pykotaUserName=%s)" % uname for uname in names])), ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit"], base=self.info["userquotabase"])
769        if result :
770            for (userquotaid, fields) in result :
771                user = self.getUser(fields.get("pykotaUserName")[0])
772                userpquota = StorageUserPQuota(self, user, printer)
773                userpquota.ident = userquotaid
774                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0] or 0)
775                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0] or 0)
776                userpquota.SoftLimit = fields.get("pykotaSoftLimit")
777                if userpquota.SoftLimit is not None :
778                    if userpquota.SoftLimit[0].upper() == "NONE" :
779                        userpquota.SoftLimit = None
780                    else :   
781                        userpquota.SoftLimit = int(userpquota.SoftLimit[0])
782                userpquota.HardLimit = fields.get("pykotaHardLimit")
783                if userpquota.HardLimit is not None :
784                    if userpquota.HardLimit[0].upper() == "NONE" :
785                        userpquota.HardLimit = None
786                    elif userpquota.HardLimit is not None :   
787                        userpquota.HardLimit = int(userpquota.HardLimit[0])
788                userpquota.DateLimit = fields.get("pykotaDateLimit")
789                if userpquota.DateLimit is not None :
790                    if userpquota.DateLimit[0].upper() == "NONE" : 
791                        userpquota.DateLimit = None
792                    else :   
793                        userpquota.DateLimit = userpquota.DateLimit[0]
794                userpquota.Exists = 1
795                usersandquotas.append((user, userpquota))
796                self.cacheEntry("USERPQUOTAS", "%s@%s" % (user.Name, printer.Name), userpquota)
797        usersandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
798        return usersandquotas
799               
800    def getPrinterGroupsAndQuotas(self, printer, names=["*"]) :       
801        """Returns the list of groups which uses a given printer, along with their quotas."""
802        groupsandquotas = []
803        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s)(|%s))" % (printer.Name, "".join(["(pykotaGroupName=%s)" % gname for gname in names])), ["pykotaGroupName"], base=self.info["groupquotabase"])
804        if result :
805            for (groupquotaid, fields) in result :
806                group = self.getGroup(fields.get("pykotaGroupName")[0])
807                grouppquota = self.getGroupPQuota(group, printer)
808                groupsandquotas.append((group, grouppquota))
809        groupsandquotas.sort(lambda x, y : cmp(x[0].Name, y[0].Name))           
810        return groupsandquotas
811       
812    def addPrinter(self, printername) :       
813        """Adds a printer to the quota storage, returns it."""
814        fields = { self.info["printerrdn"] : printername,
815                   "objectClass" : ["pykotaObject", "pykotaPrinter"],
816                   "cn" : printername,
817                   "pykotaPrinterName" : printername,
818                   "pykotaPricePerPage" : "0.0",
819                   "pykotaPricePerJob" : "0.0",
820                 } 
821        dn = "%s=%s,%s" % (self.info["printerrdn"], printername, self.info["printerbase"])
822        self.doAdd(dn, fields)
823        return self.getPrinter(printername)
824       
825    def addUser(self, user) :       
826        """Adds a user to the quota storage, returns it."""
827        newfields = {
828                       "pykotaUserName" : user.Name,
829                       "pykotaLimitBy" : (user.LimitBy or "quota"),
830                    }   
831                       
832        if user.Email :
833            newfields.update({self.info["usermail"]: user.Email})
834        mustadd = 1
835        if self.info["newuser"].lower() != 'below' :
836            try :
837                (where, action) = [s.strip() for s in self.info["newuser"].split(",")]
838            except ValueError :
839                (where, action) = (self.info["newuser"].strip(), "fail")
840            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % (where, self.info["userrdn"], user.Name), None, base=self.info["userbase"])
841            if result :
842                (dn, fields) = result[0]
843                fields["objectClass"].extend(["pykotaAccount", "pykotaAccountBalance"])
844                fields.update(newfields)
845                fields.update({ "pykotaBalance" : str(user.AccountBalance or 0.0),
846                                "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), })   
847                self.doModify(dn, fields)
848                mustadd = 0
849            else :
850                message = _("Unable to find an existing objectClass %s entry with %s=%s to attach pykotaAccount objectClass") % (where, self.info["userrdn"], user.Name)
851                if action.lower() == "warn" :   
852                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
853                else : # 'fail' or incorrect setting
854                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
855               
856        if mustadd :
857            if self.info["userbase"] == self.info["balancebase"] :           
858                fields = { self.info["userrdn"] : user.Name,
859                           "objectClass" : ["pykotaObject", "pykotaAccount", "pykotaAccountBalance"],
860                           "cn" : user.Name,
861                           "pykotaBalance" : str(user.AccountBalance or 0.0),
862                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
863                         } 
864            else :             
865                fields = { self.info["userrdn"] : user.Name,
866                           "objectClass" : ["pykotaObject", "pykotaAccount"],
867                           "cn" : user.Name,
868                         } 
869            fields.update(newfields)         
870            dn = "%s=%s,%s" % (self.info["userrdn"], user.Name, self.info["userbase"])
871            self.doAdd(dn, fields)
872            if self.info["userbase"] != self.info["balancebase"] :           
873                fields = { self.info["balancerdn"] : user.Name,
874                           "objectClass" : ["pykotaObject", "pykotaAccountBalance"],
875                           "cn" : user.Name,
876                           "pykotaBalance" : str(user.AccountBalance or 0.0),
877                           "pykotaLifeTimePaid" : str(user.LifeTimePaid or 0.0), 
878                         } 
879                dn = "%s=%s,%s" % (self.info["balancerdn"], user.Name, self.info["balancebase"])
880                self.doAdd(dn, fields)
881           
882        return self.getUser(user.Name)
883       
884    def addGroup(self, group) :       
885        """Adds a group to the quota storage, returns it."""
886        newfields = { 
887                      "pykotaGroupName" : group.Name,
888                      "pykotaLimitBY" : (group.LimitBy or "quota"),
889                    } 
890        mustadd = 1
891        if self.info["newgroup"].lower() != 'below' :
892            try :
893                (where, action) = [s.strip() for s in self.info["newgroup"].split(",")]
894            except ValueError :
895                (where, action) = (self.info["newgroup"].strip(), "fail")
896            result = self.doSearch("(&(objectClass=%s)(%s=%s))" % (where, self.info["grouprdn"], group.Name), None, base=self.info["groupbase"])
897            if result :
898                (dn, fields) = result[0]
899                fields["objectClass"].extend(["pykotaGroup"])
900                fields.update(newfields)
901                self.doModify(dn, fields)
902                mustadd = 0
903            else :
904                message = _("Unable to find an existing entry to attach pykotaGroup objectclass %s") % group.Name
905                if action.lower() == "warn" :   
906                    self.tool.printInfo("%s. A new entry will be created instead." % message, "warn")
907                else : # 'fail' or incorrect setting
908                    raise PyKotaStorageError, "%s. Action aborted. Please check your configuration." % message
909               
910        if mustadd :
911            fields = { self.info["grouprdn"] : group.Name,
912                       "objectClass" : ["pykotaObject", "pykotaGroup"],
913                       "cn" : group.Name,
914                     } 
915            fields.update(newfields)         
916            dn = "%s=%s,%s" % (self.info["grouprdn"], group.Name, self.info["groupbase"])
917            self.doAdd(dn, fields)
918        return self.getGroup(group.Name)
919       
920    def addUserToGroup(self, user, group) :   
921        """Adds an user to a group."""
922        if user.Name not in [u.Name for u in self.getGroupMembers(group)] :
923            result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
924            if result :
925                fields = result[0][1]
926                if not fields.has_key(self.info["groupmembers"]) :
927                    fields[self.info["groupmembers"]] = []
928                fields[self.info["groupmembers"]].append(user.Name)
929                self.doModify(group.ident, fields)
930                group.Members.append(user)
931               
932    def addUserPQuota(self, user, printer) :
933        """Initializes a user print quota on a printer."""
934        uuid = self.genUUID()
935        fields = { "cn" : uuid,
936                   "objectClass" : ["pykotaObject", "pykotaUserPQuota"],
937                   "pykotaUserName" : user.Name,
938                   "pykotaPrinterName" : printer.Name,
939                   "pykotaDateLimit" : "None",
940                   "pykotaPageCounter" : "0",
941                   "pykotaLifePageCounter" : "0",
942                 } 
943        dn = "cn=%s,%s" % (uuid, self.info["userquotabase"])
944        self.doAdd(dn, fields)
945        return self.getUserPQuota(user, printer)
946       
947    def addGroupPQuota(self, group, printer) :
948        """Initializes a group print quota on a printer."""
949        uuid = self.genUUID()
950        fields = { "cn" : uuid,
951                   "objectClass" : ["pykotaObject", "pykotaGroupPQuota"],
952                   "pykotaGroupName" : group.Name,
953                   "pykotaPrinterName" : printer.Name,
954                   "pykotaDateLimit" : "None",
955                 } 
956        dn = "cn=%s,%s" % (uuid, self.info["groupquotabase"])
957        self.doAdd(dn, fields)
958        return self.getGroupPQuota(group, printer)
959       
960    def writePrinterPrices(self, printer) :   
961        """Write the printer's prices back into the storage."""
962        fields = {
963                   "pykotaPricePerPage" : str(printer.PricePerPage),
964                   "pykotaPricePerJob" : str(printer.PricePerJob),
965                 }
966        self.doModify(printer.ident, fields)
967       
968    def writePrinterDescription(self, printer) :   
969        """Write the printer's description back into the storage."""
970        fields = {
971                   "description" : str(printer.Description).decode(self.tool.getCharset()).encode("UTF-8"), 
972                 }
973        self.doModify(printer.ident, fields)
974       
975    def writeUserLimitBy(self, user, limitby) :   
976        """Sets the user's limiting factor."""
977        fields = {
978                   "pykotaLimitBy" : limitby,
979                 }
980        self.doModify(user.ident, fields)         
981       
982    def writeGroupLimitBy(self, group, limitby) :   
983        """Sets the group's limiting factor."""
984        fields = {
985                   "pykotaLimitBy" : limitby,
986                 }
987        self.doModify(group.ident, fields)         
988       
989    def writeUserPQuotaDateLimit(self, userpquota, datelimit) :   
990        """Sets the date limit permanently for a user print quota."""
991        fields = {
992                   "pykotaDateLimit" : datelimit,
993                 }
994        return self.doModify(userpquota.ident, fields)
995           
996    def writeGroupPQuotaDateLimit(self, grouppquota, datelimit) :   
997        """Sets the date limit permanently for a group print quota."""
998        fields = {
999                   "pykotaDateLimit" : datelimit,
1000                 }
1001        return self.doModify(grouppquota.ident, fields)
1002       
1003    def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :   
1004        """Increase page counters for a user print quota."""
1005        fields = {
1006                   "pykotaPageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1007                   "pykotaLifePageCounter" : { "operator" : "+", "value" : nbpages, "convert" : int },
1008                 }
1009        return self.doModify(userpquota.ident, fields)         
1010       
1011    def writeUserPQuotaPagesCounters(self, userpquota, newpagecounter, newlifepagecounter) :   
1012        """Sets the new page counters permanently for a user print quota."""
1013        fields = {
1014                   "pykotaPageCounter" : str(newpagecounter),
1015                   "pykotaLifePageCounter" : str(newlifepagecounter),
1016                 } 
1017        return self.doModify(userpquota.ident, fields)         
1018       
1019    def decreaseUserAccountBalance(self, user, amount) :   
1020        """Decreases user's account balance from an amount."""
1021        fields = {
1022                   "pykotaBalance" : { "operator" : "-", "value" : amount, "convert" : float },
1023                 }
1024        return self.doModify(user.idbalance, fields, flushcache=1)         
1025       
1026    def writeUserAccountBalance(self, user, newbalance, newlifetimepaid=None) :   
1027        """Sets the new account balance and eventually new lifetime paid."""
1028        fields = {
1029                   "pykotaBalance" : str(newbalance),
1030                 }
1031        if newlifetimepaid is not None :
1032            fields.update({ "pykotaLifeTimePaid" : str(newlifetimepaid) })
1033        return self.doModify(user.idbalance, fields)         
1034           
1035    def writeNewPayment(self, user, amount) :       
1036        """Adds a new payment to the payments history."""
1037        payments = []
1038        for payment in user.Payments :
1039            payments.append("%s # %s" % (payment[0], str(payment[1])))
1040        payments.append("%s # %s" % (str(DateTime.now()), str(amount)))
1041        fields = {
1042                   "pykotaPayments" : payments,
1043                 }
1044        return self.doModify(user.idbalance, fields)         
1045       
1046    def writeLastJobSize(self, lastjob, jobsize, jobprice) :       
1047        """Sets the last job's size permanently."""
1048        fields = {
1049                   "pykotaJobSize" : str(jobsize),
1050                   "pykotaJobPrice" : str(jobprice),
1051                 }
1052        self.doModify(lastjob.ident, fields)         
1053       
1054    def writeJobNew(self, printer, user, jobid, pagecounter, action, jobsize=None, jobprice=None, filename=None, title=None, copies=None, options=None, clienthost=None, jobsizebytes=None) :
1055        """Adds a job in a printer's history."""
1056        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1057            uuid = self.genUUID()
1058            dn = "cn=%s,%s" % (uuid, self.info["jobbase"])
1059        else :   
1060            uuid = printer.LastJob.ident[3:].split(",")[0]
1061            dn = printer.LastJob.ident
1062        fields = {
1063                   "objectClass" : ["pykotaObject", "pykotaJob"],
1064                   "cn" : uuid,
1065                   "pykotaUserName" : user.Name,
1066                   "pykotaPrinterName" : printer.Name,
1067                   "pykotaJobId" : jobid,
1068                   "pykotaPrinterPageCounter" : str(pagecounter),
1069                   "pykotaAction" : action,
1070                   "pykotaFileName" : ((filename is None) and "None") or filename.decode(self.tool.getCharset()).encode("UTF-8"), 
1071                   "pykotaTitle" : ((title is None) and "None") or title.decode(self.tool.getCharset()).encode("UTF-8"), 
1072                   "pykotaCopies" : str(copies), 
1073                   "pykotaOptions" : ((options is None) and "None") or options.decode(self.tool.getCharset()).encode("UTF-8"), 
1074                   "pykotaHostName" : str(clienthost), 
1075                   "pykotaJobSizeBytes" : str(jobsizebytes),
1076                 }
1077        if (not self.disablehistory) or (not printer.LastJob.Exists) :
1078            if jobsize is not None :         
1079                fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1080            self.doAdd(dn, fields)
1081        else :   
1082            # here we explicitly want to reset jobsize to 'None' if needed
1083            fields.update({ "pykotaJobSize" : str(jobsize), "pykotaJobPrice" : str(jobprice) })
1084            self.doModify(dn, fields)
1085           
1086        if printer.LastJob.Exists :
1087            fields = {
1088                       "pykotaLastJobIdent" : uuid,
1089                     }
1090            self.doModify(printer.LastJob.lastjobident, fields)         
1091        else :   
1092            lastjuuid = self.genUUID()
1093            lastjdn = "cn=%s,%s" % (lastjuuid, self.info["lastjobbase"])
1094            fields = {
1095                       "objectClass" : ["pykotaObject", "pykotaLastJob"],
1096                       "cn" : lastjuuid,
1097                       "pykotaPrinterName" : printer.Name,
1098                       "pykotaLastJobIdent" : uuid,
1099                     } 
1100            self.doAdd(lastjdn, fields)         
1101           
1102    def writeUserPQuotaLimits(self, userpquota, softlimit, hardlimit) :
1103        """Sets soft and hard limits for a user quota."""
1104        fields = { 
1105                   "pykotaSoftLimit" : str(softlimit),
1106                   "pykotaHardLimit" : str(hardlimit),
1107                   "pykotaDateLimit" : "None",
1108                 }
1109        self.doModify(userpquota.ident, fields)
1110       
1111    def writeGroupPQuotaLimits(self, grouppquota, softlimit, hardlimit) :
1112        """Sets soft and hard limits for a group quota on a specific printer."""
1113        fields = { 
1114                   "pykotaSoftLimit" : str(softlimit),
1115                   "pykotaHardLimit" : str(hardlimit),
1116                   "pykotaDateLimit" : "None",
1117                 }
1118        self.doModify(grouppquota.ident, fields)
1119           
1120    def writePrinterToGroup(self, pgroup, printer) :
1121        """Puts a printer into a printer group."""
1122        if printer.ident not in pgroup.uniqueMember :
1123            pgroup.uniqueMember.append(printer.ident)
1124            fields = {
1125                       "uniqueMember" : pgroup.uniqueMember
1126                     } 
1127            self.doModify(pgroup.ident, fields)         
1128           
1129    def removePrinterFromGroup(self, pgroup, printer) :
1130        """Removes a printer from a printer group."""
1131        try :
1132            pgroup.uniqueMember.remove(printer.ident)
1133        except ValueError :   
1134            pass
1135        else :   
1136            fields = {
1137                       "uniqueMember" : pgroup.uniqueMember,
1138                     } 
1139            self.doModify(pgroup.ident, fields)         
1140           
1141    def retrieveHistory(self, user=None, printer=None, datelimit=None, hostname=None, limit=100) :   
1142        """Retrieves all print jobs for user on printer (or all) before date, limited to first 100 results."""
1143        precond = "(objectClass=pykotaJob)"
1144        where = []
1145        if (user is not None) and user.Exists :
1146            where.append("(pykotaUserName=%s)" % user.Name)
1147        if (printer is not None) and printer.Exists :
1148            where.append("(pykotaPrinterName=%s)" % printer.Name)
1149        if hostname is not None :
1150            where.append("(pykotaHostName=%s)" % hostname)
1151        if where :   
1152            where = "(&%s)" % "".join([precond] + where)
1153        else :   
1154            where = precond
1155        jobs = []   
1156        result = self.doSearch(where, fields=["pykotaJobSizeBytes", "pykotaHostName", "pykotaUserName", "pykotaPrinterName", "pykotaJobId", "pykotaPrinterPageCounter", "pykotaAction", "pykotaJobSize", "pykotaJobPrice", "pykotaFileName", "pykotaTitle", "pykotaCopies", "pykotaOptions", "createTimestamp"], base=self.info["jobbase"])
1157        if result :
1158            for (ident, fields) in result :
1159                job = StorageJob(self)
1160                job.ident = ident
1161                job.JobId = fields.get("pykotaJobId")[0]
1162                job.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
1163                try :
1164                    job.JobSize = int(fields.get("pykotaJobSize", [0])[0])
1165                except ValueError :   
1166                    job.JobSize = None
1167                try :   
1168                    job.JobPrice = float(fields.get("pykotaJobPrice", [0.0])[0])
1169                except ValueError :
1170                    job.JobPrice = None
1171                job.JobAction = fields.get("pykotaAction", [""])[0]
1172                job.JobFileName = fields.get("pykotaFileName", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
1173                job.JobTitle = fields.get("pykotaTitle", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
1174                job.JobCopies = int(fields.get("pykotaCopies", [0])[0])
1175                job.JobOptions = fields.get("pykotaOptions", [""])[0].decode("UTF-8").encode(self.tool.getCharset()) 
1176                job.JobHostName = fields.get("pykotaHostName", [""])[0]
1177                job.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
1178                date = fields.get("createTimestamp", ["19700101000000"])[0]
1179                year = int(date[:4])
1180                month = int(date[4:6])
1181                day = int(date[6:8])
1182                hour = int(date[8:10])
1183                minute = int(date[10:12])
1184                second = int(date[12:14])
1185                job.JobDate = "%04i-%02i-%02i %02i:%02i:%02i" % (year, month, day, hour, minute, second)
1186                if (datelimit is None) or (job.JobDate <= datelimit) :
1187                    job.UserName = fields.get("pykotaUserName")[0]
1188                    job.PrinterName = fields.get("pykotaPrinterName")[0]
1189                    job.Exists = 1
1190                    jobs.append(job)
1191            jobs.sort(lambda x,y : cmp(y.JobDate, x.JobDate))       
1192            if limit :   
1193                jobs = jobs[:int(limit)]
1194        return jobs
1195       
1196    def deleteUser(self, user) :   
1197        """Completely deletes an user from the Quota Storage."""
1198        todelete = []   
1199        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaUserName=%s))" % user.Name, base=self.info["jobbase"])
1200        for (ident, fields) in result :
1201            todelete.append(ident)
1202           
1203        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s))" % user.Name, ["pykotaPrinterName", "pykotaUserName"], base=self.info["userquotabase"])
1204        for (ident, fields) in result :
1205            # ensure the user print quota entry will be deleted
1206            todelete.append(ident)
1207           
1208            # if last job of current printer was printed by the user
1209            # to delete, we also need to delete the printer's last job entry.
1210            printername = fields["pykotaPrinterName"][0]
1211            printer = self.getPrinter(printername)
1212            if printer.LastJob.UserName == user.Name :
1213                todelete.append(printer.LastJob.lastjobident)
1214           
1215        for ident in todelete :   
1216            self.doDelete(ident)
1217           
1218        result = self.doSearch("objectClass=pykotaAccount", None, base=user.ident, scope=ldap.SCOPE_BASE)   
1219        if result :
1220            fields = result[0][1]
1221            for k in fields.keys() :
1222                if k.startswith("pykota") :
1223                    del fields[k]
1224                elif k.lower() == "objectclass" :   
1225                    todelete = []
1226                    for i in range(len(fields[k])) :
1227                        if fields[k][i].startswith("pykota") : 
1228                            todelete.append(i)
1229                    todelete.sort()       
1230                    todelete.reverse()
1231                    for i in todelete :
1232                        del fields[k][i]
1233            if fields.get("objectClass") or fields.get("objectclass") :
1234                self.doModify(user.ident, fields, ignoreold=0)       
1235            else :   
1236                self.doDelete(user.ident)
1237        result = self.doSearch("(&(objectClass=pykotaAccountBalance)(pykotaUserName=%s))" % user.Name, ["pykotaUserName"], base=self.info["balancebase"])
1238        for (ident, fields) in result :
1239            self.doDelete(ident)
1240       
1241    def deleteGroup(self, group) :   
1242        """Completely deletes a group from the Quota Storage."""
1243        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaGroupName=%s))" % group.Name, ["pykotaGroupName"], base=self.info["groupquotabase"])
1244        for (ident, fields) in result :
1245            self.doDelete(ident)
1246        result = self.doSearch("objectClass=pykotaGroup", None, base=group.ident, scope=ldap.SCOPE_BASE)   
1247        if result :
1248            fields = result[0][1]
1249            for k in fields.keys() :
1250                if k.startswith("pykota") :
1251                    del fields[k]
1252                elif k.lower() == "objectclass" :   
1253                    todelete = []
1254                    for i in range(len(fields[k])) :
1255                        if fields[k][i].startswith("pykota") : 
1256                            todelete.append(i)
1257                    todelete.sort()       
1258                    todelete.reverse()
1259                    for i in todelete :
1260                        del fields[k][i]
1261            if fields.get("objectClass") or fields.get("objectclass") :
1262                self.doModify(group.ident, fields, ignoreold=0)       
1263            else :   
1264                self.doDelete(group.ident)
1265               
1266    def deletePrinter(self, printer) :   
1267        """Completely deletes an user from the Quota Storage."""
1268        result = self.doSearch("(&(objectClass=pykotaLastJob)(pykotaPrinterName=%s))" % printer.Name, base=self.info["lastjobbase"])
1269        for (ident, fields) in result :
1270            self.doDelete(ident)
1271        result = self.doSearch("(&(objectClass=pykotaJob)(pykotaPrinterName=%s))" % printer.Name, base=self.info["jobbase"])
1272        for (ident, fields) in result :
1273            self.doDelete(ident)
1274        result = self.doSearch("(&(objectClass=pykotaGroupPQuota)(pykotaPrinterName=%s))" % printer.Name, base=self.info["groupquotabase"])
1275        for (ident, fields) in result :
1276            self.doDelete(ident)
1277        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s))" % printer.Name, base=self.info["userquotabase"])
1278        for (ident, fields) in result :
1279            self.doDelete(ident)
1280        for parent in self.getParentPrinters(printer) : 
1281            try :
1282                parent.uniqueMember.remove(printer.ident)
1283            except ValueError :   
1284                pass
1285            else :   
1286                fields = {
1287                           "uniqueMember" : parent.uniqueMember,
1288                         } 
1289                self.doModify(parent.ident, fields)         
1290        self.doDelete(printer.ident)   
1291       
1292    def extractPrinters(self) :
1293        """Extracts all printer records."""
1294        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames()] if p.Exists]
1295        if entries :
1296            result = [ ("dn", "pykotaPrinterName", "pykotaPricePerPage", "pykotaPricePerPage", "description") ]
1297            for entry in entries :
1298                result.append((entry.ident, entry.Name, entry.PricePerPage, entry.PricePerJob, entry.Description))
1299            return result 
1300       
1301    def extractUsers(self) :
1302        """Extracts all user records."""
1303        entries = [u for u in [self.getUser(name) for name in self.getAllUsersNames()] if u.Exists]
1304        if entries :
1305            result = [ ("dn", "pykotaUserName", self.info["usermail"], "pykotaBalance", "pykotaLifeTimePaid", "pykotaLimitBy") ]
1306            for entry in entries :
1307                result.append((entry.ident, entry.Name, entry.Email, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy))
1308            return result 
1309       
1310    def extractGroups(self) :
1311        """Extracts all group records."""
1312        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames()] if g.Exists]
1313        if entries :
1314            result = [ ("dn", "pykotaGroupName", "pykotaBalance", "pykotaLifeTimePaid", "pykotaLimitBy") ]
1315            for entry in entries :
1316                result.append((entry.ident, entry.Name, entry.AccountBalance, entry.LifeTimePaid, entry.LimitBy))
1317            return result 
1318       
1319    def extractPayments(self) :
1320        """Extracts all payment records."""
1321        pass
1322       
1323    def extractUpquotas(self) :
1324        """Extracts all userpquota records."""
1325        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames()] if p.Exists]
1326        if entries :
1327            pass   
1328       
1329    def extractGpquotas(self) :
1330        """Extracts all grouppquota records."""
1331        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames()] if p.Exists]
1332        if entries :
1333            pass
1334       
1335    def extractUmembers(self) :
1336        """Extracts all user groups members."""
1337        entries = [g for g in [self.getGroup(name) for name in self.getAllGroupsNames()] if g.Exists]
1338        if entries :
1339            result = [ ("pykotaGroupName", "pykotaUserName", "groupdn", "userdn") ]
1340            for entry in entries :
1341                for member in entry.Members :
1342                    result.append((entry.Name, member.Name, entry.ident, member.ident))
1343            return result       
1344               
1345    def extractPmembers(self) :
1346        """Extracts all printer groups members."""
1347        entries = [p for p in [self.getPrinter(name) for name in self.getAllPrintersNames()] if p.Exists]
1348        if entries :
1349            result = [ ("pykotaPGroupName", "pykotaPrinterName", "pgroupdn", "printerdn") ]
1350            for entry in entries :
1351                for parent in self.getParentPrinters(entry) :
1352                    result.append((parent.Name, entry.Name, parent.ident, entry.ident))
1353            return result       
1354       
1355    def extractHistory(self) :
1356        """Extracts all jobhistory records."""
1357        entries = self.retrieveHistory(limit=None)
1358        if entries :
1359            result = [ ("pykotaUserName", "pykotaPrinterName", "dn", "pykotaJobId", "pykotaPrinterPageCounter", "pykotaJobSize", "pykotaAction", "createTimeStamp", "pykotaFileName", "pykotaTitle", "pykotaCopies", "pykotaOptions", "pykotaJobPrice", "pykotaHostName", "pykotaJobSizeBytes") ] 
1360            for entry in entries :
1361                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)) 
1362            return result   
Note: See TracBrowser for help on using the browser.