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

Revision 1969, 71.9 kB (checked in by jalet, 19 years ago)

Integrated and extended Stefan Wold's patch to store print quota entries
directly below the user or the group object with the LDAP backend

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