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

Revision 3413, 15.0 kB (checked in by jerome, 16 years ago)

Removed unnecessary spaces at EOL.

  • 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 = { u"history" : N_("History"),
50                       u"users" : N_("Users"),
51                       u"groups" : N_("Groups"),
52                       u"printers" : N_("Printers"),
53                       u"upquotas" : N_("Users Print Quotas"),
54                       u"gpquotas" : N_("Users Groups Print Quotas"),
55                       u"payments" : N_("History of Payments"),
56                       u"pmembers" : N_("Printers Groups Membership"),
57                       u"umembers" : N_("Users Groups Membership"),
58                       u"billingcodes" : N_("Billing Codes"),
59                       u"all": N_("All"),
60                     }
61    validformats = { u"csv" : N_("Comma Separated Values"),
62                     u"ssv" : N_("Semicolon Separated Values"),
63                     u"tsv" : N_("Tabulation Separated Values"),
64                     u"xml" : N_("eXtensible Markup Language"),
65                     u"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=True) :
78        """Print Quota Data Dumper."""
79        self.adminOnly(restricted)
80
81        datatype = options.data
82        if datatype not in self.validdatatypes.keys() :
83            raise PyKotaCommandLineError, _("Invalid data type '%(datatype)s', see help.") % locals()
84
85        orderby = options.orderby or []
86        if orderby :
87            fields = [f.strip() for f in orderby.split(",")]
88            orderby = []
89            for field in fields :
90                if field.isalpha() \
91                   or ((field[0] in ("+", "-")) and field[1:].isalpha()) :
92                    orderby.append(field)
93                else :
94                    logerr(_("Skipping invalid ordering statement '%(field)s'") % locals())
95
96        extractonly = {}
97        if datatype == u"all" :
98            if (options.format != u"xml") or options.sum or arguments :
99                self.printInfo(_("Dumping all PyKota's datas forces format to XML, and disables --sum and filters."), "warn")
100            options.format = u"xml"
101            options.sum = None
102        else :
103            for filterexp in arguments :
104                if filterexp.strip() :
105                    try :
106                        (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
107                        filterkey = filterkey.encode("ASCII", "replace").lower()
108                        if filterkey not in self.validfilterkeys :
109                            raise ValueError
110                    except ValueError :
111                        raise PyKotaCommandLineError, _("Invalid filter value '%(filterexp)s', see help.") % locals()
112                    else :
113                        extractonly.update({ filterkey : filtervalue })
114
115        format = options.format
116        if (format not in self.validformats.keys()) \
117           or ((format == u"cups") \
118              and ((datatype != u"history") or options.sum)) :
119            raise PyKotaCommandLineError, _("Invalid format '%(format)s', see help.") % locals()
120
121        if (format == u"xml") and not hasJAXML :
122            raise PyKotaToolError, _("XML output is disabled because the jaxml module is not available.")
123
124        if datatype not in (u"payments", u"history") :
125            if options.sum :
126                raise PyKotaCommandLineError, _("Invalid data type '%(datatype)s' for --sum command line option, see help.") % locals()
127            if extractonly.has_key(u"start") or extractonly.has_key(u"end") :
128                self.printInfo(_("Invalid filter for the '%(datatype)s' data type.") % locals(), "warn")
129                try :
130                    del extractonly[u"start"]
131                except KeyError :
132                    pass
133                try :
134                    del extractonly[u"end"]
135                except KeyError :
136                    pass
137
138        retcode = 0
139        nbentries = 0
140        mustclose = False
141        outfname = options.output.strip().encode(sys.getfilesystemencoding())
142        if outfname == "-" :
143            self.outfile = sys.stdout
144        else :
145            self.outfile = open(outfname, "w")
146            mustclose = True
147
148        if datatype == u"all" :
149            # NB : order does matter to allow easier or faster restore
150            allentries = []
151            datatypes = [ "printers", "pmembers", "users", "groups", \
152                          "billingcodes", "umembers", "upquotas", \
153                          "gpquotas", "payments", "history" ]
154            neededdatatypes = datatypes[:]
155            for datatype in datatypes :
156                entries = getattr(self.storage, "extract%s" % datatype.title())(extractonly) # We don't care about ordering here
157                if entries :
158                    nbentries += len(entries)
159                    allentries.append(entries)
160                else :
161                    neededdatatypes.remove(datatype)
162            retcode = self.dumpXml(allentries, neededdatatypes)
163        else :
164            datatype = datatype.encode("ASCII")
165            format = format.encode("ASCII")
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                    self.printInfo("%s : %s" % (_("PyKota data dumper failed : I/O error"), msg), "error")
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 = "%02i/%s/%04i:%02i:%02i:%02i %+03i%02i" % (jobdate.day,
294                                                                 months[jobdate.month - 1],
295                                                                 jobdate.year,
296                                                                 jobdate.hour,
297                                                                 jobdate.minute,
298                                                                 jobdate.second,
299                                                                 gmtoffset.hour,
300                                                                 gmtoffset.minute)
301            jobsize = entry[fields["jobsize"]] or 0
302            copies = entry[fields["copies"]] or 1
303            hostname = entry[fields["hostname"]] or ""
304            billingcode = entry[fields["billingcode"]] or "-"
305            for pagenum in range(1, jobsize+1) :
306                line = "%s %s %s [%s] %s %s %s %s" % (printername, username, jobid, jobdate, pagenum, copies, billingcode, hostname)
307                self.outfile.write("%s\n" % line.encode(self.charset,
308                                                        "replace"))
309        return 0
310
311    def dumpXml(self, allentries, datatypes) :
312        """Dumps datas as XML."""
313        x = jaxml.XML_document(encoding="UTF-8")
314        x.pykota(version=version.__version__, author=version.__author__)
315        for (entries, datatype) in zip(allentries, datatypes) :
316            x._push()
317            x.dump(storage=self.config.getStorageBackend()["storagebackend"], type=datatype)
318            headers = entries[0]
319            for entry in entries[1:] :
320                x._push()
321                x.entry()
322                for (header, value) in zip(headers, entry) :
323                    try :
324                        strvalue = saxutils.escape(value.encode("UTF-8", \
325                                                                "replace"), \
326                                                   { "'" : "&apos;", \
327                                                     '"' : "&quot;" })
328                    except AttributeError :
329                        strvalue = str(value)
330                    # We use 'str' instead of 'unicode' below to be compatible
331                    # with older releases of PyKota.
332                    # The XML dump will contain UTF-8 encoded strings,
333                    #�not unicode strings anyway.
334                    x.attribute(strvalue, \
335                                type=type(value).__name__.replace("unicode", "str"), \
336                                name=header)
337                x._pop()
338            x._pop()
339        x._output(self.outfile)
340        return 0
Note: See TracBrowser for help on using the browser.