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

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

Database backends now convert from and to unicode instead of UTF-8.
The data dumper now expects unicode datas from the database.

  • 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 import version
42from pykota.tool import PyKotaTool, N_
43from pykota.errors import PyKotaToolError, PyKotaCommandLineError
44
45class DumPyKota(PyKotaTool) :       
46    """A class for dumpykota."""
47    validdatatypes = { "history" : N_("History"),
48                       "users" : N_("Users"),
49                       "groups" : N_("Groups"),
50                       "printers" : N_("Printers"),
51                       "upquotas" : N_("Users Print Quotas"),
52                       "gpquotas" : N_("Users Groups Print Quotas"),
53                       "payments" : N_("History of Payments"),
54                       "pmembers" : N_("Printers Groups Membership"), 
55                       "umembers" : N_("Users Groups Membership"),
56                       "billingcodes" : N_("Billing Codes"),
57                       "all": N_("All"),
58                     }
59    validformats = { "csv" : N_("Comma Separated Values"),
60                     "ssv" : N_("Semicolon Separated Values"),
61                     "tsv" : N_("Tabulation Separated Values"),
62                     "xml" : N_("eXtensible Markup Language"),
63                     "cups" : N_("CUPS' page_log"),
64                   } 
65    validfilterkeys = [ "username",
66                        "groupname",
67                        "printername",
68                        "pgroupname",
69                        "hostname",
70                        "billingcode",
71                        "jobid", 
72                        "start",
73                        "end",
74                      ]
75    def main(self, arguments, options, restricted=1) :
76        """Print Quota Data Dumper."""
77        if restricted and not self.config.isAdmin :
78            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
79           
80        datatype = options["data"]
81        if datatype not in self.validdatatypes.keys() :
82            raise PyKotaCommandLineError, _("Invalid modifier [%s] for --data command line option, see help.") % datatype
83                   
84        orderby = options["orderby"]             
85        if orderby :
86            fields = [f.strip() for f in orderby.split(",")]
87            orderby = []
88            for field in fields :
89                if field.isalpha() \
90                   or ((field[0] in ("+", "-")) and field[1:].isalpha()) :
91                    orderby.append(field)
92                else :   
93                    self.printInfo("Skipping invalid ordering statement '%(field)s'" % locals(), "error") 
94        else :
95            orderby = []
96           
97        extractonly = {}
98        if datatype == "all" :           
99            if (options["format"] != "xml") or options["sum"] or arguments :
100                self.printInfo(_("Dumping all PyKota's datas forces format to XML, and disables --sum and filters."), "warn")
101            options["format"] = "xml"
102            options["sum"] = None
103        else :   
104            for filterexp in arguments :
105                if filterexp.strip() :
106                    try :
107                        (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
108                        filterkey = filterkey.lower()
109                        if filterkey not in self.validfilterkeys :
110                            raise ValueError               
111                    except ValueError :   
112                        raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
113                    else :   
114                        extractonly.update({ filterkey : filtervalue })
115           
116        format = options["format"]
117        if (format not in self.validformats.keys()) \
118           or ((format == "cups") \
119              and ((datatype != "history") or options["sum"])) :
120            raise PyKotaCommandLineError, _("Invalid modifier [%s] for --format command line option, see help.") % format
121           
122        if (format == "xml") and not hasJAXML :
123            raise PyKotaToolError, _("XML output is disabled because the jaxml module is not available.")
124           
125        if datatype not in ("payments", "history") : 
126            if options["sum"] : 
127                raise PyKotaCommandLineError, _("Invalid data type [%s] for --sum command line option, see help.") % datatype
128            if extractonly.has_key("start") or extractonly.has_key("end") :   
129                self.printInfo(_("Invalid filter for the %(datatype)s data type.") % locals(), "warn")
130                try :
131                    del extractonly["start"]
132                except KeyError :   
133                    pass
134                try :
135                    del extractonly["end"]
136                except KeyError :   
137                    pass
138           
139        retcode = 0
140        nbentries = 0   
141        mustclose = 0   
142        if options["output"].strip() == "-" :   
143            self.outfile = sys.stdout
144        else :   
145            self.outfile = open(options["output"], "w")
146            mustclose = 1
147           
148        if datatype == "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            entries = getattr(self.storage, "extract%s" % datatype.title())(extractonly, orderby)
165            if entries :
166                nbentries = len(entries)
167                retcode = getattr(self, "dump%s" % format.title())([self.summarizeDatas(entries, datatype, extractonly, options["sum"])], [datatype])
168               
169        if mustclose :
170            self.outfile.close()
171            if not nbentries : 
172                os.remove(options["output"])
173           
174        return retcode
175       
176    def summarizeDatas(self, entries, datatype, extractonly, sum=0) :   
177        """Transforms the datas into a summarized view (with totals).
178       
179           If sum is false, returns the entries unchanged.
180        """   
181        if not sum :
182            return entries
183        else :   
184            headers = entries[0]
185            nbheaders = len(headers)
186            fieldnumber = {}
187            fieldname = {}
188            for i in range(nbheaders) :
189                fieldnumber[headers[i]] = i
190           
191            if datatype == "payments" :
192                totalize = [ ("amount", float) ]
193                keys = [ "username" ]
194            else : # elif datatype == "history"
195                totalize = [ ("jobsize", int), 
196                             ("jobprice", float),
197                             ("jobsizebytes", int),
198                             ("precomputedjobsize", int),
199                             ("precomputedjobprice", float),
200                           ]
201                keys = [ k for k in ("username", "printername", "hostname", "billingcode") if k in extractonly.keys() ]
202               
203            newentries = [ headers ]
204            sortedentries = entries[1:]
205            if keys :
206                # If we have several keys, we can sort only on the first one, because they
207                # will vary the same way.
208                sortedentries.sort(lambda x, y, fnum=fieldnumber[keys[0]] : cmp(x[fnum], y[fnum]))
209            totals = {}
210            for (k, t) in totalize :
211                totals[k] = { "convert" : t, "value" : 0.0 }
212            prevkeys = {}
213            for k in keys :
214                prevkeys[k] = sortedentries[0][fieldnumber[k]]
215            for entry in sortedentries :
216                curval = '-'.join([str(entry[fieldnumber[k]]) for k in keys])
217                prevval = '-'.join([str(prevkeys[k]) for k in keys])
218                if curval != prevval :
219                    summary = [ "*" ] * nbheaders
220                    for k in keys :
221                        summary[fieldnumber[k]] = prevkeys[k]
222                    for k in totals.keys() :   
223                        summary[fieldnumber[k]] = totals[k]["convert"](totals[k]["value"])
224                    newentries.append(summary)
225                    for k in totals.keys() :   
226                        totals[k]["value"] = totals[k]["convert"](entry[fieldnumber[k]])
227                else :   
228                    for k in totals.keys() :   
229                        totals[k]["value"] += totals[k]["convert"](entry[fieldnumber[k]] or 0.0)
230                for k in keys :
231                    prevkeys[k] = entry[fieldnumber[k]]
232            summary = [ "*" ] * nbheaders
233            for k in keys :
234                summary[fieldnumber[k]] = prevkeys[k]
235            for k in totals.keys() :   
236                summary[fieldnumber[k]] = totals[k]["convert"](totals[k]["value"])
237            newentries.append(summary)
238            return newentries
239           
240    def dumpWithSeparator(self, separator, allentries) :   
241        """Dumps datas with a separator."""
242        for entries in allentries :
243            for entry in entries :
244                line = []
245                for value in entry :
246                    try :
247                        strvalue = '"%s"' % value.encode(self.charset, \
248                                                         "replace").replace(separator, "\\%s" % separator).replace('"', '\\"')
249                    except AttributeError :
250                        if value is None :
251                            strvalue = '"None"' # Double quotes around None to prevent spreadsheet from failing
252                        else :   
253                            strvalue = str(value)
254                    line.append(strvalue)
255                try :
256                    self.outfile.write("%s\n" % separator.join(line))
257                except IOError, msg :   
258                    sys.stderr.write("%s : %s\n" % (_("PyKota data dumper failed : I/O error"), msg))
259                    return -1
260        return 0       
261       
262    def dumpCsv(self, allentries, dummy) :   
263        """Dumps datas with a comma as the separator."""
264        return self.dumpWithSeparator(",", allentries)
265                           
266    def dumpSsv(self, allentries, dummy) :   
267        """Dumps datas with a comma as the separator."""
268        return self.dumpWithSeparator(";", allentries)
269                           
270    def dumpTsv(self, allentries, dummy) :   
271        """Dumps datas with a comma as the separator."""
272        return self.dumpWithSeparator("\t", allentries)
273       
274    def dumpCups(self, allentries, dummy) :   
275        """Dumps history datas as CUPS' page_log format."""
276        months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
277        entries = allentries[0]
278        fieldnames = entries[0]
279        fields = {}
280        for i in range(len(fieldnames)) :
281            fields[fieldnames[i]] = i
282        sortindex = fields["jobdate"]   
283        entries = entries[1:]
284        entries.sort(lambda m, n, si=sortindex : cmp(m[si], n[si]))
285        for entry in entries :   
286            printername = entry[fields["printername"]]
287            username = entry[fields["username"]]
288            jobid = entry[fields["jobid"]]
289            jobdate = DateTime.ISO.ParseDateTime(str(entry[fields["jobdate"]])[:19])
290            gmtoffset = jobdate.gmtoffset()
291            #jobdate = "%s %+03i00" % (jobdate.strftime("%d/%b/%Y:%H:%M:%S"), gmtoffset.hour)
292            jobdate = "%02i/%s/%04i:%02i:%02i:%02i %+03i%02i" % (jobdate.day,
293                                                                 months[jobdate.month - 1],
294                                                                 jobdate.year,
295                                                                 jobdate.hour,
296                                                                 jobdate.minute,
297                                                                 jobdate.second,
298                                                                 gmtoffset.hour,
299                                                                 gmtoffset.minute)
300            jobsize = entry[fields["jobsize"]] or 0
301            copies = entry[fields["copies"]] or 1
302            hostname = entry[fields["hostname"]] or ""
303            billingcode = entry[fields["billingcode"]] or "-"
304            for pagenum in range(1, jobsize+1) :
305                self.outfile.write("%s %s %s [%s] %s %s %s %s\n" % (printername, username, jobid, jobdate, pagenum, copies, billingcode, hostname))
306        return 0       
307       
308    def dumpXml(self, allentries, datatypes) :
309        """Dumps datas as XML."""
310        x = jaxml.XML_document(encoding="UTF-8")
311        x.pykota(version=version.__version__, author=version.__author__)
312        for (entries, datatype) in zip(allentries, datatypes) :
313            x._push()
314            x.dump(storage=self.config.getStorageBackend()["storagebackend"], type=datatype)
315            headers = entries[0]
316            for entry in entries[1:] :
317                x._push()
318                x.entry()
319                for (header, value) in zip(headers, entry) :
320                    try :
321                        strvalue = saxutils.escape(value.encode("UTF-8", \
322                                                                "replace"), \
323                                                   { "'" : "&apos;", \
324                                                     '"' : "&quot;" })
325                    except AttributeError :   
326                        strvalue = str(value)
327                    # We use 'str' instead of 'unicode' below to be compatible
328                    # with older releases of PyKota.
329                    # The XML dump will contain UTF-8 encoded strings,
330                    #�not unicode strings anyway.
331                    x.attribute(strvalue, \
332                                type=type(value).__name__.replace("unicode", "str"), \
333                                name=header)
334                x._pop()   
335            x._pop()   
336        x._output(self.outfile)
337        return 0       
Note: See TracBrowser for help on using the browser.