devilry_report — Devilry report framework

The devilry_report module provides a framework for generating downloadable reports.

Datamodel API

class devilry.devilry_report.models.DevilryReport(*args, **kwargs)

Bases: django.db.models.base.Model

A model representing a report of various data, e.g complete user report.

The model is designed for being updated asynchronously with a continuously updated status while generating a report. Of course, the a report can also be generated synchronously, but usually the report generators will perform time-consuming tasks.

Report data is stored as binary data, and is always generated for a specific user.

generated_by_user

The user(AUTH_USER_MODEL) that generated the report.

created_datetime

The datetime the report was generated. Defaults to timezone.now

started_datetime

When the report generation was started.

finished_datetime

When the report generation was finished.

generator_type

The generator type.

This is specified in a subclass of devilry.devilry_report.abstract_generator.AbstractReportGenerator.

generator_options

JSON-field for generator options that are specific to the generator_type.

If the generator

STATUS_CHOICES = <ievv_opensource.utils.choices_with_meta.ChoicesWithMeta object>

Supported status types.

status

The current status of the report.

  • unprocessed: The report generation has not started yet.
  • generating: Report data is being generated.
  • success: The report was successfully generated.
  • error: Something happened during report generation. Data will be available in the status_data-field.
status_data

The status data of the report generation. Usually only contains data when the some error occurred during report generation.

output_filename

Name of the generated file with results.

content_type

Content-type used when creating a download-request.

result

The complete report stored as binary data.

clean()

Hook for doing any extra model-wide validation after clean() has been called on every field by self.clean_fields. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field defined by NON_FIELD_ERRORS.

generator

Fetch generator from the generator registry based on the generator_type field. :returns: Subclass. :rtype: AbstractReportGenerator

generate()
Typically called within RQ task
  • Sets started_datetime to NOW
  • Sets status=”generating”
  • Calls self.generator.generate() - on completion: - If no exception - set status=”success” - If exception - save traceback in status_data and set status=”error” - Set finished_datetime
exception DoesNotExist

Bases: django.core.exceptions.ObjectDoesNotExist

exception MultipleObjectsReturned

Bases: django.core.exceptions.MultipleObjectsReturned

Generators

The framework provides an abstract generator-class you will need to subclass. You need to implement a set of required methods in the generator-subclass, and the actual data parsing.

class devilry.devilry_report.abstract_generator.AbstractReportGenerator(devilry_report)

Bases: object

Abstract generator class that generators must inherit from. Provides an interface for generators used by devilry.devilry_report.models.DevilryReport.

classmethod get_generator_type()

Get the generator type as string.

Returns:Generator type.
Return type:str
get_output_filename_prefix()

Get the output filename prefix. Returns report by default.

Returns:Output filename prefix.
Return type:str
get_output_file_extension()

Get the output file extension.

Returns:Output file extension.
Return type:str
get_content_type()

The content-type used for download.

Returns:A HTTP-supported content-type
Return type:str
validate()

Validate required input. Mostly used for validating devilry.devilry_report.models.DevilryReport.generator_options. This method is optional and does not have to be overridden.

If everything is validated, do nothing, else raise ValidationError.

generate(file_like_object)

Must be overridden in subclass. Should generated a byte format that can be stored in the devilry.devilry_report.models.DevilryReport.

Parameters:file_like_object – An object that can be read from.
get_object_iterable()

Override this and and return an iterable of “objects”.

class devilry.devilry_report.abstract_generator.AbstractExcelReportGenerator(row=1, column=0, *args, **kwargs)

Bases: devilry.devilry_report.abstract_generator.AbstractReportGenerator

Abstract generator class for generating an Excel worksheet with the xlsxwriter library.

get_output_file_extension()

Get the output file extension.

Returns:Output file extension.
Return type:str
get_content_type()

The content-type used for download.

Returns:A HTTP-supported content-type
Return type:str
add_worksheet_headers(worksheet)

Override and add worksheet headers.

Parameters:worksheet – A xlsxwriter Worksheet instance.
write_data_to_worksheet(worksheet_tuple, row, column, obj)

Override this and write the data to the worksheet.

Parameters:
  • worksheet_tuple – A tuple of type(str) and xlsxwriter Worksheet instance.
  • row – Row position integer.
  • column – Column position integer.
  • obj – The object to write data from.
get_work_sheets()

Override this method if want to add multiple worksheets.

Adds a single worksheet by default.

Must return a list of xlsx.Worksheet.

Generator registry

