ReportBro Setup

This section demonstrates how to include ReportBro into your web application and generate reports on your own server based on a demo app.

Albums Demo Application

The albums demo application allows to create and maintain a list of music albums and to download a PDF report based on the displayed data in the list. It is available for both Django (version 2.1+) and web2py (version 2.17.1+) web framework.

demo app overview preview demo app edit view preview

Prerequisites to run the application

  • Install Python from the Python Website
    ReportBro supports Python 2.7 and 3.5+. If you do not have Python installed use the latest stable 3.x release.
    NOTE: For the Django demo app you need Python 3.5+ because the app is written for Django 2.1 which itself supports Python 3.5+
  • Optional: install a virtual environment (a tool to create isolated Python environments) for Python. See Creating virtual environments for further information.
  • Install ReportBro Lib with Python Package Manager pip
    $ pip install reportbro-lib

Django Demo App

  • Download and unpack the django demo app
  • Open a console and install django
    $ pip install django
  • Change to the django_demoapp directory, then run
    $ python manage.py makemigrations albums
  • Create the sqlite db tables
    $ python manage.py migrate
  • Start the server
    $ python manage.py runserver

web2py Demo App

  • Download and unpack the web2py demo app
  • Download and unpack the web2py source code version
  • Copy the albums directory from the demo app into the applications directory of the web2py directory
  • Change to the web2py installation directory
  • Start the server
    $ python web2py.py
The albums demo is now ready to run - call http://localhost:8000/albums/ in your browser to start.

How the application works

If you're familiar with MVC web frameworks the demo app source code should be easy to understand. We will explain the most important parts of the demo application based on the Django implementation.

Database Tables

The demo app uses an sqlite db which will be created and stored in django_demoapp/db.sqlite3. It contains the following tables:

  • report_request: stores report preview requests from ReportBro Designer.
  • report_definition: stores the ReportBro Designer report definition when it is saved. Therefore the table holds either one or zero rows. In a real application this table would usually store multiple report definitions for different types of reports, e.g. invoice report, customer list, contract and so on.
  • album: stores the custom data. We can view all stored albums, add new albums and edit existing ones. We further retrieve data from this table to print our report with ReportBro.

Report Definition

Before we can generate a pdf (or Excel) report, we need to create a report definition in ReportBro Designer. In this demo in case no report definition exists we use a predefined template by calling create_album_report_template in albums/utils.py so you don't have to start from scratch and can immediately edit and print the report.

{% extends "../layout.html" %}

{% block content %}
<div id="reportbro"></div>

<script type="text/javascript">

function saveReport() {
    var report = $('#reportbro').reportBro('getReport');

    //console.log(JSON.stringify(report));
    $.ajax("{% url 'albums:report_save' 'albums_report' %}", {
        data: JSON.stringify(report),
        type: "PUT", contentType: "application/json",
        success: function(data) {
            $('#reportbro').reportBro('setModified', false);
        },
        error: function(jqXHR, textStatus, errorThrown) {
            alert('saving report failed');
        }
    });
}

$(document).ready(function() {
    var rb = $('#reportbro').reportBro({
        reportServerUrl: "{% url 'albums:report_run' %}",
        saveCallback: saveReport
    });
    var report = {{report_definition}};
    if (report) {
        rb.load(report);
    }
});
</script>
{% endblock %}
        
We display the ReportBro Designer and load the report definition into the Designer with rb.load(report). We initialize a save callback so we can store the report definition in our database. This is done in saveReport() where an ajax call transmits our report definition to the save function in albums/report_views.py.

# save report_definition in our table, called by save button in ReportBro Designer
def save(request, report_type):
    if report_type != 'albums_report':
        #  currently we only support the albums report
        raise Http404('report_type not supported')
    json_data = json.loads(request.body.decode('utf-8'))
    # perform some basic input verification
    if not isinstance(json_data, dict) or not isinstance(json_data.get('docElements'), list) or\
            not isinstance(json_data.get('styles'), list) or not isinstance(json_data.get('parameters'), list) or\
            not isinstance(json_data.get('documentProperties'), dict) or not isinstance(json_data.get('version'), int):
        return HttpResponseBadRequest('invalid values')

    report_definition = dict(
        docElements=json_data.get('docElements'), styles=json_data.get('styles'),
        parameters=json_data.get('parameters'),
        documentProperties=json_data.get('documentProperties'), version=json_data.get('version'))

    now = datetime.datetime.now()
    if ReportDefinition.objects.filter(report_type=report_type).update(
            report_definition=report_definition, last_modified_at=now) == 0:
        ReportDefinition.objects.create(
            report_type=report_type, report_definition=report_definition, last_modified_at=now)
    return HttpResponse('ok')
        
save() is called from the save callback and here we update or insert (in case it does not exist yet) the report definition in the database table.

Report Generation


def report(request):
    year = request.GET.get('year')
    if year:
        try:
            year = int(year)
        except (ValueError, TypeError):
            return HttpResponseBadRequest('invalid year parameter')
    else:
        year = None

    params = dict(year=year, albums=list(get_albums(year)), current_date=datetime.date.now())

    if ReportDefinition.objects.filter(report_type='albums_report').count() == 0:
        create_album_report_template()

    report_definition = ReportDefinition.objects.get(report_type='albums_report')
    if not report_definition:
        return HttpResponseServerError('no report_definition available')

    try:
        report = Report(json.loads(report_definition.report_definition), params)
        if report.errors:
            # report definition should never contain any errors,
            # unless you saved an invalid report and didn't test in ReportBro Designer
            raise ReportBroError(report.errors[0])

        pdf_report = report.generate_pdf()
        return FileResponse(io.BytesIO(pdf_report), as_attachment=False, filename='albums.pdf')
    except ReportBroError as ex:
        return HttpResponseServerError('report error: ' + str(ex.error))
    except Exception as ex:
        return HttpResponseServerError('report exception: ' + str(ex))
        

