CACHING REPORTS
===============

Here are the tests to make a good caching system for cache generated reports,
with the goal to avoid to generate the same report twice (or more times).

    >>> import os
    >>> cur_dir = os.path.dirname(os.path.abspath(__file__))

    >>> from geraldo.utils import A4, cm, TA_CENTER, TA_RIGHT
    
    >>> from geraldo import Report, ReportBand, Label, ObjectValue, SystemField,\
    ...     FIELD_ACTION_COUNT, BAND_WIDTH

Cache Status
------------

Summing up, there are three kinds (statuses) of caching reports:

    >>> from geraldo.cache import DEFAULT_CACHE_STATUS, CACHE_DISABLED,\
    ...     CACHE_BY_QUERYSET, CACHE_BY_RENDER

Default cache status must be disabled

    >>> DEFAULT_CACHE_STATUS == CACHE_DISABLED
    True

- No cache - there is no cache, of course

    >>> CACHE_DISABLED == 0
    True

- Cache driven by queryset - the cache is stored by a hash key generated from
  queryset values ( before call method render_pages() ). Basically the cache will
  respect data variation, but won't respect stylizing

    >>> CACHE_BY_QUERYSET == 1
    True

- Cache driven by rendered pages - the cache is stored by a hash key generated
  from repr() of rendered pages and respectivelly elements ( after call method
  render_pages() ). Basically the cache will respect everything ( safer but slower ).

    >>> CACHE_BY_RENDER == 2
    True

Cache Backends
--------------

For a while, Geraldo will only supports file caching, because there is no good
reason to store files with size in kbytes on memory or database, but it's important
to have a way to implement it, if the developer wants.

