Album App

Get the app
albumapp-django
albumapp-flask
albumapp-web2py
Database Tables
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