root / pykota / trunk / pykota / dumper.py @ 3295

Revision 3295, 15.5 kB (checked in by jerome, 16 years ago)

Made the CGI scripts work again.
Moved even more functions to the utils module.
Removed the cgifuncs module, moved (and changed) content into utils.
If no output encoding defined, use UTF-8 : when wget is used to try
the CGI scripts, it doesn't set by default the accepted charset and
language headers.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# -*- coding: UTF-8 -*-
2#
3# PyKota : Print Quotas for CUPS
4#
5# (c) 2003, 2004, 2005, 2006, 2007, 2008 Jerome Alet <alet@librelogiciel.com>
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18#
19# $Id$
20#
21#
22
23"""This module handles all the data dumping facilities for PyKota."""
24
25import sys
26import os
27import pwd
28from xml.sax import saxutils
29
30from mx import DateTime
31
32try :
33    import jaxml
34except ImportError :   
35    sys.stderr.write("The jaxml Python module is not installed. XML output is disabled.\n")
36    sys.stderr.write("Download jaxml from http://www.librelogiciel.com/software/ or from your Debian archive of choice\n")
37    hasJAXML = False
38else :   
39    hasJAXML = True
40
41from pykota.utils import *
42
43from pykota import version
44from pykota.tool import PyKotaTool
45from pykota.errors import PyKotaToolError, PyKotaCommandLineError
46
47class DumPyKota(PyKotaTool) :       
48    """A class for dumpykota."""
49    validdatatypes = { "history" : N_("History"),
50                       "users" : N_("Users"),
51                       "groups" : N_("Groups"),
52                       "printers" : N_("Printers"),
53                       "upquotas" : N_("Users Print Quotas"),
54                       "gpquotas" : N_("Users Groups Print Quotas"),
55                       "payments" : N_("History of Payments"),
56                       "pmembers" : N_("Printers Groups Membership"), 
57                       "umembers" : N_("Users Groups Membership"),
58                       "billingcodes" : N_("Billing Codes"),
59                       "all": N_("All"),
60                     }
61    validformats = { "csv" : N_("Comma Separated Values"),
62                     "ssv" : N_("Semicolon Separated Values"),
63                     "tsv" : N_("Tabulation Separated Values"),
64                     "xml" : N_("eXtensible Markup Language"),
65                     "cups" : N_("CUPS' page_log"),
66                   } 
67    validfilterkeys = [ "username",
68                        "groupname",
69                        "printername",
70                        "pgroupname",
71                        "hostname",
72                        "billingcode",
73                        "jobid", 
74                        "start",
75                        "end",
76                      ]
77    def main(self, arguments, options, restricted=1) :
78        """Print Quota Data Dumper."""
79        if restricted and not self.config.isAdmin :
80            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
81           
82        datatype = options["data"]
83        if datatype not in self.validdatatypes.keys() :
84            raise PyKotaCommandLineError, _("Invalid modifier [%s] for --data command line option, see help.") % datatype
85                   
86        orderby = options["orderby"]             
87        if orderby :
88            fields = [f.strip() for f in orderby.split(",")]
89            orderby = []
90            for field in fields :
91                if field.isalpha() \
92                   or ((field[0] in ("+", "-")) and field[1:].isalpha()) :
93                    orderby.append(field)
94                else :   
95                    self.printInfo("Skipping invalid ordering statement '%(field)s'" % locals(), "error") 
96        else :
97            orderby = []
98           
99        extractonly = {}
100        if datatype == "all" :           
101            if (options["format"] != "xml") or options["sum"] or arguments :
102                self.printInfo(_("Dumping all PyKota's datas forces format to XML, and disables --sum and filters."), "warn")
103            options["format"] = "xml"
104            options["sum"] = None
105        else :   
106            for filterexp in arguments :
107                if filterexp.strip() :
108                    try :
109                        (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
110                        filterkey = filterkey.lower()
111                        if filterkey not in self.validfilterkeys :
112                            raise ValueError               
113                    except ValueError :   
114                        raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
115                    else :   
116                        extractonly.update({ filterkey : filtervalue })
117           
118        format = options["format"]
119        if (format not in self.validformats.keys()) \
120           or ((format == "cups") \
121              and ((datatype != "history") or options["sum"])) :
122            raise PyKotaCommandLineError, _("Invalid modifier [%s] for --format command line option, see help.") % format
123           
124        if (format == "xml") and not hasJAXML :
125            raise PyKotaToolError, _("XML output is disabled because the jaxml module is not available.")
126           
127        if datatype not in ("payments", "history") : 
128            if options["sum"] : 
129                raise PyKotaCommandLineError, _("Invalid data type [%s] for --sum command line option, see help.") % datatype
130            if extractonly.has_key("start") or extractonly.has_key("end") :   
131                self.printInfo(_("Invalid filter for the %(datatype)s data type.") % locals(), "warn")
132                try :
133                    del extractonly["start"]
134                except KeyError :   
135                    pass
136                try :
137                    del extractonly["end"]
138                except KeyError :   
139                    pass
140           
141        retcode = 0
142        nbentries = 0   
143        mustclose = 0   
144        if options["output"].strip() == "-" :   
145            self.outfile = sys.stdout
146        else :   
147            self.outfile = open(options["output"], "w")
148            mustclose = 1
149           
150        if datatype == "all" :   
151            # NB : order does matter to allow easier or faster restore
152            allentries = []
153            datatypes = [ "printers", "pmembers", "users", "groups", \
154                          "billingcodes", "umembers", "upquotas", \
155                          "gpquotas", "payments", "history" ]
156            neededdatatypes = datatypes[:]             
157            for datatype in datatypes :
158                entries = getattr(self.storage, "extract%s" % datatype.title())(extractonly) # We don't care about ordering here
159                if entries :
160                    nbentries += len(entries)
161                    allentries.append(entries)
162                else :   
163                    neededdatatypes.remove(datatype)
164            retcode = self.dumpXml(allentries, neededdatatypes)
165        else :   
166            entries = getattr(self.storage, "extract%s" % datatype.title())(extractonly, orderby)
167            if entries :
168                nbentries = len(entries)
169                retcode = getattr(self, "dump%s" % format.title())([self.summarizeDatas(entries, datatype, extractonly, options["sum"])], [datatype])
170               
171        if mustclose :
172            self.outfile.close()
173            if not nbentries : 
174                os.remove(options["output"])
175           
176        return retcode
177       
178    def summarizeDatas(self, entries, datatype, extractonly, sum=0) :   
179        """Transforms the datas into a summarized view (with totals).
180       
181           If sum is false, returns the entries unchanged.
182        """   
183        if not sum :
184            return entries
185        else :   
186            headers = entries[0]
187            nbheaders = len(headers)
188            fieldnumber = {}
189            fieldname = {}
190            for i in range(nbheaders) :
191                fieldnumber[headers[i]] = i
192           
193            if datatype == "payments" :
194                totalize = [ ("amount", float) ]
195                keys = [ "username" ]
196            else : # elif datatype == "history"
197                totalize = [ ("jobsize", int), 
198                             ("jobprice", float),
199                             ("jobsizebytes", int),
200                             ("precomputedjobsize", int),
201                             ("precomputedjobprice", float),
202                           ]
203                keys = [ k for k in ("username", "printername", "hostname", "billingcode") if k in extractonly.keys() ]
204               
205            newentries = [ headers ]
206            sortedentries = entries[1:]
207            if keys :
208                # If we have several keys, we can sort only on the first one, because they
209                # will vary the same way.
210                sortedentries.sort(lambda x, y, fnum=fieldnumber[keys[0]] : cmp(x[fnum], y[fnum]))
211            totals = {}
212            for (k, t) in totalize :
213                totals[k] = { "convert" : t, "value" : 0.0 }
214            prevkeys = {}
215            for k in keys :
216                prevkeys[k] = sortedentries[0][fieldnumber[k]]
217            for entry in sortedentries :
218                curval = '-'.join([str(entry[fieldnumber[k]]) for k in keys])
219                prevval = '-'.join([str(prevkeys[k]) for k in keys])
220                if curval != prevval :
221                    summary = [ "*" ] * nbheaders
222                    for k in keys :
223                        summary[fieldnumber[k]] = prevkeys[k]
224                    for k in totals.keys() :   
225                        summary[fieldnumber[k]] = totals[k]["convert"](totals[k]["value"])
226                    newentries.append(summary)
227                    for k in totals.keys() :   
228                        totals[k]["value"] = totals[k]["convert"](entry[fieldnumber[k]])
229                else :   
230                    for k in totals.keys() :   
231                        totals[k]["value"] += totals[k]["convert"](entry[fieldnumber[k]] or 0.0)
232                for k in keys :
233                    prevkeys[k] = entry[fieldnumber[k]]
234            summary = [ "*" ] * nbheaders
235            for k in keys :
236                summary[fieldnumber[k]] = prevkeys[k]
237            for k in totals.keys() :   
238                summary[fieldnumber[k]] = totals[k]["convert"](totals[k]["value"])
239            newentries.append(summary)
240            return newentries
241           
242    def dumpWithSeparator(self, separator, allentries) :   
243        """Dumps datas with a separator."""
244        for entries in allentries :
245            for entry in entries :
246                line = []
247                for value in entry :
248                    try :
249                        strvalue = '"%s"' % value.encode(self.charset, \
250                                                         "replace").replace(separator, "\\%s" % separator).replace('"', '\\"')
251                    except AttributeError :
252                        if value is None :
253                            strvalue = '"None"' # Double quotes around None to prevent spreadsheet from failing
254                        else :   
255                            strvalue = str(value)
256                    line.append(strvalue)
257                try :
258                    self.outfile.write("%s\n" % separator.join(line))
259                except IOError, msg :   
260                    sys.stderr.write("%s : %s\n" % (_("PyKota data dumper failed : I/O error"), msg))
261                    return -1
262        return 0       
263       
264    def dumpCsv(self, allentries, dummy) :   
265        """Dumps datas with a comma as the separator."""
266        return self.dumpWithSeparator(",", allentries)
267                           
268    def dumpSsv(self, allentries, dummy) :   
269        """Dumps datas with a comma as the separator."""
270        return self.dumpWithSeparator(";", allentries)
271                           
272    def dumpTsv(self, allentries, dummy) :   
273        """Dumps datas with a comma as the separator."""
274        return self.dumpWithSeparator("\t", allentries)
275       
276    def dumpCups(self, allentries, dummy) :   
277        """Dumps history datas as CUPS' page_log format."""
278        months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
279        entries = allentries[0]
280        fieldnames = entries[0]
281        fields = {}
282        for i in range(len(fieldnames)) :
283            fields[fieldnames[i]] = i
284        sortindex = fields["jobdate"]   
285        entries = entries[1:]
286        entries.sort(lambda m, n, si=sortindex : cmp(m[si], n[si]))
287        for entry in entries :   
288            printername = entry[fields["printername"]]
289            username = entry[fields["username"]]
290            jobid = entry[fields["jobid"]]
291            jobdate = DateTime.ISO.ParseDateTime(str(entry[fields["jobdate"]])[:19])
292            gmtoffset = jobdate.gmtoffset()
293            #jobdate = "%s %+03i00" % (jobdate.strftime("%d/%b/%Y:%H:%M:%S"), gmtoffset.hour)
294            jobdate = "%02i/%s/%04i:%02i:%02i:%02i %+03i%02i" % (jobdate.day,
295                                                                 months[jobdate.month - 1],
296                                                                 jobdate.year,
297                                                                 jobdate.hour,
298                                                                 jobdate.minute,
299                                                                 jobdate.second,
300                                                                 gmtoffset.hour,
301                                                                 gmtoffset.minute)
302            jobsize = entry[fields["jobsize"]] or 0
303            copies = entry[fields["copies"]] or 1
304            hostname = entry[fields["hostname"]] or ""
305            billingcode = entry[fields["billingcode"]] or "-"
306            for pagenum in range(1, jobsize+1) :
307                self.outfile.write("%s %s %s [%s] %s %s %s %s\n" % (printername, username, jobid, jobdate, pagenum, copies, billingcode, hostname))
308        return 0       
309       
310    def dumpXml(self, allentries, datatypes) :
311        """Dumps datas as XML."""
312        x = jaxml.XML_document(encoding="UTF-8")
313        x.pykota(version=version.__version__, author=version.__author__)
314        for (entries, datatype) in zip(allentries, datatypes) :
315            x._push()
316            x.dump(storage=self.config.getStorageBackend()["storagebackend"], type=datatype)
317            headers = entries[0]
318            for entry in entries[1:] :
319                x._push()
320                x.entry()
321                for (header, value) in zip(headers, entry) :
322                    try :
323                        strvalue = saxutils.escape(value.encode("UTF-8", \
324                                                                "replace"), \
325                                                   { "'" : "&apos;", \
326                                                     '"' : "&quot;" })
327                    except AttributeError :   
328                        strvalue = str(value)
329                    # We use 'str' instead of 'unicode' below to be compatible
330                    # with older releases of PyKota.
331                    # The XML dump will contain UTF-8 encoded strings,
332                    #�not unicode strings anyway.
333                    x.attribute(strvalue, \
334                                type=type(value).__name__.replace("unicode", "str"), \
335                                name=header)
336                x._pop()   
337            x._pop()   
338        x._output(self.outfile)
339        return 0       
Note: See TracBrowser for help on using the browser.