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

Revision 1806, 67.5 kB (checked in by jalet, 20 years ago)

Now warnpykota only warns users who have already printed, to not confuse
users who have just opened their account.

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