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

Revision 3142, 14.4 kB (checked in by jerome, 17 years ago)

Make start= and end= filters also work with payments. For an unknown
reason it didn't already work (not implemented !)...
Ensures that these filters are only allowed for payments and
history, otherwise an SQL syntax error could have occured.

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