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)¶
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.
- property 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¶
- exception 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)¶
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)¶
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¶
Registry of devilry_report generator types. Holds exactly one subclass of
devilry.devilry_report.abstract_generator.AbstractReportGenerator
for each generator_type.Ensures there is only one instance created. Make sure to use super() in subclasses.
- get(generator_type)¶
Get a subclass of
devilry.devilry_report.abstract_generator.AbstractReportGenerator
stored in the registry by thegenerator_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¶
A non-singleton version of
Registry
. For tests.Ensures there is only one instance created. Make sure to use super() in subclasses.
- 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
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>