Python Icon

Album App

The album app features creating and maintaining an overview of music albums and downloading a PDF report based on the displayed data. The app demonstrates how to include ReportBro into a web application and how to generate reports.

Get the app

Album app is available for Django (version 4.1+), Flask (version 2+) and web2py (version 2.18.5+) web frameworks. The most important parts of the demo application are explained based on the Django implementation.

albumapp-django

ReportBro album application for Django web framework. This is a fully working demo app to showcase ReportBro and how you can integrate it in your Django application.

albumapp-flask

ReportBro album application for Flask web framework. This is a fully working demo app to showcase ReportBro and how you can integrate it in your Flask application.

albumapp-web2py

ReportBro album application for web2py web framework. This is a fully working demo app to showcase ReportBro and how you can integrate it in your web2py application.

Database Tables

We use 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. 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.

albums/template/albums/report/edit.html

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

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

<script type="text/javascript">

function saveReport() {
    const reportData = rb.getReport();

    //console.log(JSON.stringify(reportData));
    axios.put('{% url 'albums:report_save' 'albums_report' %}', reportData
    ).then(function (response) {
        // report definition saved successfully,
        // set modified flag to false to disable save button
        rb.setModified(false);
    })
    .catch(function (error) {
        alert('saving report failed');
    });
}

const rb = new ReportBro(document.getElementById('reportbro'), {
    reportServerUrl: '{% url 'albums:report_run' %}',
    saveCallback: saveReport
});

const 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.

albums/report_views.py:save()

# 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

albums/album_views.py:report

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

albums/album_views.py

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=gettext('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=gettext('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=gettext('error.the field must contain a valid year')))
        except (ValueError, TypeError):
            rv['errors'].append(dict(field='year', msg=gettext('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()
                    

albums/album_views.py also contains the index view to to show all albums stored in the database, the edit view to edit an existing album or create a new one, and a save function to save an album - passed as json data from an ajax request sent in albums/templates/albums/album/edit.html