diff --git a/.gitignore b/.gitignore index c7ff23a..8cf9f60 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ Geraldo.egg-info *$py.class site/fonts site/layout - +*~ +.directory diff --git a/geraldo/__init__.py b/geraldo/__init__.py index c9f0288..ac7bd3c 100644 --- a/geraldo/__init__.py +++ b/geraldo/__init__.py @@ -59,4 +59,5 @@ from graphics import RoundRect, Rect, Line, Circle, Arc, Ellipse, Image from exceptions import EmptyQueryset, ObjectNotFound, ManyObjectsFound, AbortEvent from cross_reference import CrossReferenceMatrix +from barcodes import BarCode diff --git a/geraldo/barcodes.py b/geraldo/barcodes.py index 81425ff..09d6ae1 100644 --- a/geraldo/barcodes.py +++ b/geraldo/barcodes.py @@ -11,7 +11,8 @@ from reportlab.graphics.barcode.code93 import Extended93, Standard93 from reportlab.graphics.barcode.usps import FIM, POSTNET from reportlab.graphics.barcode.usps4s import USPS_4State -from reportlab.graphics.barcode import createBarcodeDrawing +from reportlab.graphics.barcode.qr import QrCodeWidget +from reportlab.graphics.barcode import createBarcodeDrawing SUPPORTED_BARCODE_TYPES = getCodeNames() BARCODE_CLASSES = { @@ -29,8 +30,10 @@ 'Standard39': Standard39, 'Standard93': Standard93, 'USPS_4State': USPS_4State, + 'QR': QrCodeWidget, } + class BarCode(Graphic): """Class used by all barcode types generation. A barcode is just another graphic element, with basic attributes, like 'left', 'top', 'width', 'height' and @@ -41,7 +44,7 @@ class BarCode(Graphic): means you must have a value like 0.01*cm or less to have a good result). Another attribute is 'routing_attribute' used only by type 'USPS_4State'. - + Also supports 'get_value' lambda attribute, like ObjectValue (with the argument 'inst')""" @@ -51,6 +54,7 @@ class BarCode(Graphic): attribute_name = None checksum = 0 routing_attribute = None + aditional_barcode_params = {} get_value = None # A lambda function to get customized values def clone(self): @@ -61,6 +65,7 @@ def clone(self): new.get_value = self.get_value new.checksum = self.checksum new.routing_attribute = self.routing_attribute + new.aditional_barcode_params = self.aditional_barcode_params return new @@ -73,13 +78,16 @@ def set_type(self, typ): def render(self): if not getattr(self, '_rendered_drawing', None): - kwargs = { - 'value': self.get_object_value(), - 'barWidth': self.width, - 'barHeight': self.height, - } - - if self.type in ('EAN13','EAN8',): + kwargs = self.aditional_barcode_params + kwargs['value'] = self.get_object_value() + + if 'barWidth' not in kwargs: + kwargs['barWidth'] = self.width + + if 'barHeight' not in kwargs: + kwargs['barHeight'] = self.height + + if self.type in ('EAN13','EAN8','QR'): self._rendered_drawing = createBarcodeDrawing(self.type, **kwargs) else: cls = BARCODE_CLASSES[self.type] @@ -113,4 +121,3 @@ def _set_width(self, value): self._width = value width = property(_get_width, _set_width) - diff --git a/geraldo/base.py b/geraldo/base.py index ebf8e92..c60ab3a 100644 --- a/geraldo/base.py +++ b/geraldo/base.py @@ -1,9 +1,9 @@ import copy, types, new -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback from utils import calculate_size, get_attr_value, landscape, format_date, memoize,\ BAND_WIDTH, BAND_HEIGHT, CROSS_COLS, CROSS_ROWS, cm, A4, black, TA_LEFT, TA_CENTER,\ @@ -15,9 +15,9 @@ class GeraldoObject(object): """Base class inherited by all report classes, including band, subreports, groups, graphics and widgets. - + Attributes: - + * parent - this is setted by its parent when it is initializing. There is no automated way to get it.""" @@ -47,9 +47,9 @@ def destroy(self): def find_by_name(self, name, many=False): """Find child by informed name (and raises an exception if doesn't find). - + Attributes: - + * name - object name to find * many - boolean attribute that means it returns many objects or not - in the case there are more than one object with the @@ -67,6 +67,7 @@ def find_by_name(self, name, many=False): # Search on child's children try: + print(child, name) ch_found = child.find_by_name(name, many=True) except ObjectNotFound: ch_found = [] @@ -79,7 +80,7 @@ def find_by_name(self, name, many=False): # Found nothing if not found: raise ObjectNotFound('There is no child with name "%s"'%name) - + # Found many elif len(found) > 1 and not many: raise ManyObjectsFound('There are many childs with name "%s"'%name) @@ -89,9 +90,9 @@ def find_by_name(self, name, many=False): def find_by_type(self, typ): """Find child by informed type (and raises an exception if doesn't find). - + Attributes: - + * typ - class type to find """ found = [] @@ -167,8 +168,8 @@ class BaseReport(GeraldoObject): default_stroke_color = black default_fill_color = black borders = None - - # Events (don't make a method with their names, override 'do_*' instead) + + # Events - can be either a method or a regular function before_print = None # | before render before_generate = None # | after render / before generate after_print = None # V after generate @@ -188,6 +189,11 @@ def __init__(self, queryset=None): # Calls the method that set this as parent if their children self.set_parent_on_children() + def _set_band_attr_of_band_elements(self, band): + if band.elements: + for element in band.elements: + element.band = band + def transform_classes_to_objects(self): """Finds all band classes in the report and instantiante them. This is important to have a safety on separe inherited reports each one @@ -196,19 +202,24 @@ def transform_classes_to_objects(self): # Basic bands if self.band_begin and not isinstance(self.band_begin, ReportBand): self.band_begin = self.band_begin() + self._set_band_attr_of_band_elements(self.band_begin) if self.band_summary and not isinstance(self.band_summary, ReportBand): self.band_summary = self.band_summary() + self._set_band_attr_of_band_elements(self.band_summary) if self.band_page_header and not isinstance(self.band_page_header, ReportBand): self.band_page_header = self.band_page_header() + self._set_band_attr_of_band_elements(self.band_page_header) if self.band_page_footer and not isinstance(self.band_page_footer, ReportBand): self.band_page_footer = self.band_page_footer() + self._set_band_attr_of_band_elements(self.band_page_footer) if self.band_detail and not isinstance(self.band_detail, ReportBand): self.band_detail = self.band_detail() self.band_detail.is_detail = True + self._set_band_attr_of_band_elements(self.band_detail) # Groups groups = self.groups @@ -216,7 +227,7 @@ def transform_classes_to_objects(self): def get_objects_list(self): """Returns the list with objects to be rendered. - + This should be refactored in the future to support big amounts of objects.""" if not self.queryset: @@ -229,7 +240,7 @@ def format_date(self, date, expression): You should override this method to force UTF-8 decode or something like this (until we find a better and agnosthic solution). - + Please don't hack this method up. Just override it on your report class.""" return format_date(date, expression) @@ -300,22 +311,34 @@ def get_object_value(self, obj=None, attribute_name=None, action=None): called from their children and on...""" raise AttributeNotFound - # Events methods + # Events - can be either a method or a regular function def do_before_print(self, generator): if self.before_print: - self.before_print(self, generator) + if type(self.before_print) == types.MethodType: + self.before_print(generator) + else: + self.before_print(self, generator) def do_before_generate(self, generator): if self.before_generate: - self.before_generate(self, generator) + if type(self.before_generate) == types.MethodType: + self.before_generate(generator) + else: + self.before_generate(self, generator) def do_after_print(self, generator): if self.after_print: - self.after_print(self, generator) + if type(self.after_print) == types.MethodType: + self.after_print(generator) + else: + self.after_print(self, generator) def do_on_new_page(self, page, page_number, generator): if self.on_new_page: - self.on_new_page(self, page, page_number, generator) + if type(self.on_new_page) == types.MethodType: + self.on_new_page(page, page_number, generator) + else: + self.on_new_page(self, page, page_number, generator) def get_variable_value(self, name, system_fields): """Returns the value for a given variable name""" @@ -327,7 +350,7 @@ def get_variable_value(self, name, system_fields): class ReportMetaclass(type): """This metaclass registers the declared classes to a local variable.""" - + def __new__(cls, name, bases, attrs): # Merges default_style with inherited report classes if isinstance(attrs.get('default_style', None), dict): @@ -362,10 +385,10 @@ def get_report_class_by_registered_id(reg_id): class Report(BaseReport): """This class must be inherited to be used as a new report. - + A report has bands and is driven by a QuerySet. It can have a title and margins definitions. - + Depends on ReportLab to work properly""" __metaclass__ = ReportMetaclass @@ -422,7 +445,7 @@ def __init__(self, queryset=None): def generate_by(self, generator_class, *args, **kwargs): """This method uses a generator inherited class to generate a report to a desired format, like XML, HTML or PDF, for example. - + The arguments *args and **kwargs are passed to class initializer.""" # Check empty queryset and raises an error if this is not acceptable @@ -437,10 +460,10 @@ def generate_by(self, generator_class, *args, **kwargs): def generate_under_process_by(self, generator_class, *args, **kwargs): """Uses the power of multiprocessing library to run report generation under a Process and save memory consumming, with better use of multi-core servers. - + This just will work well if you are generating in a destination file or file-like object (i.e. an HttpResponse on Django). - + It doesn't returns nothing because Process doesn't.""" import tempfile, random, os @@ -519,13 +542,13 @@ def set_parent_on_children(self): class SubReport(BaseReport): """Class to be used for subreport objects. It doesn't need to be inherited. - + - 'queryset_string' must be a string with path for Python compatible queryset. - 'get_queryset' is an optional lambda attribute can be used in replacement to queryset_string to make more dynamic querysets - + Examples: - + * '%(object)s.user_permissions.all()' * '%(object)s.groups.all()' * 'Message.objects.filter(user=%(object)s)' @@ -554,6 +577,9 @@ def __init__(self, **kwargs): setattr(self, k, v) + # Transforms band classes to band objects + self.transform_classes_to_objects() + # Calls the method that set this as parent if their children self.set_parent_on_children() @@ -561,6 +587,22 @@ def __init__(self, **kwargs): if self.band_detail: self.band_detail.is_detail = True + def transform_classes_to_objects(self): + """Finds all band classes in the report and instantiante them. This + is important to have a safety on separe inherited reports each one + from other.""" + + # Basic bands + if self.band_header and not isinstance(self.band_header, ReportBand): + self.band_header = self.band_header() + + if self.band_footer and not isinstance(self.band_footer, ReportBand): + self.band_footer = self.band_footer() + + if self.band_detail and not isinstance(self.band_detail, ReportBand): + self.band_detail = self.band_detail() + self.band_detail.is_detail = True + def queryset(self): if not self._queryset: # Lambda function @@ -646,8 +688,8 @@ class ReportBand(GeraldoObject): default_style = None auto_expand_height = False is_detail = False - - # Events (don't make a method with their names, override 'do_*' instead) + + # Events - can be either a method or a regular function before_print = None after_print = None @@ -670,7 +712,7 @@ def transform_classes_to_objects(self): """Finds all child band classes in this class and instantiante them. This is important to have a safety on separe inherited reports each one from other.""" - + child_bands = self.child_bands self.child_bands = [isinstance(child, ReportBand) and child or child() for child in child_bands] @@ -721,18 +763,25 @@ def set_parent_on_children(self): # Events methods def do_before_print(self, generator): if self.before_print: - self.before_print(self, generator) + if type(self.before_print) == types.MethodType: + self.before_print(generator) + else: + self.before_print(self, generator) def do_after_print(self, generator): if self.after_print: - self.after_print(self, generator) + if type(self.after_print) == types.MethodType: + self.after_print(generator) + else: + self.after_print(self, generator) + class DetailBand(ReportBand): """You should use this class instead of ReportBand in detail bands. - + It is useful when you want to have detail band with strict width, with margins or displayed inline like labels. - + * display_inline: use it together attribute 'width' to specify that you want to make many detail bands per line. Useful to make labels.""" @@ -771,10 +820,10 @@ def transform_classes_to_objects(self): """Finds all band classes in this class and instantiante them. This is important to have a safety on separe inherited reports each one from other.""" - + if self.band_header and not isinstance(self.band_header, ReportBand): self.band_header = self.band_header() - + if self.band_footer and not isinstance(self.band_footer, ReportBand): self.band_footer = self.band_footer() @@ -793,7 +842,7 @@ def set_parent_on_children(self): # Bands if self.band_header: self.band_header.parent = self if self.band_footer: self.band_footer.parent = self - + class Element(GeraldoObject): """The base class for widgets and graphics""" left = 0 @@ -801,8 +850,8 @@ class Element(GeraldoObject): _width = 0 _height = 0 visible = True - - # Events (don't make a method with their names, override 'do_*' instead) + + # Events - can be either a method or a regular function before_print = None after_print = None @@ -888,11 +937,17 @@ def set_parent_on_children(self): # Events methods def do_before_print(self, generator): if self.before_print: - self.before_print(self, generator) + if type(self.before_print) == types.MethodType: + self.before_print(generator) + else: + self.before_print(self, generator) def do_after_print(self, generator): if self.after_print: - self.after_print(self, generator) + if type(self.after_print) == types.MethodType: + self.after_print(generator) + else: + self.after_print(self, generator) _repr_for_cache_attrs = ('left','top','height','width','visible') def repr_for_cache_hash_key(self): diff --git a/geraldo/generators/base.py b/geraldo/generators/base.py index d87e5b2..efda942 100644 --- a/geraldo/generators/base.py +++ b/geraldo/generators/base.py @@ -6,7 +6,7 @@ from geraldo.graphics import Graphic, RoundRect, Rect, Line, Circle, Arc,\ Ellipse, Image from geraldo.barcodes import BarCode -from geraldo.base import GeraldoObject, ManyElements +from geraldo.base import GeraldoObject, ManyElements, SubReport from geraldo.cache import CACHE_BY_QUERYSET, CACHE_BY_RENDER, CACHE_DISABLED,\ make_hash_key, get_cache_backend from geraldo.charts import BaseChart @@ -97,7 +97,7 @@ def execute(self): # Initializes pages self._is_first_page = True - + def render_border(self, borders_dict, rect_dict): """Renders a border in the coordinates setted in the rect.""" b_all = borders_dict.get('all', None) @@ -119,8 +119,8 @@ def render_border(self, borders_dict, rect_dict): if b_left: graphic = isinstance(b_left, Graphic) and b_left or Line() graphic.set_rect( - left=rect_dict['left'], top=rect_dict['top'], - right=rect_dict['left'], bottom=rect_dict['bottom'] + left=rect_dict['left'], top=rect_dict['top'] - rect_dict['height'], + right=rect_dict['left'], height=rect_dict['height'] ) # If border is a number, it is recognized as the stroke width if isinstance(b_left, (int, float)): @@ -135,6 +135,10 @@ def render_border(self, borders_dict, rect_dict): left=rect_dict['left'], top=rect_dict['top'], right=rect_dict['right'], bottom=rect_dict['top'] ) + #graphic.set_rect( + #left=rect_dict['left'], top=rect_dict['top'] - rect_dict['height'], + #right=rect_dict['right'], bottom=rect_dict['top'] - rect_dict['height'] + #) # If border is a number, it is recognized as the stroke width if isinstance(b_top, (int, float)): graphic.stroke_width = b_top @@ -145,8 +149,8 @@ def render_border(self, borders_dict, rect_dict): if b_right: graphic = isinstance(b_right, Graphic) and b_right or Line() graphic.set_rect( - left=rect_dict['right'], top=rect_dict['top'], - right=rect_dict['right'], bottom=rect_dict['bottom'] + left=rect_dict['right'], top=rect_dict['top'] - rect_dict['height'], + right=rect_dict['right'], height=rect_dict['height'] ) # If border is a number, it is recognized as the stroke width if isinstance(b_right, (int, float)): @@ -158,8 +162,8 @@ def render_border(self, borders_dict, rect_dict): if b_bottom: graphic = isinstance(b_right, Graphic) and b_right or Line() graphic.set_rect( - left=rect_dict['left'], top=rect_dict['bottom'], - right=rect_dict['right'], bottom=rect_dict['bottom'] + left=rect_dict['left'], top=rect_dict['top'] - rect_dict['height'], + right=rect_dict['right'], bottom=rect_dict['top'] - rect_dict['height'] ) # If border is a number, it is recognized as the stroke width if isinstance(b_bottom, (int, float)): @@ -177,7 +181,7 @@ def make_band_rect(self, band, top_position, left_position): 'height': self.calculate_size(band.height), } return band_rect - + def make_widget_rect(self, widget, band_rect): """Returns the right widget rect on the PDF canvas""" widget_rect = { @@ -210,8 +214,11 @@ def render_element(self, element, current_object, band, band_rect, temp_top, widget.band = band # This should be done by a metaclass in Band domain TODO widget.page = self._rendered_pages[-1] - # Border rect - widget_rect = self.make_widget_rect(widget, band_rect) + # Changes the widget size according to padding + widget.left += self.calculate_size(widget.padding_left) + widget.top += self.calculate_size(widget.padding_top) + widget.width -= self.calculate_size(widget.padding_left) + self.calculate_size(widget.padding_right) + widget.height -= self.calculate_size(widget.padding_top) + self.calculate_size(widget.padding_bottom) if isinstance(widget, SystemField): widget.left = band_rect['left'] + self.calculate_size(widget.left) @@ -237,9 +244,9 @@ def render_element(self, element, current_object, band, band_rect, temp_top, widget.left = band_rect['left'] + self.calculate_size(widget.left) widget.top = self.calculate_top(temp_top, self.calculate_size(widget.top), self.calculate_size(para.height)) - temp_height = self.calculate_size(element.top) + self.calculate_size(para.height) + temp_height = self.calculate_size(element.top) + self.calculate_size(para.height) + self.calculate_size(element.padding_bottom) else: - temp_height = self.calculate_size(element.top) + self.calculate_size(widget.height) + temp_height = self.calculate_size(element.top) + self.calculate_size(element.height) # Sets element height as the highest if temp_height > self._highest_height: @@ -247,9 +254,6 @@ def render_element(self, element, current_object, band, band_rect, temp_top, self._rendered_pages[-1].add_element(widget) - # Borders - self.render_border(widget.borders or {}, widget_rect) - # Graphic element elif isinstance(element, Graphic): graphic = element.clone() @@ -321,6 +325,9 @@ def render_element(self, element, current_object, band, band_rect, temp_top, for el in element.get_elements(): self.render_element(el, current_object, band, band_rect, temp_top, top_position) + # Subreports + elif isinstance(element, SubReport): + self.render_subreports([element]) def render_band(self, band, top_position=None, left_position=None, update_top=True, current_object=None): @@ -330,7 +337,7 @@ def render_band(self, band, top_position=None, left_position=None, # Calls the before_print event try: band.do_before_print(generator=self) - except AbortEvent: + except AbortEvent, TypeError: return False # Sets the current object @@ -372,16 +379,36 @@ def render_band(self, band, top_position=None, left_position=None, self.render_element(element, current_object, band, band_rect, temp_top, top_position) - # Updates top position - if update_top: - if band.auto_expand_height: - band_height = self._highest_height - else: - band_height = self.calculate_size(band.height) + # Loop at band widgets to draw their borders + # This needs to be here, so we know the highest_height of them all + for element in band.elements: + # Doesn't render not visible element + if not element.visible: + continue - band_height += self.calculate_size(getattr(band, 'margin_top', 0)) - band_height += self.calculate_size(getattr(band, 'margin_bottom', 0)) + # Widget element + if isinstance(element, Widget): + widget = element.clone() + # Renders the widget borders + if band.auto_expand_height: + widget.height = self._highest_height + + widget_rect = self.make_widget_rect(widget, band_rect) + self.render_border(widget.borders or {}, widget_rect) + + # + # Updates actual band height + # + if band.auto_expand_height: + band_height = self._highest_height + else: + band_height = self.calculate_size(band.height) + band_height += self.calculate_size(getattr(band, 'margin_top', 0)) + band_height += self.calculate_size(getattr(band, 'margin_bottom', 0)) + + # Updates top position + if update_top: self.update_top_pos(band_height) # Updates left position @@ -401,9 +428,11 @@ def render_band(self, band, top_position=None, left_position=None, self.render_band(child_band) - # Calls the before_print event + # Calls the after_print event band.do_after_print(generator=self) + #self.force_blank_page_by_height(self.calculate_size(band_height)) + return True def force_blank_page_by_height(self, height): @@ -412,7 +441,7 @@ def force_blank_page_by_height(self, height): if Decimal(str(self.get_available_height())) < Decimal(str(height)): self.start_new_page() return True - + return False def append_new_page(self): @@ -445,7 +474,7 @@ def start_new_page(self, with_header=True): self.render_border(self.report.borders, self._page_rect) # Page footer - self.render_page_footer() + #self.render_page_footer() def render_begin(self): """Renders the report begin band if it exists""" @@ -505,8 +534,7 @@ def render_page_footer(self): # Call method that print the band area and its widgets self.render_band( self.report.band_page_footer, - top_position=self.calculate_size(self.report.page_size[1]) -\ - self.calculate_size(self.report.margin_bottom) -\ + top_position=self.calculate_size(self.report.margin_bottom) +\ self.calculate_size(self.report.band_page_footer.height), update_top=False, ) @@ -524,15 +552,19 @@ def render_end_current_page(self): self._current_page_number += 1 self._is_first_page = False self.update_top_pos(set_position=0) # <---- update top position - + def render_bands(self): """Loops into the objects list to create the report pages until the end""" - + # Preparing local auxiliar variables self._current_page_number = self.report.first_page_number self._current_object_index = 0 objects = self.report.get_objects_list() + # Sets the current object, so it can be used in header band + if len(objects): + self._current_object = objects[self._current_object_index] + # just an alias to make it shorter d_band = self.report.band_detail @@ -565,7 +597,7 @@ def render_bands(self): self.calc_changed_groups(first_object_on_page) if not first_object_on_page: - # The current_object of the groups' footers is the previous + # The current_object of the groups' footers is the previous # object, so we have access, in groups' footers, to the last # object before the group breaking self._current_object = objects[self._current_object_index-1] @@ -640,7 +672,7 @@ def calculate_top(self, *args): return sum(args) def get_top_pos(self): - """We use this to use this to get the current top position, + """We use this to use this to get the current top position, considering also the top margin.""" ret = self.calculate_size(self.report.margin_top) + self._current_top_position @@ -668,7 +700,7 @@ def update_top_pos(self, increase=0, decrease=0, set_position=None): decreasing or setting it with a new value.""" if set_position is not None: self._current_top_position = set_position - else: + else: self._current_top_position += increase self._current_top_position -= decrease @@ -679,7 +711,7 @@ def update_left_pos(self, increase=0, decrease=0, set_position=None): decreasing or setting it with a new value.""" if set_position is not None: self._current_left_position = set_position - else: + else: self._current_left_position += increase self._current_left_position -= decrease @@ -707,7 +739,7 @@ def wrap_barcode_on(self, barcode, width, height): def set_fill_color(self, color): """Sets the current fill on canvas. Used for fonts and shape fills""" pass - + def set_stroke_color(self, color): """Sets the current stroke on canvas""" pass @@ -761,7 +793,7 @@ def render_groups_headers(self, first_object_on_page=False): # Forces a new page if this group is defined to do it if not new_page and group.force_new_page and self._current_object_index > 0 and not first_object_on_page: self.render_page_footer() - self.start_new_page() + #self.start_new_page() # Renders the group header band if group.band_header and group.band_header.visible: @@ -817,10 +849,10 @@ def filter_object(obj): # SubReports - def render_subreports(self): + def render_subreports(self, subreports=None): """Renders subreports bands for the current object in, usings its own queryset. - + For a while just the detail band is rendered. Maybe in future we change this to accept header and footer.""" @@ -830,7 +862,10 @@ def force_new_page(height): self.render_page_footer() self.start_new_page() - for subreport in self.report.subreports: + subreports = subreports or self.report.subreports or [] + + #for subreport in self.report.subreports: + for subreport in subreports: # Subreports must have detail band if not subreport.band_detail or not subreport.visible: continue @@ -850,7 +885,7 @@ def force_new_page(height): force_new_page(subreport.band_header.height) # Renders the header band - if subreport.band_header.visible: + if subreport.band_header and subreport.band_header.visible: self.render_band(subreport.band_header) # Forces new page if there is no available space @@ -893,7 +928,7 @@ def fetch_from_cache(self): if hasattr(self.filename, 'write') and callable(self.filename.write): self.filename.write(buffer) return True - + # Write to file path elif isinstance(self.filename, basestring): fp = file(self.filename, 'w') diff --git a/geraldo/generators/csvgen.py b/geraldo/generators/csvgen.py index 4c244ed..8d4eb23 100644 --- a/geraldo/generators/csvgen.py +++ b/geraldo/generators/csvgen.py @@ -10,7 +10,7 @@ class CSVGenerator(ReportGenerator): """This is a generator to output data in CSV format. This format can be imported as a spreadsheet to Excel, OpenOffice Calc, Google Docs Spreadsheet, and others. - + Attributes: * 'filename' - is the file path you can inform optionally to save text to. @@ -90,6 +90,10 @@ def generate_csv(self): # First row with column names if self.first_row_with_column_names: cells = [(col.name or col.expression or col.attribute_name) for col in columns] + for i in range(len(cells)): + if isinstance(cell[i], unicode): + cell[i] = cell[i].encode('utf-8') + self.writer.writerow(cells) while self._current_object_index < len(objects): @@ -111,7 +115,10 @@ def generate_csv(self): widget.band = self.report.band_detail widget.page = None - cells.append(widget.text) + if isinstance(widget.text, unicode): + cells.append(widget.text.encode('utf-8')) + else: + cells.append(widget.text) # Next object self._current_object_index += 1 diff --git a/geraldo/generators/pdf.py b/geraldo/generators/pdf.py index 0b5b568..1a6dd6f 100644 --- a/geraldo/generators/pdf.py +++ b/geraldo/generators/pdf.py @@ -70,11 +70,11 @@ def __init__(self, report, filename=None, canvas=None, return_canvas=False, # nor if return_canvas attribute is setted as True if canvas or self.return_canvas or self.return_pages: self.multiple_canvas = False - + # Initializes multiple canvas controller variables elif self.multiple_canvas: self.temp_files = [] - + # Just a unique name (current time + id of this object + formatting string for counter + PDF extension) self.temp_file_name = datetime.datetime.now().strftime('%Y%m%d%H%M%s') + str(id(self)) + '_%s.pdf' @@ -106,7 +106,7 @@ def execute(self): # Check the cache if self.cached_before_generate(): return - + # Calls the "after render" event self.report.do_before_generate(generator=self) @@ -202,7 +202,7 @@ def append_pdf(input, output): fp = file(self.filename, 'wb') else: fp = self.filename - + output.write(fp) # Closes and clear objects @@ -286,7 +286,7 @@ def wrap_barcode_on(self, barcode, width, height): def set_fill_color(self, color): """Sets the current fill on canvas. Used for fonts and shape fills""" self.canvas.setFillColor(color) - + def set_stroke_color(self, color): """Sets the current stroke on canvas""" self.canvas.setStrokeColor(color) @@ -341,21 +341,21 @@ def generate_pages(self): # Widget element if isinstance(element, Widget): widget = element - + # Set element colors self.set_fill_color(widget.font_color) - + self.generate_widget(widget, self.canvas, num) - + # Graphic element elif isinstance(element, Graphic): graphic = element - + # Set element colors self.set_fill_color(graphic.fill_color) self.set_stroke_color(graphic.stroke_color) self.set_stroke_width(graphic.stroke_width) - + self.generate_graphic(graphic, self.canvas) self.canvas.showPage() @@ -494,14 +494,14 @@ def generate_graphic(self, graphic, canvas=None): drawing.drawOn(canvas, graphic.left, graphic.top) else: return - + # Calls the after_print event graphic.do_after_print(generator=self) def prepare_additional_fonts(self): """This method loads additional fonts and register them using ReportLab PDF metrics package. - + Just supports TTF fonts, for a while.""" if not self.report.additional_fonts: @@ -526,4 +526,3 @@ def prepare_additional_fonts(self): # Old style: font name and file path else: pdfmetrics.registerFont(TTFont(font_family_name, fonts_or_file)) - diff --git a/geraldo/utils.py b/geraldo/utils.py index 94ae3f2..61dad8c 100644 --- a/geraldo/utils.py +++ b/geraldo/utils.py @@ -51,7 +51,7 @@ def _get_memoized_value(func, args, kwargs): """Used internally by memoize decorator to get/store function results""" key = (repr(args), repr(kwargs)) - + if not key in func._cache_dict: ret = func(*args, **kwargs) func._cache_dict[key] = ret @@ -77,9 +77,9 @@ def get_attr_value(obj, attr_path): is a method with no arguments (or arguments with default values) it calls the method. If the expression string has a path to a child attribute, it supports. - + Examples: - + attribute_name = 'name' attribute_name = 'name.upper' attribute_name = 'customer.name.lower' @@ -90,7 +90,13 @@ def get_attr_value(obj, attr_path): parts = attr_path.split('.') try: - val = getattr(obj, parts[0]) + if hasattr(obj, '_default_' + parts[0]): + default = getattr(obj, '_default_' + parts[0]) + val = getattr(obj, parts[0], default) + if val is None: + val = default + else: + val = getattr(obj, parts[0]) except AttributeError: try: val = obj[parts[0]] @@ -102,7 +108,7 @@ def get_attr_value(obj, attr_path): if callable(val): val = val() - + return val @memoize @@ -115,7 +121,7 @@ def calculate_size(size): # like '10*cm' or '15.8*rows' # I want to check if eval is better way to do it than # do a regex matching and calculate. TODO - + return size # Replaced by ReportLab landscape and portrait functions @@ -142,7 +148,7 @@ def run_under_process(func): """This is a decorator that uses multiprocessing library to run a function under a new process. To use it on Python 2.4 you need to install python-multiprocessing package. - + Just remember that Process doesn't support returning value""" def _inner(*args, **kwargs): diff --git a/geraldo/widgets.py b/geraldo/widgets.py index de9b201..bd5e2ad 100644 --- a/geraldo/widgets.py +++ b/geraldo/widgets.py @@ -1,9 +1,9 @@ import datetime, types, decimal, re -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback from base import BAND_WIDTH, BAND_HEIGHT, Element, SubReport from utils import get_attr_value, SYSTEM_FIELD_CHOICES, FIELD_ACTION_VALUE, FIELD_ACTION_COUNT,\ @@ -17,7 +17,7 @@ class Widget(Element): _width = 5*cm style = {} truncate_overflow = False - + get_value = None # A lambda function to get customized values instance = None @@ -25,6 +25,10 @@ class Widget(Element): generator = None band = None borders = None + padding_top = 0 + padding_bottom = 0 + padding_left = 0 + padding_right = 0 def __init__(self, **kwargs): """This initializer is prepared to set arguments informed as attribute @@ -44,12 +48,16 @@ def clone(self): new.generator = self.generator new.band = self.band new.borders = self.borders + new.padding_top = self.padding_top + new.padding_bottom = self.padding_bottom + new.padding_left = self.padding_left + new.padding_right = self.padding_right return new class Label(Widget): """A label is just a simple text. - + 'get_value' lambda must have 'text' argument.""" _text = '' @@ -84,15 +92,15 @@ def clone(self): class ObjectValue(Label): """This shows the value from a method, field or property from objects got from the queryset. - + You can inform an action to show the object value or an aggregation function on it. - + You can also use 'display_format' attribute to set a friendly string formating, with a mask or additional text. - + 'get_value' and 'get_text' lambda attributes must have 'instance' argument. - + Set 'stores_text_in_cache' to False if you want this widget get its value and text on render and generate moments.""" @@ -118,6 +126,11 @@ def __init__(self, *args, **kwargs): if self.expression: self.prepare_expression() + if 'default_object_value' in kwargs: + self.default_object_value = kwargs['default_object_value'] + else: + self.default_object_value = '' + def prepare_expression(self): if not self.expression: pass @@ -144,7 +157,10 @@ def get_object_value(self, instance=None, attribute_name=None): try: return self.get_value(self, instance) except TypeError: - return self.get_value(instance) + try: + return self.get_value(instance) + except: + return self.default_object_value # Checks this is an expression tokens = EXP_TOKENS.split(attribute_name) @@ -182,7 +198,7 @@ def clean(val): return float(val) elif isinstance(val, float) and self.converts_float_to_decimal: return decimal.Decimal(str(val)) - + return val return map(clean, values) @@ -244,7 +260,7 @@ def _text(self): self._cached_text = unicode(self.get_text(self.instance, value)) else: self._cached_text = unicode(value) - + return self.display_format % self._cached_text def _set_text(self, value): @@ -261,6 +277,7 @@ def clone(self): new.stores_text_in_cache = self.stores_text_in_cache new.expression = self.expression new.on_expression_error = self.on_expression_error + new.get_text = self.get_text return new @@ -304,7 +321,7 @@ def get_value_by_expression(self, expression=None): class SystemField(Label): """This shows system informations, like the report title, current date/time, page number, pages count, etc. - + 'get_value' lambda must have 'expression' and 'fields' argument.""" expression = '%(report_title)s' @@ -327,8 +344,8 @@ def __init__(self, **kwargs): self.fields['current_datetime'] = datetime.datetime.now() def _text(self): - page_number = (self.fields.get('page_number') or self.generator._current_page_number) + self.generator.first_page_number - 1 - page_count = self.fields.get('page_count') or self.generator.get_page_count() + page_number = (self.fields.get('page_number') or self.generator._current_page_number) + self.report.first_page_number - 1 + page_count = (self.fields.get('page_count') or self.generator.get_page_count()) + self.report.first_page_number - 1 fields = { 'report_title': self.fields.get('report_title') or self.report.title, @@ -339,7 +356,7 @@ def _text(self): 'current_datetime': self.fields.get('current_datetime') or datetime.datetime.now(), 'report_author': self.fields.get('report_author') or self.report.author, } - + if self.get_value: return self.get_value(self.expression, fields) diff --git a/setup.py b/setup.py index a4569c3..f768f99 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ # Geraldo setup +import os.path # Downloads setuptools if not find it before try to import try: from setuptools import setup, find_packages @@ -9,7 +10,11 @@ from setuptools import setup, find_packages from setuptools import setup -from geraldo.version import get_version + +# Importing geraldo.version.get_version here would cause an attempt to import +# `reportlab` (via, e.g., geraldo.graphics, imported from geraldo.__init__). +# So we execfile the module directly instead, since it has no dependencies. +execfile(os.path.join(os.path.dirname(__file__), "geraldo", "version.py")) setup( name = 'Geraldo',