albums/album_views.py contains the report view to print a pdf report. We load the report definition (initially designed in ReportBro Designer) from the report_definition table and use it to create a reportbro.Report instance. Note the params variable which contains our dynamic data. This is a dict containing all listed albums, an optional year filter and the current date. If you view the report template at http://localhost:8000/albums/report/edit you will see those parameters in the parameter section on the left. It is very important that the parameter types defined in ReportBro Designer match the types of your parameters in the params dict passed to reportbro.Report. For example the current_date and year parameters are defined as Date and Number in the Designer, while in the report view they are of types datetime.datetime and int respectively.

Application Data


import datetime
import io
import json

from django.forms.models import model_to_dict
from django.http import FileResponse, HttpResponseBadRequest, HttpResponseServerError, JsonResponse
from django.shortcuts import render
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _
from django.views.decorators.csrf import ensure_csrf_cookie
from reportbro import Report, ReportBroError

from .models import Album, ReportDefinition
from .utils import create_album_report_template, get_menu_items


def data(request):
    year = request.GET.get('year')
    if year:
        try:
            year = int(year)
        except (ValueError, TypeError):
            return HttpResponseBadRequest('invalid year parameter')
    else:
        year = None
    return JsonResponse(list(get_albums(year)), safe=False)


@ensure_csrf_cookie
def edit(request, album_id=None):
    context = {'is_new': album_id is None}
    context['menu_items'] = get_menu_items('album')
    if album_id is not None:
        album = Album.objects.get(id=album_id)
        context['album'] = SafeString(json.dumps(model_to_dict(album)))
    else:
        context['album'] = SafeString(json.dumps(dict(id='', name='', year=None, best_of_compilation=False)))
    return render(request, 'albums/album/edit.html', context)


@ensure_csrf_cookie
def index(request):
    context = {}
    context['menu_items'] = get_menu_items('album')
    context['albums'] = SafeString(json.dumps(list(get_albums())))
    return render(request, 'albums/album/index.html', context)


def report(request):
    year = request.GET.get('year')
    if year:
        try:
            year = int(year)
        except (ValueError, TypeError):
            return HttpResponseBadRequest('invalid year parameter')
    else:
        year = None

    params = dict(year=year, albums=list(get_albums(year)), current_date=datetime.datetime.now())

    if ReportDefinition.objects.filter(report_type='albums_report').count() == 0:
        create_album_report_template()

    report_definition = ReportDefinition.objects.get(report_type='albums_report')
    if not report_definition:
        return HttpResponseServerError('no report_definition available')

    try:
        report = Report(json.loads(report_definition.report_definition), params)
        if report.errors:
            # report definition should never contain any errors,
            # unless you saved an invalid report and didn't test in ReportBro Designer
            raise ReportBroError(report.errors[0])

        pdf_report = report.generate_pdf()
        return FileResponse(io.BytesIO(pdf_report), as_attachment=False, filename='albums.pdf')
    except ReportBroError as ex:
        return HttpResponseServerError('report error: ' + str(ex.error))
    except Exception as ex:
        return HttpResponseServerError('report exception: ' + str(ex))


def save(request):
    json_data = json.loads(request.body.decode('utf-8'))
    if not isinstance(json_data, dict):
        return HttpResponseBadRequest('invalid values')
    album = json_data.get('album')
    if not isinstance(album, dict):
        return HttpResponseBadRequest('invalid values')
    album_id = None
    if album.get('id'):
        try:
            album_id = int(album.get('id'))
        except (ValueError, TypeError):
            return HttpResponseBadRequest('invalid album id')

    values = dict(best_of_compilation=album.get('best_of_compilation'))
    rv = dict(errors=[])
    if not album.get('name'):
        rv['errors'].append(dict(field='name', msg=str(_('error.the field must not be empty'))))
    else:
        values['name'] = album.get('name')
    if not album.get('artist'):
        rv['errors'].append(dict(field='artist', msg=str(_('error.the field must not be empty'))))
    else:
        values['artist'] = album.get('artist')
    if album.get('year'):
        try:
            values['year'] = int(album.get('year'))
            if values['year'] < 1900 or values['year'] > 2100:
                rv['errors'].append(dict(field='year', msg=str(_('error.the field must contain a valid year'))))
        except (ValueError, TypeError):
            rv['errors'].append(dict(field='year', msg=str(_('error.the field must contain a number'))))
    else:
        values['year'] = None

    if not rv['errors']:
        # no validation errors -> save album
        if album_id:
            Album.objects.filter(id=album_id).update(**values)
        else:
            Album.objects.create(**values)
    return JsonResponse(rv)


def get_albums(year=None):
    albums = Album.objects.all()
    if year is not None:
        albums = albums.filter(year=year)
    return albums.values()
        

Further there are views to view and edit albums. Since this has nothing to do with ReportBro in particular and is pretty simple we only give a short explanation of those views:

  • data: Returns all albums from the database table, optionally filtered by album year. This is called when the year filter in the index page is changed.
  • edit: Show edit form to either edit an existing album or to add a new one.
  • index: Page to view all existing albums stored in the database.
  • save: Performs some basic validation and saves an album in the database if there were no validation errors.

Report Generation

Finally we want to use ReportBro and print some PDFs!

Go to http://localhost:8000/albums/album/index/ to add your album data. Click the download button to view the PDF generated by ReportBro.