1 |
# Copyright 2003 Fundacion Via Libre |
# -*- coding: latin1 -*- |
2 |
|
# |
3 |
|
# Copyright 2003 Fundación Via Libre |
4 |
# |
# |
5 |
# This file is part of PAPO. |
# This file is part of PAPO. |
6 |
# |
# |
7 |
# PAPO is free software; you can redistribute it and/or modify |
# PAPO is free software; you can redistribute it and/or modify |
8 |
# it under the terms of the GNU General Public License as published by |
# 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 |
# the Free Software Foundation; either version 2 of the License, or |
10 |
# (at your option) any later version. |
# (at your option) any later version. |
11 |
# |
# |
12 |
# PAPO is distributed in the hope that it will be useful, |
# PAPO is distributed in the hope that it will be useful, |
13 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 |
# GNU General Public License for more details. |
# GNU General Public License for more details. |
16 |
# |
# |
17 |
# You should have received a copy of the GNU General Public License |
# You should have received a copy of the GNU General Public License |
18 |
# along with PAPO; if not, write to the Free Software |
# along with PAPO; if not, write to the Free Software |
19 |
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
20 |
|
|
21 |
from __future__ import nested_scopes |
from UserList import UserList |
22 |
import sys |
from xml.dom.ext.reader import PyExpat |
23 |
import urllib |
from xml.dom import Node |
24 |
import xml.sax |
from xml import xpath |
25 |
from xml.sax.handler import ContentHandler |
|
26 |
|
import pathetic |
27 |
from Layouts import * |
import compute |
28 |
from Stuff import * |
|
29 |
|
from errors import * |
30 |
class Handler(ContentHandler): |
|
31 |
meth_dict = { "header": "block", |
def init_dentry_from_xml(dentry, dom): |
32 |
"page_header": "block", |
for box in xpath.Evaluate('box', dom): |
33 |
"partial_header": "block", |
p=[] |
34 |
"footer": "block", |
for i in box.childNodes: |
35 |
"page_footer": "block", |
if i.nodeType == Node.TEXT_NODE: |
36 |
"partial_footer": "block", |
p.append(i.data) |
37 |
"detail_entry": "dentry", |
elif i.nodeName == 'field': |
38 |
} |
name = i.getAttribute('name') |
39 |
class_dict = { "header": Block, |
default = i.getAttribute('default') |
40 |
"page_header": Block, |
p.append(pathetic.Field(dentry, name, default)) |
41 |
"partial_header": Block, |
elif i.nodeName == 'compute': |
42 |
"footer": Block, |
expr = i.getAttribute('expr') |
43 |
"page_footer": Block, |
watch = i.getAttribute('watch') |
44 |
"partial_footer": Block, |
if watch: |
45 |
"detail_entry": Dentry, |
watch = [j.strip() for j in watch.split(',')] |
46 |
"box": Box, |
else: |
47 |
"detail": Detail, |
watch = [] |
48 |
"compute": Compute, |
p.append(compute.Compute(dentry, expr, watch)) |
49 |
"field": Field, |
kw={} |
50 |
} |
for i in ('line', 'column', 'width', 'height'): |
51 |
|
kw[i] = int(box.getAttribute(i) or 0) |
52 |
def __init__(self, template): |
for i in ('vfill', 'raw', 'wrap'): |
53 |
ContentHandler.__init__(self) |
kw[i] = box.getAttribute(i) == 'true' |
54 |
self.stack = [template] |
kw['align'] = {"right": 1, "center": 0}.setdefault(box.getAttribute('align'), -1) |
55 |
|
dentry.add_box(*p, **kw) |
56 |
def characters(self, chars): |
|
57 |
#chars = chars.strip() |
def init_detail_from_xml(detail, dom): |
58 |
if chars.strip(): |
for den in xpath.Evaluate('detail_entry', dom): |
59 |
self.stack[-1].append(Literal(chars)) |
d = detail.add_dentry(den.getAttribute('name')) |
60 |
|
init_dentry_from_xml(d, den) |
61 |
def startElement(self, name, attrs): |
for det in xpath.Evaluate('detail', dom): |
62 |
method = getattr(self, "start_" + self.meth_dict.get(name, name), None) |
d = detail.add_detail(det.getAttribute('name')) |
63 |
if callable(method): |
init_detail_from_xml(d, det) |
64 |
self.stack.append(method(name, attrs)) |
for i in ('header', 'footer'): |
65 |
|
for j in ('', 'page_', 'partial_'): |
66 |
|
n=j+i |
67 |
|
d = xpath.Evaluate(n, dom) |
68 |
|
if d: |
69 |
|
d=d[0] |
70 |
|
try: |
71 |
|
h = int(d.getAttribute('height')) |
72 |
|
except ValueError: |
73 |
|
raise InvalidExtremaError, \ |
74 |
|
"%s/%s should have an explicit, numeric, height attribute" \ |
75 |
|
% (detail.fqdn, n) |
76 |
|
x=pathetic.Shelf(detail, n, h) |
77 |
|
setattr(detail, n, x) |
78 |
|
init_dentry_from_xml(x, d) |
79 |
|
else: |
80 |
|
setattr(detail, n, None) |
81 |
|
|
82 |
|
class Page: |
83 |
|
def __init__(self, height, max_height, prev): |
84 |
|
if height: |
85 |
|
self.expand = 1 |
86 |
else: |
else: |
87 |
try: |
self.expand = 0 |
88 |
c = self.class_dict[name] |
self.target_height = max_height |
89 |
except KeyError: |
if prev is None: |
90 |
raise DocumentLayoutError, "I don't know how to handle a %s" % name |
self.details = [] |
91 |
|
self.data = [] |
92 |
thing = c(attrs) |
else: |
93 |
thing._type = name |
self.details = prev.details[:] |
94 |
thing._parent = self.stack[-1] |
# el encabezado de una página es cada uno de los page_headers de |
95 |
self.stack[-1].append(thing) |
# los detalles abiertos desde el de más afuera hasta más adentro, |
96 |
self.stack.append(thing) |
# seguido de todos los partial_header en el mismo orden |
97 |
|
self.data = filter(None, [i.page_header for i in self.details] |
98 |
def endElement(self, name): |
+ [i.partial_header or i.header for i in self.details]) |
99 |
method = getattr(self, "end_" + self.meth_dict.get(name, name), None) |
self.prev = prev |
|
if callable(method): |
|
|
method() |
|
|
self.stack.pop() |
|
|
|
|
|
def start_block(self, name, attrs): |
|
|
block = Block(attrs) |
|
|
detail = self.stack[-1] |
|
|
block.name = name |
|
|
|
|
|
detail.append(block) |
|
|
detail.by_name[name] = block |
|
100 |
|
|
|
return block |
|
101 |
|
|
102 |
def start_dentry(self, name, attrs): |
def post(self): |
103 |
dentry = Dentry(attrs) |
# el pie de una página es el partial_footer de cada uno de los |
104 |
detail = self.stack[-1] |
# detalles abiertos, seguido de los page_footer de cada uno de los |
105 |
detail.append(dentry) |
# detalles abiertos, de adentro hacia afuera. |
106 |
detail.by_name[dentry.name] = dentry |
post = filter(None, |
107 |
|
[i.page_footer for i in self.details] |
108 |
return dentry |
+ [i.partial_footer for i in self.details]) |
109 |
|
post.reverse() |
110 |
def start_field(self, name, attrs): |
return post |
|
field = Field(attrs) |
|
|
box = self.stack[-1] |
|
|
block = self.stack[-2] |
|
111 |
|
|
|
box.append(field) |
|
|
block.by_name[field.name] = field |
|
|
|
|
|
return field |
|
|
|
|
|
def start_detail(self, name, attrs): |
|
|
detail = Detail(attrs) |
|
|
parent = self.stack[-1] |
|
|
parent.append(detail) |
|
|
parent.by_name[detail.name] = detail |
|
112 |
|
|
113 |
return detail |
def extend(self, other): |
114 |
|
self.data.extend(other) |
115 |
|
|
116 |
|
def append(self, other): |
117 |
|
self.data.append(other) |
118 |
|
|
119 |
class Template(Detail): |
def data_length(self): |
120 |
def __init__(self, url=None): |
l=0 |
121 |
Detail.__init__(self, {"name": "Template"}) |
for i in self.data: |
122 |
if url is not None: |
if isinstance(i, pathetic.Pathetic): |
123 |
self.parse(url) |
l+=i.height |
124 |
|
else: |
125 |
|
l+=1 |
126 |
|
return l |
127 |
|
|
128 |
|
def __len__(self): |
129 |
|
if not self.details: |
130 |
|
return 0 |
131 |
|
h1 = self.data_length() |
132 |
|
h2 = reduce(lambda a, b: a+b.height, self.post(), 0) |
133 |
|
if self.details[-1].footer: |
134 |
|
h2 = max(h2, self.details[-1].footer.height) |
135 |
|
return h1+h2 |
136 |
|
return h1 |
137 |
|
|
138 |
|
def replace(self, old, new): |
139 |
|
# linear search -- sucks, pero qué se le va a hacer |
140 |
|
i = None |
141 |
|
page = self |
142 |
|
while page: |
143 |
|
try: |
144 |
|
i = page.data.index(old) |
145 |
|
page.data = page.data[:i] + new + page.data[i+1:] |
146 |
|
page = page.prev |
147 |
|
except ValueError: |
148 |
|
page = page.prev |
149 |
|
|
150 |
|
def add_detail(self, detail): |
151 |
|
self.details.append(detail) |
152 |
|
if detail.header: |
153 |
|
self.append(detail.header) |
154 |
|
|
155 |
|
|
156 |
def parse(self, url): |
def close_detail(self): |
157 |
try: |
d = self.details.pop() |
158 |
self.file = urllib.urlopen(url) |
for i in ('header', 'footer'): |
159 |
xml.sax.parse(self.file, Handler(self)) |
for j in ('', 'page_', 'partial_'): |
160 |
finally: |
n=j+i |
161 |
self.file.close() |
x = getattr(d, n, None) |
162 |
|
if x: |
163 |
|
self.replace(x, x.render()) |
164 |
|
|
165 |
|
def render(self): |
166 |
|
post = self.post() |
167 |
|
r=[] |
168 |
|
for i in self.data: |
169 |
|
if isinstance(i, pathetic.Pathetic): |
170 |
|
r.extend(i.render()) |
171 |
|
else: |
172 |
|
r.append(i) |
173 |
|
maxrlen = self.target_height - len(post) |
174 |
|
if self.target_height and len(r) < maxrlen and self.expand: |
175 |
|
i=None |
176 |
|
if self.details: |
177 |
|
i = self.details[0].dict.get(self.details[-1].fqdn + '_empty/') |
178 |
|
if i: |
179 |
|
i = i.render() |
180 |
|
else: |
181 |
|
i = [' '*len(r[-1])] |
182 |
|
r.extend(i*(maxrlen - len(r) - 1)) |
183 |
|
for i in post: |
184 |
|
if isinstance(i, pathetic.Pathetic): |
185 |
|
r.extend(i.render()) |
186 |
|
else: |
187 |
|
r.append(i) |
188 |
|
r.append('\f') |
189 |
|
return r |
190 |
|
|
191 |
|
|
192 |
|
class Report: |
193 |
|
def __init__(self, file): |
194 |
|
(dom,) = xpath.Evaluate('/report', PyExpat.Reader().fromUri(file)) |
195 |
|
max_height = int(dom.getAttribute('max_height') or 0) |
196 |
|
height = int(dom.getAttribute('height') or 0) |
197 |
|
max_num_pages = int(dom.getAttribute('max_num_pages') or 0) |
198 |
|
template = pathetic.Template(max_num_pages=max_num_pages, |
199 |
|
max_height=max_height, |
200 |
|
height=height) |
201 |
|
init_detail_from_xml(template, dom) |
202 |
|
self.template = template |
203 |
|
self.init_stack() |
204 |
|
self.init_pages() |
205 |
|
self.path = [] |
206 |
|
self.add_detail('') |
207 |
|
self.set_extrema() |
208 |
|
|
209 |
|
def set_extrema(self): |
210 |
|
for i in ('header', 'footer'): |
211 |
|
for j in ('', 'page_', 'partial_'): |
212 |
|
n=j+i |
213 |
|
if self.details: |
214 |
|
x = getattr(self.details[-1], n) |
215 |
|
else: |
216 |
|
x = None |
217 |
|
setattr(self, n, x) |
218 |
|
|
219 |
|
def init_stack(self): |
220 |
|
self.details = [] |
221 |
|
|
222 |
|
def init_pages(self): |
223 |
|
self.pages = [] |
224 |
|
self.add_page() |
225 |
|
|
226 |
|
def add_page(self): |
227 |
|
prev = None |
228 |
|
if self.pages: |
229 |
|
prev = self.pages[-1] |
230 |
|
page = Page(self.template.height, self.template.max_height, prev) |
231 |
|
self.pages.append(page) |
232 |
|
|
233 |
|
def guess_fqdn(self, name): |
234 |
|
if self.path: |
235 |
|
return '/'.join(self.path) + '/' + name + '/' |
236 |
|
else: |
237 |
|
return '/' |
238 |
|
|
239 |
|
def add_dentry(self, name, **kw): |
240 |
|
d = self.template.dict[self.guess_fqdn(name)] |
241 |
|
out = d.render(**kw) |
242 |
|
if self.template.max_height and \ |
243 |
|
len(self.pages[-1]) + len(out) > self.template.max_height: |
244 |
|
if self.template.max_num_pages and \ |
245 |
|
len(self.pages) >= self.template.max_num_pages: |
246 |
|
raise ReportOverflowError, "no more space in report" |
247 |
|
else: |
248 |
|
self.add_page() |
249 |
|
self.pages[-1].extend(out) |
250 |
|
|
251 |
|
def add_detail(self, name): |
252 |
|
detail = self.template.dict[self.guess_fqdn(name)] |
253 |
|
self.path.append(name) |
254 |
|
self.details.append(detail) |
255 |
|
self.pages[-1].add_detail(detail) |
256 |
|
self.set_extrema() |
257 |
|
|
258 |
|
def close_detail(self): |
259 |
|
self.pages[-1].close_detail() |
260 |
|
self.details.pop() |
261 |
|
self.path.pop() |
262 |
|
self.set_extrema() |
263 |
|
|
264 |
|
def render(self): |
265 |
|
render = [] |
266 |
|
pageno = 1 |
267 |
|
compute.set_numpages(len(self.pages)) |
268 |
|
for i in self.pages: |
269 |
|
compute.set_pageno(pageno) |
270 |
|
pageno += 1 |
271 |
|
render.extend(i.render()) |
272 |
|
return render |
273 |
|
|
274 |
|
def __str__(self): |
275 |
|
|
276 |
|
# la impresión ocurre al completar. Entonces lo que hay que hacer es |
277 |
|
# cerrar todos los detalles que queden abiertos (menos el último, para |
278 |
|
# que salga después el page_footer) |
279 |
|
while len(self.details) > 1: |
280 |
|
self.close_detail() |
281 |
|
|
282 |
|
s = "\n".join(["\n".join(self.render())]) |
283 |
|
return s |
284 |
|
|
285 |
|
if __name__=='__main__': |
286 |
|
import time |
287 |
|
r=Report('test.xml') |
288 |
|
r.add_detail('bar') |
289 |
|
#r.header(fecha=time.ctime()) |
290 |
|
r.add_dentry('item', moneda='Morlacks with cochinillas arábigas', contado=100, ctacte=850, otros=50, total=1000) |
291 |
|
r.add_dentry('item', moneda='MORLACKS WITH COCHINILLAS ARÁBIGAS', contado=10, ctacte=85, otros=5, total=100) |
292 |
|
r.add_dentry('item', moneda='Morlacks with cochinillas arábigas', contado=100, ctacte=850, otros=50, total=1000) |
293 |
|
r.add_dentry('item', moneda='Morlacks with cochinillas arábigas', contado=100, ctacte=850, otros=50, total=1000) |
294 |
|
r.add_detail('foo') |
295 |
|
r.add_dentry('test') |
296 |
|
r.add_dentry('test') |
297 |
|
r.add_dentry('test') |
298 |
|
r.close_detail() |
299 |
|
r.add_detail('foo') |
300 |
|
r.add_dentry('test') |
301 |
|
r.add_dentry('test') |
302 |
|
r.add_dentry('test') |
303 |
|
r.close_detail() |
304 |
|
r.add_dentry('item', moneda='Morlacks with cochinillas arábigas', contado=100, ctacte=850, otros=50, total=1000) |
305 |
|
print r |
306 |
|
|