root/trunk/dabo/lib/reportWriter.py

Revision 4902, 65.0 kB (checked in by paul, 1 week ago)

Added 5000 points below the font's baseline, which will fix Carl's problem with
the PyCon? badges. Happy New Year!

  • Property svn:eol-style set to native
Line 
1 # -*- coding: utf-8 -*-
2 import copy
3 import datetime
4
5 import decimal
6 Decimal = decimal.Decimal
7
8 import locale
9 import sys
10 import os
11 ######################################################
12 # Very first thing: check for required libraries:
13 _failedLibs = []
14 for lib in ("reportlab", "PIL"):
15     try:
16         __import__(lib)
17     except ImportError:
18         _failedLibs.append(lib)
19
20 if len(_failedLibs) > 0:
21     msg = """
22 The Dabo Report Writer has dependencies on libraries you
23 don't appear to have installed. You still need:
24
25     %s
26
27 PIL is the Python Imaging Library available from
28 http://www.pythonware.com/products/pil
29
30 reportlab is the ReportLab toolkit available from
31 http://www.reportlab.org
32
33 If you are on a Debian Linux system, just issue:
34 sudo apt-get install python-reportlab
35 sudo apt-get install python-imaging
36
37     """ % "\n\t".join(_failedLibs)
38
39     raise ImportError(msg)
40 del(_failedLibs)
41 #######################################################
42
43 import cStringIO
44 import reportlab.pdfgen.canvas as canvas
45 import reportlab.graphics.shapes as shapes
46 import reportlab.lib.pagesizes as pagesizes
47 import reportlab.lib.units as units
48 import reportlab.lib.styles as styles
49 import reportlab.platypus as platypus
50 #import reportlab.lib.colors as colors
51 from dabo.lib.xmltodict import xmltodict
52 from dabo.lib.xmltodict import dicttoxml
53 from dabo.dLocalize import _
54 from dabo.lib.caselessDict import CaselessDict
55 from reportlab.lib.utils import ImageReader
56 from PIL import Image as PILImage
57 import reportUtils
58
59 # The below block tried to use the experimental para.Paragraph which
60 # handles more html tags, including hyperlinks. However, I couldn't
61 # get it to work... among other things, para doesn't accept None as
62 # the availableHeight argument to wrap().
63 if False:
64     try:
65         from reportlab.platypus.para import Paragraph as ParaClass
66     except ImportError:
67         print "No Para class, using Paragraph."
68         ParaClass = platypus.Paragraph
69 else:
70     ParaClass = platypus.Paragraph
71
72
73 def toPropDict(dataType, default, doc):
74     return {"dataType": dataType, "default": default, "doc": doc}
75
76
77 class ReportObjectCollection(list):
78     """Abstract ordered list of things like variables, groups, and band objects."""
79
80     def __init__(self, parent=None, *args, **kwargs):
81         super(ReportObjectCollection, self).__init__(*args, **kwargs)
82         self.parent = parent
83
84     def addObject(self, cls):
85         obj = cls(self)
86         self.append(obj)
87         return obj
88
89     def getPropDoc(self, prop):
90         return ""
91
92     def _getDesProps(self):
93         return {}
94
95     DesignerProps = property(_getDesProps, None, None,
96         _("""Returns a dict of editable properties for the control, with the
97         prop names as the keys, and the value for each another dict,
98         containing the following keys: 'type', which controls how to display
99         and edit the property, and 'readonly', which will prevent editing
100         when True. (dict)""") )
101
102
103 class Variables(ReportObjectCollection): pass
104 class Groups(ReportObjectCollection): pass
105 class Objects(ReportObjectCollection): pass
106
107
108 class ReportObject(CaselessDict):
109     """Abstract report object, such as a drawable object, a variable, or a group."""
110     def __init__(self, parent=None, *args, **kwargs):
111         super(ReportObject, self).__init__(*args, **kwargs)
112         self.parent = parent
113         self.initAvailableProps()
114         self.insertRequiredElements()
115
116     def __getattr__(self, att):
117         rw = self.Report.reportWriter
118
119         # 1) Try mapping the requested attribute to the reportWriter. This will handle
120         #    things like 'self.Application'.
121         try:
122             return getattr(rw, att)
123         except AttributeError:
124             pass
125
126         # 2) Try mapping to a variable (self.ord_amount -> self.Variables["ord_amount"])
127         if self.Variables.has_key(att):
128             return self.Variables.get(att)
129
130         # 3) Try mapping to a field in the dataset (self.ordid -> self.Record["ordid"])
131         if self.Record.has_key(att):
132             return self.Record.get(att)
133
134         raise AttributeError("Can't get attribute '%s'." % att)
135
136
137     def initAvailableProps(self):
138         self.AvailableProps["Comment"] = toPropDict(str, "",
139                 """You can add a comment here, the report will ignore it.""")
140
141     def insertRequiredElements(self):
142         """Insert any missing required elements into the object."""
143         pass
144
145     def addElement(self, cls):
146         """Add a new element, replacing existing one of same name."""
147         obj = cls(self)
148         self[obj.__class__.__name__] = obj
149         return obj
150
151     def addObject(self, cls, collectionClass=Objects):
152         obj = cls(self)
153         collectionName = Objects.__name__
154         objects = self.get(collectionName, collectionClass(self))
155         objects.append(obj)
156         self[collectionName] = objects
157         return obj
158
159     def getMemento(self, start=None):
160         """Return a copy of all the key/values of this and all sub-objects."""
161         if start is None:
162             start = self
163         m = {"type": start.__class__.__name__}
164
165         for k, v in start.items():
166             if isinstance(v, dict):
167                 m[k] = self.getMemento(v)
168             elif isinstance(v, list):
169                 m[k] = []
170                 for c in v:
171                     m[k].append(self.getMemento(c))
172             else:
173                 m[k] = v
174         return m           
175
176
177     def getProp(self, prop, evaluate=True, returnException=False):
178         """Return the value of the property.
179
180         If defined, it will be eval()'d. Otherwise, the default will be returned.
181         If there isn't a default, an exception will be raised as the object isn't
182         set up to have the passed prop.
183         """
184         def getDefault():
185             if self.AvailableProps.has_key(prop):
186                 val = self.AvailableProps[prop]["default"]
187                 if not evaluate:
188                     # defaults are not stringified:
189                     val = repr(val)
190                 return val
191             else:
192                 raise ValueError("Property name '%s' unrecognized." % prop)
193
194         if self.has_key(prop):
195             if not evaluate or prop == "type":
196                 return self[prop]
197             try:
198                 return eval(self[prop])
199             except Exception, e:
200                 # eval() failed. Return the default or the exception string.
201                 if returnException:
202                     return e
203                 return getDefault()
204         else:
205             # The prop isn't defined, use the default.
206             return getDefault()
207
208
209     def setProp(self, prop, val):
210         """Update the value of the property."""
211         if not self.AvailableProps.has_key(prop):
212             raise ValueError("Property '%s' doesn't exist." % prop)
213         self[prop] = val
214
215
216     def getPropVal(self, propName):
217         return self.getProp(propName, evaluate=False)
218
219     def getPropDoc(self, propName):
220         return self.AvailableProps[propName]["doc"]
221
222     def updatePropVal(self, propName, propVal):
223         self.setProp(str(propName), str(propVal))
224
225
226     def _getAvailableProps(self):
227         if hasattr(self, "_AvailableProps"):
228             val = self._AvailableProps
229         else:
230             val = self._AvailableProps = CaselessDict()
231         return val
232
233     def _setAvailableProps(self, val):
234         self._AvailableProps = val
235
236
237     def _getBands(self):
238         return self.Report.reportWriter.Bands
239
240
241     def _getDesProps(self):
242         strType = {"type" : str, "readonly" : False, "alsoDirectEdit": True}
243         props = self.AvailableProps.keys()
244         desProps = {}
245         for prop in props:
246             desProps[prop] = strType.copy()
247             if "color" in prop.lower():
248                 desProps[prop]["customEditor"] = "editColor"
249 ## 2006/1/30: The commented code below makes a dropdown list for the standard
250 #             fonts. However, it blows away the user being able to type in a
251 #             font that isn't in the standard list. Same with the other props.
252 #             We need to get a combobox editor working, so that the user can
253 #             free-form type as well as select a predefined value from the list.
254 #           if prop.lower() == "fontname":
255 #               desProps[prop]["type"] = list
256 #               desProps[prop]["values"] = ['"Courier"', '"Courier-Bold"',
257 #                       '"Courier-Oblique"', '"Courier-BoldOblique"', '"Helvetica"',
258 #                       '"Helvetica-Bold"', '"Helvetica-Oblique"', '"Helvetica-BoldOblique"',
259 #                       '"Times-Roman"', '"Times-Bold"', '"Times-Italic"',
260 #                       '"Times-BoldItalic"', '"Symbol"', '"ZapfDingbats"']
261 #           if prop.lower() == "hanchor":
262 #               desProps[prop]["type"] = list
263 #               desProps[prop]["values"] = ['"Left"', '"Center"', '"Right"']
264 #           if prop.lower() == "vanchor":
265 #               desProps[prop]["type"] = list
266 #               desProps[prop]["values"] = ['"Bottom"', '"Middle"', '"Top"']
267 #           if prop.lower() == "orientation" and self.__class__.__name__ == "Page":
268 #               desProps[prop]["type"] = list
269 #               desProps[prop]["values"] = ['"Portrait"', '"Landscape"']
270         return desProps
271
272
273     def _getRecord(self):
274         if hasattr(self.Report, "_liveRecord"):
275             return self.Report._liveRecord
276         return {}
277
278
279     def _getReport(self):
280         parent = self
281         while not isinstance(parent, Report):
282             parent = parent.parent
283         return parent
284
285
286     def _getVariables(self):
287         return self.Report.reportWriter.Variables
288
289
290     AvailableProps = property(_getAvailableProps, _setAvailableProps)
291     Bands = property(_getBands)
292
293     DesignerProps = property(_getDesProps, None, None,
294         _("""Returns a dict of editable properties for the control, with the
295         prop names as the keys, and the value for each another dict,
296         containing the following keys: 'type', which controls how to display
297         and edit the property, and 'readonly', which will prevent editing
298         when True. (dict)""") )
299
300     Record = property(_getRecord)
301     Report = property(_getReport)
302     Variables = property(_getVariables)
303
304
305 class Drawable(ReportObject):
306     """Abstract drawable report object, such as a rectangle or string."""
307     def initAvailableProps(self):
308         super(Drawable, self).initAvailableProps()
309
310         self.AvailableProps["DesignerLock"] = toPropDict(bool, False,
311                 """Specifies whether the object's geometry can be changed interactively.
312
313                 Setting designerLock to True protects you from accidentally changing
314                 the size and position of the object with the mouse at design time.""")
315
316         self.AvailableProps["x"] = toPropDict(float, 0.0,
317                 """Specifies the horizontal position of the object, relative to hAnchor.""")
318
319         self.AvailableProps["y"] = toPropDict(float, 0.0,
320                 """Specifies the vertical position of the object, relative to vAnchor.""")
321
322         self.AvailableProps["Width"] = toPropDict(float, 55.0,
323                 """Specifies the width of the object.""")
324
325         self.AvailableProps["Height"] = toPropDict(float, 18.0,
326                 """Specifies the height of the object.""")
327
328         self.AvailableProps["Rotation"] = toPropDict(float, 0.0,
329                 """Specifies the rotation of the object, in degrees.""")
330
331         self.AvailableProps["hAnchor"] = toPropDict(str, "left",
332                 """Specifies where horizontal position is relative to.
333
334                 Must evaluate to 'left', 'center', or 'right'.""")
335
336         self.AvailableProps["vAnchor"] = toPropDict(str, "bottom",
337                 """Specifies where vertical position is relative to.
338
339                 Must evaluate to 'bottom', 'middle', or 'top'.""")
340
341         self.AvailableProps["Show"] = toPropDict(bool, None,
342                 """Determines if the object is shown on the report.
343
344                 Specify an expression that evaluates to True or False. If False,
345                 the object will not be shown on the report. Otherwise, it will.
346                 Just like all other properties, your expression will be evaluated
347                 every time this object is to be printed.
348                 """)
349
350
351 class Report(ReportObject):
352     """Represents the report."""
353
354     def initAvailableProps(self):
355         super(Report, self).initAvailableProps()
356
357         self.AvailableProps["Title"] = toPropDict(str, "",
358                 """Specifies the title of the report.""")
359
360         self.AvailableProps["ColumnCount"] = toPropDict(int, 1,
361                 """Specifies the number of columns to divide the report into.""")
362
363     def insertRequiredElements(self):
364         """Insert any missing required elements into the report form."""
365         self.setdefault("Title", "")
366         self.setdefault("Page", Page(self))
367         self.setdefault("PageHeader", PageHeader(self))
368         self.setdefault("Detail", Detail(self))
369         self.setdefault("PageFooter", PageFooter(self))
370         self.setdefault("PageBackground", PageBackground(self))
371         self.setdefault("PageForeground", PageForeground(self))
372         self.setdefault("Groups", Groups(self))
373         self.setdefault("Variables", Variables(self))
374
375
376 class Page(ReportObject):
377     """Represents the page."""
378     def initAvailableProps(self):
379         super(Page, self).initAvailableProps()
380         self.AvailableProps["MarginBottom"] = toPropDict(float, ".5 in",
381                 """Specifies the page's bottom margin.""")
382
383         self.AvailableProps["MarginLeft"] = toPropDict(float, ".5 in",
384                 """Specifies the page's left margin.""")
385
386         self.AvailableProps["MarginTop"] = toPropDict(float, ".5 in",
387                 """Specifies the page's top margin.""")
388
389         self.AvailableProps["MarginRight"] = toPropDict(float, ".5 in",
390                 """Specifies the page's right margin.""")
391
392         self.AvailableProps["Orientation"] = toPropDict(str, "portrait",
393                 """Specifies the page orientation.
394
395                 Must evaluate to one of 'portrait' or 'landscape'.""")
396
397         self.AvailableProps["Size"] = toPropDict(str, "letter",
398                 """Specifies the page size.
399
400                 This is a tuple of (width, heigth) such as:
401                   ('8 in', '5.5 in')
402
403                 You may also use, in place of the tuple,  some common
404                 identifiers such as:
405                   'Letter'
406                   'A4'
407
408                 See also the Orientation property, which merely swaps
409                 the width and height values. """)
410
411
412 class Group(ReportObject):
413     """Represents report groups."""
414     def initAvailableProps(self):
415         super(Group, self).initAvailableProps()
416         self.AvailableProps["expr"] = toPropDict(str, None,
417                 """Specifies the group expression.
418
419                 When the value of the group expression changes, a new group will
420                 be started.""")
421
422         self.AvailableProps["StartOnNewPage"] = toPropDict(bool, False,
423                 _("""Specifies whether new groups should begin on a new page."""))
424
425         self.AvailableProps["ReprintHeaderOnNewPage"] = toPropDict(bool, False,
426                 _("""Specifies whether the group header gets reprinted on new pages."""))
427
428         self.AvailableProps["ResetPageNumber"] = toPropDict(bool, False,
429                 _("""Specifies whether the page number gets reset with a new group."""))
430
431     def insertRequiredElements(self):
432         if not self.has_key("GroupHeader"):
433             self["GroupHeader"] = GroupHeader(self)
434         if not self.has_key("GroupFooter"):
435             self["GroupFooter"] = GroupFooter(parent=self)
436
437 class Variable(ReportObject):
438     """Represents report variables."""
439     def initAvailableProps(self):
440         super(Variable, self).initAvailableProps()
441         self.AvailableProps["InitialValue"] = toPropDict(str, None,
442                 """Specifies the variable's initial value.""")
443
444         self.AvailableProps["expr"] = toPropDict(str, None,
445                 """Specifies the variable expression.
446
447                 At every new record in the cursor, the variable expression will be
448                 evaluated.""")
449
450         self.AvailableProps["Name"] = toPropDict(str, None,
451                 """Specifies the name of the variable.""")
452
453         self.AvailableProps["ResetAt"] = toPropDict(str, None,
454                 """Specifies when to reset the variable to the initial value.
455
456                 Typically, this will match a particular group expression.""")
457
458
459
460 class Band(ReportObject):
461     """Abstract band."""
462     def initAvailableProps(self):
463         super(Band, self).initAvailableProps()
464         self.AvailableProps["Height"] = toPropDict(float, 0.0,
465                 """Specifies the height of the band.
466
467                 If the height evaluates to None, the height of the band will size
468                 itself dynamically at runtime.""")
469
470         self.AvailableProps["DesignerLock"] = toPropDict(bool, False,
471                 """Specifies whether the band height can be changed interactively.
472
473                 Setting designerLock to True protects you from accidentally changing
474                 the height of the band with the mouse at design time.""")
475
476     def insertRequiredElements(self):
477         """Insert any missing required elements into the band."""
478         self.setdefault("Objects", Objects(self))
479
480     def _getBandName(self):
481         name = self.__class__.__name__
482         return "%s%s" % (name[0].lower(), name[1:])
483        
484
485 class PageBackground(Band): pass
486 class PageHeader(Band): pass
487 class Detail(Band): pass
488 class PageFooter(Band): pass
489 class GroupHeader(Band): pass
490 class GroupFooter(Band): pass
491 class PageForeground(Band): pass
492
493
494 class Rectangle(Drawable):
495     """Represents a rectangle."""
496     def initAvailableProps(self):
497         super(Rect, self).initAvailableProps()
498         self.AvailableProps["FillColor"] = toPropDict(tuple, None,
499                 """Specifies the fill color.
500
501                 If None, the fill color will be transparent.""")
502
503         self.AvailableProps["StrokeWidth"] = toPropDict(float, 1,
504                 """Specifies the width of the stroke, in points.""")
505
506         self.AvailableProps["StrokeColor"] = toPropDict(tuple, (0, 0, 0),
507                 """Specifies the stroke color.""")
508
509         self.AvailableProps["StrokeDashArray"] = toPropDict(tuple, None,
510                 """Specifies the stroke dash.
511
512                 For instance, (1,1) will give you a dotted look, (1,1,5,1) will
513                 give you a dash-dot look.""")
514
515 ## backwards compatibility:
516 Rect = Rectangle
517
518 class String(Drawable):
519     """Represents a text string."""
520     def initAvailableProps(self):
521         super(String, self).initAvailableProps()
522         self.AvailableProps["expr"] = toPropDict(str, None,
523                 """Specifies the string to print.""")
524
525         self.AvailableProps["BorderWidth"] = toPropDict(float, 0,
526                 """Specifies the width of the border around the string.""")
527
528         self.AvailableProps["BorderColor"] = toPropDict(tuple, (0, 0, 0),
529                 """Specifies the border color.""")
530
531         self.AvailableProps["Align"] = toPropDict(str, "left",
532                 """Specifies the string alignment.
533
534                 This must evaluate to one of 'left', 'center', or 'right'.""")
535
536         self.AvailableProps["FontName"] = toPropDict(str, "Helvetica",
537                 """Specifies the font name, boldface, and italics all in one.
538
539                 There are only a handful of reliable selections:
540                     Courier
541                     Courier-Bold
542                     Courier-Oblique
543                     Courier-BoldOblique
544
545                     Helvetica
546                     Helvetica-Bold
547                     Helvetica-Oblique
548                     Helvetica-BoldOblique
549
550                     Times-Roman
551                     Times-Bold
552                     Times-Italic
553                     Times-BoldItalic
554
555                     Symbol
556
557                     ZapfDingbats
558
559                 Please note that for predictable cross-platform results, you need to
560                 stick to the fonts above. Otherwise, you'll need to ensure any TTF
561                 fonts you specify are distributed to all systems that create the
562                 reports. If you specify a font name that doesn't exist, the Dabo report
563                 writer will default to 'Helvetica'.
564                 """)
565
566         self.AvailableProps["FontSize"] = toPropDict(float, 10,
567                 """Specifies the size of the font, in points.""")
568
569         self.AvailableProps["FontColor"] = toPropDict(tuple, (0, 0, 0),
570                 """Specifies the color of the text.""")
571
572         self.AvailableProps["ScalePercent"] = toPropDict(tuple, (100.0, 100.0),
573                 """Specifies the scaling of the string. Set to (150,100) to make it wide.""")
574
575
576 class Image(Drawable):
577     """Represents an image."""
578     def initAvailableProps(self):
579         super(Image, self).initAvailableProps()
580         self.AvailableProps["expr"] = toPropDict(str, "",
581                 """Specifies the image to use.""")
582
583         self.AvailableProps["BorderWidth"] = toPropDict(float, 0,
584                 """Specifies the width of the image border.""")
585
586         self.AvailableProps["BorderColor"] = toPropDict(tuple, (0, 0, 0),
587                 """Specifies the color of the image border.""")
588
589         self.AvailableProps["ImageMask"] = toPropDict(tuple, None,
590                 """Specifies the image mask.""")
591
592         self.AvailableProps["ScaleMode"] = toPropDict(str, "scale",
593                 """Specifies how to handle frame and image of differing size.
594
595                 "scale" will change the image size to fit the frame. "clip" will
596                 display the image in the frame as-is.""")
597
598 class BarGraph(Drawable):
599         """Represents a bar graph"""
600     def initAvailableProps(self):
601         super(BarGraph, self).initAvailableProps()
602
603         self.AvailableProps["expr"] = toPropDict(list, [],
604                 """Specifies the data to display on the graph.""")
605
606         self