To be able to use a generator, you need to register the generator-class in a registry singleton in the apps.py-file in the app you where the generator belongs. Do NOT create or register generators in the devilry_report-app, this is a framework, and thus other apps should be dependent on this app not the other way around.

class devilry.devilry_report.generator_registry.Registry

Bases: ievv_opensource.utils.singleton.Singleton

Registry of devilry_report generator types. Holds exactly one subclass of devilry.devilry_report.abstract_generator.AbstractReportGenerator for each generator_type.

get(generator_type)

Get a subclass of devilry.devilry_report.abstract_generator.AbstractReportGenerator stored in the registry by the generator_type.

Parameters:generator_type (str) – A devilry.devilry_report.abstract_generator.AbstractReportGenerator.get_generator_type.
Raises:ValueError – If generator_type is not in the registry.
add(generator_class)

Add the provided generator_class to the registry.

Parameters:generator_class – A subclass of devilry.devilry_report.abstract_generator.AbstractReportGenerator.
Raises:ValueError – When a generator class with the same devilry.devilry_report.abstract_generator.AbstractReportGenerator.get_generator_type already exists in the registry.
remove(generator_type)

Remove a generator class with the provided generator_type from the registry.

Parameters:generator_type (str) – A devilry.devilry_report.abstract_generator.AbstractReportGenerator.get_generator_type.
Raises:ValueError – If the generator_type does not exist in the registry.
class devilry.devilry_report.generator_registry.MockableRegistry

Bases: devilry.devilry_report.generator_registry.Registry

A non-singleton version of Registry. For tests.

classmethod make_mockregistry(*generator_classes)

Shortcut for making a mock registry.

Typical usage in a test:

from django import test
from unittest import mock
from devilry.devilry_report.generator import AbstractReportGenerator
from devilry.devilry_report import generator_registry

class TestSomething(test.TestCase):

    def test_something(self):
        class Mock1(AbstractReportGenerator):
            @classmethod get_generator_type(cls):
                return 'mock1'

        class Mock2(AbstractReportGenerator):
            @classmethod get_generator_type(cls):
                return 'mock2'

        mockregistry = generator_registry.MockableRegistry.make_mockregistry(
            Mock1, Mock2)

        with mock.patch('devilry.devilry_report.generator_registry.Registry.get_instance',
                        lambda: mockregistry):
            pass  # Your test code here
Parameters:*generator_classes – Zero or more devilry.devilry_report.abstract_generator.AbstractReportGenerator subclasses.
Returns:An object of this class with the requested generator_classes registered.

Simple example of the basic usage

1. Create your generator class

In some app of your choice, subclass the AbstractReportGenerator-class and override the required methods. See the class for which methods to override.

2. Register the generator-class

Register you generator in the apps.py-file:

class SomeDevilryAppConfig(AppConfig):
name = 'devilry.devilry_admin'
verbose_name = 'Devilry admin'

def ready(self):
    from devilry.devilry_report import generator_registry as report_generator_registry
    from devilry.devilry_someapp.path.to.generator import MyGenerator

    report_generator_registry.Registry.get_instance().add(
        generator_class=MyGenerator
    )

3. Implementing support for starting the report generation and download

To implement support for generating and downloading the report, you need to provide valid data for the report download view. The view for generating and downloading a report is the same view, but generation is triggered via the HTTP POST method, and download via HTTP GET.

To generate a report, we need to add support for posting to the generate/download view. This view requires a JSON-blob defining as a minimum what generator-type to use.

1. Add the generate/download view to your urls. By default, the DownloadReportView only accepts a download by the user that created the report. If you need any extra permission-checks, you will need to subclass the view. The view can be added to a urls.py file, or an CrAdmin-app. We’ll use a CrAdmin-app for this example:

DownloadReportView.as_view(),
name='download_report')

2. Add a JSON-blob that can be posted to the DownloadReportView. This is usually added in the get_context_data-method of the view that supports generating a report:

def get_context_data(self, **kwargs):
    ...
    context['report_options'] = json.dumps({
        'generator_type': '<generatortype (defined by get_generator_type on you generator-class)>',
        'generator_options': {// You can provide data here if you generator-class requires it, else leave this empty}
    })
    return context
  1. Add a form for posting the report_options data in the template:

    <form action="{% cradmin_appurl 'download_report' %}" method="POST">
        {% csrf_token %}
        <input type="hidden" name="report_options" value="{{ report_options }}">
        <input class="btn btn-primary" type="submit" name="confirm" value="{% trans "Download results" %}"/>
    </form>