**BaseCacheBackend** is an abstract class that people can inherit to make their own
backends (use Django's cache system, for example)

    >>> from geraldo.cache import BaseCacheBackend

    >>> hasattr(BaseCacheBackend, 'get')
    True

    >>> hasattr(BaseCacheBackend, 'set')
    True

    >>> hasattr(BaseCacheBackend, 'exists')
    True

**FileCacheBackend** is the one Geraldo handles for itself. Just write in and read
from a directory, named by the hash key, plus the compatible extension.

    >>> from geraldo.cache import FileCacheBackend

Its important to keep aware on this backend doesn't support expiration, because no
matters how much time goes, if the data/render is the same, then the result should
also be the same. If you really wants to implement expiration, you have to
write your out inheritance of BaseCacheBackend.

Cache settings
--------------

Besides 'CACHE_STATUS' setting, you can also inform other settings about caching,
like the following ones

- Cache backend - an instance of a Cache backend class

    >>> from geraldo.cache import CACHE_BACKEND

- Cache file root - the directory path where files will be writen/read

    >>> from geraldo.cache import CACHE_FILE_ROOT

Report class
------------

The report class can implement cache's parameters for itself, for granularity.

    >>> hasattr(Report, 'cache_status')
    True

    >>> hasattr(Report, 'cache_backend')
    True

Cache prefix sets the report's cache prefix (default is its class path)

    >>> hasattr(Report, 'cache_prefix')
    True

Granularity to the setting 'CACHE_FILE_ROOT'

    >>> hasattr(Report, 'cache_file_root')
    True

Test class
----------

    >>> class SimpleListReport(Report):
    ...     title = 'Demonstration without Django'
    ... 
    ...     cache_status = CACHE_BY_QUERYSET
    ...     cache_file_root = os.path.join(cur_dir, 'output', 'cache')
    ... 
    ...     class band_page_header(ReportBand):
    ...         height = 1.3*cm
    ...         elements = [
    ...             SystemField(expression='%(report_title)s', top=0.1*cm, left=0, width=BAND_WIDTH,
    ...                 style={'fontName': 'Helvetica', 'fontSize': 14, 'alignment': TA_CENTER}),
    ...             Label(text="ID", top=0.8*cm, left=0),
    ...             Label(text="Name", top=0.8*cm, left=3*cm),
    ...         ]
    ...         borders = {'bottom': True}
    ... 
    ...     class band_page_footer(ReportBand):
    ...         height = 0.5*cm
    ...         elements = [
    ...             Label(text='Created with Geraldo Reports', top=0.1*cm, left=0),
    ...             SystemField(expression='Page # %(page_number)d of %(page_count)d', top=0.1*cm,
    ...                 width=BAND_WIDTH, style={'alignment': TA_RIGHT}),
    ...         ]
    ...         borders = {'top': True}
    ... 
    ...     class band_detail(ReportBand):
    ...         height = 0.5*cm
    ...         elements = [
    ...             ObjectValue(attribute_name='id', top=0, left=0, name='other-field-with-event'),
    ...             ObjectValue(attribute_name='name', top=0, left=3*cm, name='field-with-event'),
    ...         ]

    >>> objects_list = [
    ...     dict(id=1, name='Rio de Janeiro'),
    ...     dict(id=2, name='New York'),
    ...     dict(id=3, name='Paris'),
    ...     dict(id=4, name='London'),
    ...     dict(id=5, name='Tokyo'),
    ...     dict(id=6, name='Moscow'),
    ...     dict(id=7, name='Beijing'),
    ...     dict(id=8, name='Hamburg'),
    ...     dict(id=9, name='New Delhi'),
    ...     dict(id=10, name='Jakarta'),
    ... ]

    >>> report = SimpleListReport(queryset=objects_list)

Default cache_prefix
--------------------

    >>> report.cache_prefix
    '__builtin__-SimpleListReport'

Hash keys generating
--------------------

    >>> from geraldo.cache import make_hash_key

This function will check the report class to find all explicitly used attributes,
and will make a dictionary for each item in queryset and make a long string, that
will be converted to a hash key.

    >>> bool(make_hash_key(report, objects_list))
    True

It uses a method on Report 'get_cache_relevant_attributes', if exists, to get the
attributes list.

    >>> import new
    >>> def get_cache_relevant_attributes(self):
    ...     return ['name']
    >>> report.get_cache_relevant_attributes = new.instancemethod(
    ...     get_cache_relevant_attributes, report, SimpleListReport,
    ... )

    >>> report.get_cache_relevant_attributes()
    ['name']

    >>> bool(make_hash_key(report, objects_list))
    True

PDF generation
--------------

    >>> from geraldo.generators import PDFGenerator

Hack the render method to print a 'r' to the output

    >>> def render_bands(self):
    ...     super(PDFGenerator, self).render_bands()
    ...     print 'r'
    >>> PDFGenerator.render_bands = render_bands

Hack the generation method to print a 'g' to the output

    >>> def generate_pages(self):
    ...     self._generate_pages()
    ...     print 'g'
    >>> PDFGenerator._generate_pages = PDFGenerator.generate_pages
    >>> PDFGenerator.generate_pages = generate_pages

Cached by Queryset
------------------

Generate the report - first time (must print 'r' and 'g' and write to cache)

    >>> report.generate_by(PDFGenerator, filename=os.path.join(cur_dir, 'output/cached-report.pdf'))
    r
    g

Generate the report again (now must no print the 'r' nor the 'g')

    >>> report.generate_by(PDFGenerator, filename=os.path.join(cur_dir, 'output/cached-report.pdf'))

Cached by Rendered Pages
------------------------

    >>> report.cache_status = CACHE_BY_RENDER

Generate the report - first time (must print 'r' and 'g' and write to cache)

    >>> report.generate_by(PDFGenerator, filename=os.path.join(cur_dir, 'output/cached-report.pdf'))
    r
    g

Generate the report again (just print the 'r')

    >>> report.generate_by(PDFGenerator, filename=os.path.join(cur_dir, 'output/cached-report.pdf'))
    r

Cache manually disabled
-----------------------

Besides generic and granulary ways of cache enabling, you can enable or disable
when you were executing the generation method, setting argument 'cache_enabled'
to None (default choice), True (force enabling) or False (force disabling).

    >>> report.generate_by(PDFGenerator, filename=os.path.join(cur_dir, 'output/no-cached-report.pdf'),
    ...     cache_enabled=False)
    r
    g

Clean files from test cache file root

    >>> import shutil
    >>> shutil.rmtree(report.cache_file_root)

Remove hacked methods

    >>> del PDFGenerator.render_bands
    >>> PDFGenerator.generate_pages = PDFGenerator._generate_pages
    >>> del PDFGenerator._generate_pages

