Module for handling project reporting functionality in the Hypha application.
This module provides views and utilities for managing project reports, including
creating, viewing, updating, and administering reports. It implements access control,
form handling, and notification systems for the reporting workflow.
Dependencies:
- Django (including django-filters, django-htmx, django-tables2)
- Hypha application modules (activity, projects, stream_forms, users, utils)
ReportingMixin
Mixin that ensures a project has a report configuration.
If a project is in progress but doesn't have a report_config,
this mixin creates one before proceeding with the view.
dispatch
dispatch(*args, **kwargs)
Ensure project has a report configuration if it's in progress.
Parameters:
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
HttpResponse
–
The response from the parent class's dispatch method.
Source code in hypha/apply/projects/reports/views.py
| def dispatch(self, *args, **kwargs):
"""
Ensure project has a report configuration if it's in progress.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponse: The response from the parent class's dispatch method.
"""
project = self.get_object()
if project.is_in_progress:
if not hasattr(project, "report_config"):
ReportConfig.objects.create(project=project)
return super().dispatch(*args, **kwargs)
|
ReportAccessMixin
Bases: UserPassesTestMixin
Mixin that controls access to report-related views.
Allows access to staff members, finance users, and the project owner.
model
class-attribute
instance-attribute
permission_denied_message
class-attribute
instance-attribute
permission_denied_message = gettext('You do not have permission to access this report.')
test_func
Test whether the current user has access to the report.
Returns:
-
bool
–
bool | None: True if user has permission to view the report, False otherwise.
Source code in hypha/apply/projects/reports/views.py
| def test_func(self) -> bool:
"""
Test whether the current user has access to the report.
Returns:
bool | None: True if user has permission to view the report, False otherwise.
"""
return has_object_permission(
"view_report", self.request.user, self.get_object()
)
|
ReportDetailView
Bases: DetailView
View for displaying the details of a report.
model
class-attribute
instance-attribute
template_name
class-attribute
instance-attribute
template_name = 'reports/report_detail.html'
permission_denied_message
class-attribute
instance-attribute
permission_denied_message = gettext('You do not have permission to access this report.')
dispatch
dispatch(*args, **kwargs)
Source code in hypha/apply/projects/reports/views.py
| def dispatch(self, *args, **kwargs):
report = self.get_object()
if not has_object_permission("view_report", self.request.user, report):
raise PermissionDenied(self.permission_denied_message)
return super().dispatch(*args, **kwargs)
|
ReportUpdateView
Bases: BaseStreamForm
, UpdateView
View for updating a report.
This view handles both creating new reports and editing existing ones.
It supports draft saving and manages form field population from existing data.
model
class-attribute
instance-attribute
object
class-attribute
instance-attribute
template_name
class-attribute
instance-attribute
template_name = 'reports/report_form.html'
permission_denied_message
class-attribute
instance-attribute
permission_denied_message = gettext('You do not have permission to update this report.')
wagtail_reference_index_ignore
class-attribute
instance-attribute
wagtail_reference_index_ignore = True
dispatch
dispatch(request, *args, **kwargs)
Set up the report object and check permissions before proceeding.
Parameters:
-
request
–
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
HttpResponse
–
The response from the parent class's dispatch method.
Raises:
-
PermissionDenied
–
If user doesn't have 'report_update' permission.
Source code in hypha/apply/projects/reports/views.py
| def dispatch(self, request, *args, **kwargs):
"""
Set up the report object and check permissions before proceeding.
Args:
request: The HttpRequest object.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponse: The response from the parent class's dispatch method.
Raises:
PermissionDenied: If user doesn't have 'report_update' permission.
"""
report = self.get_object()
self.object = report
if not has_object_permission("update_report", self.request.user, report):
raise PermissionDenied(self.permission_denied_message)
# super().dispatch calls get_context_data() which calls the rest to get the form fully ready for use.
return super().dispatch(request, *args, **kwargs)
|
get_context_data
get_context_data(*args, **kwargs)
Prepare the context data for the template.
Django note: super().dispatch calls get_context_data.
Parameters:
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
dict
–
The context data dictionary.
Source code in hypha/apply/projects/reports/views.py
| def get_context_data(self, *args, **kwargs):
"""
Prepare the context data for the template.
Django note: super().dispatch calls get_context_data.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
dict: The context data dictionary.
"""
# Is this where we need to get the associated form fields? Not in the form itself but up here? Yes. But in a
# roundabout way: get_form (here) gets fields and calls get_form_class (here) which calls get_form_fields
# (super) which sets up the fields in the returned form.
form = self.get_form()
context_data = {
"form": form,
"object": self.object,
"report_form": True
if self.object.project.submission.page.specific.report_forms.first()
else False,
**kwargs,
}
return context_data
|
get_form(form_class=None, draft=False)
Return an instance of the form to be used in this view.
Handles setting up form fields based on the report configuration or previous data.
Parameters:
-
form_class
–
The form class to use, if not using the default.
-
draft
–
Boolean indicating if this is a draft form.
Returns:
-
Form
–
An instance of the form to be used.
Source code in hypha/apply/projects/reports/views.py
| def get_form(self, form_class=None, draft=False):
"""
Return an instance of the form to be used in this view.
Handles setting up form fields based on the report configuration or previous data.
Args:
form_class: The form class to use, if not using the default.
draft: Boolean indicating if this is a draft form.
Returns:
Form: An instance of the form to be used.
"""
if self.object.current is None or self.object.form_fields is None:
# Here is where we get the form_fields, the ProjectReportForm associated with the Fund:
report_form = (
self.object.project.submission.page.specific.report_forms.first()
)
if report_form:
self.form_fields = report_form.form.form_fields
else:
self.form_fields = {}
else:
self.form_fields = self.object.form_fields
if form_class is None:
form_class = self.get_form_class(draft=draft)
report_instance = form_class(**self.get_form_kwargs())
return report_instance
|
get_initial
Get initial data for the form.
Populates the form with existing data from draft or current report version.
Handles file fields specially to properly display them.
Returns:
-
dict
–
Initial data for the form.
Source code in hypha/apply/projects/reports/views.py
| def get_initial(self):
"""
Get initial data for the form.
Populates the form with existing data from draft or current report version.
Handles file fields specially to properly display them.
Returns:
dict: Initial data for the form.
"""
initial = {}
if self.object.draft:
current = self.object.draft
else:
current = self.object.current
# current here is a ReportVersion which should already have the data associated.
if current:
# The following allows existing data to populate the form. This code was inspired by (aka copied from)
# ProjectFormEditView.get_paf_form_kwargs().
initial = current.raw_data
# Is the following needed to see the file in a friendly URL? Does not appear so. But needed to not blow up.
for field_id in current.file_field_ids:
initial.pop(field_id + "-uploads", False)
initial[field_id] = get_placeholder_file(current.raw_data.get(field_id))
return initial
|
Get the keyword arguments for instantiating the form.
Adds the current user to the form kwargs.
Returns:
-
dict
( dict
) –
The keyword arguments for the form.
Source code in hypha/apply/projects/reports/views.py
| def get_form_kwargs(self) -> dict:
"""
Get the keyword arguments for instantiating the form.
Adds the current user to the form kwargs.
Returns:
dict: The keyword arguments for the form.
"""
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
|
post
post(request, *args, **kwargs)
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
Handles saving drafts, form validation, and sending notifications.
Parameters:
-
request
–
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
HttpResponse
–
Redirect to success URL or redisplay form if invalid.
Source code in hypha/apply/projects/reports/views.py
| def post(self, request, *args, **kwargs):
"""
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
Handles saving drafts, form validation, and sending notifications.
Args:
request: The HttpRequest object.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponse: Redirect to success URL or redisplay form if invalid.
"""
save_draft = "save" in request.POST # clicked on save button?
form = self.get_form(draft=save_draft)
if form.is_valid():
form.save(form_fields=self.form_fields)
form.delete_temporary_files()
should_notify = True
if self.object.draft:
# It was a draft submission
should_notify = False
else:
if self.object.submitted != self.object.current.submitted:
# It was a staff edit - post submission
should_notify = False
if should_notify:
messenger(
MESSAGES.SUBMIT_REPORT,
request=self.request,
user=self.request.user,
source=self.object.project,
related=self.object,
)
response = HttpResponseRedirect(self.get_success_url())
else:
response = self.form_invalid(form)
return response
|
get_success_url
Source code in hypha/apply/projects/reports/views.py
| def get_success_url(self) -> str:
return self.object.project.get_absolute_url()
|
from_db
classmethod
from_db(db, field_names, values)
Deserialize form data when loading from database.
Parameters:
-
db
–
-
field_names
–
List of field names being loaded
-
values
–
Returns:
-
–
Instance with deserialized form data
Source code in hypha/apply/stream_forms/models.py
| @classmethod
def from_db(cls, db, field_names, values):
"""Deserialize form data when loading from database.
Args:
db: Database connection
field_names: List of field names being loaded
values: Values for the fields
Returns:
Instance with deserialized form data
"""
instance = super().from_db(db, field_names, values)
if "form_data" in field_names:
instance.form_data = cls.deserialize_form_data(
instance, instance.form_data, instance.form_fields
)
return instance
|
deserialize_form_data(instance, form_data, form_fields)
Convert stored form data back into Python objects.
Parameters:
-
instance
–
-
form_data
–
Raw form data from database
-
form_fields
–
Returns:
Source code in hypha/apply/stream_forms/models.py
| @classmethod
def deserialize_form_data(cls, instance, form_data, form_fields):
"""Convert stored form data back into Python objects.
Args:
instance: Form instance
form_data: Raw form data from database
form_fields: Form field definitions
Returns:
Deserialized form data
"""
data = form_data.copy()
# PERFORMANCE NOTE:
# Do not attempt to iterate over form_fields - that will fully instantiate the form_fields
# including any sub queries that they do
for _i, field_data in enumerate(form_fields.raw_data):
block = form_fields.stream_block.child_blocks[field_data["type"]]
field_id = field_data.get("id")
try:
value = data[field_id]
except KeyError:
pass
else:
data[field_id] = block.decode(value)
return data
|
get_defined_fields
Get the form field definitions.
Returns:
-
–
StreamField containing form field blocks
Raises:
-
AttributeError
–
If form_fields attribute is not defined on instance
Source code in hypha/apply/stream_forms/models.py
| def get_defined_fields(self):
"""Get the form field definitions.
Returns:
StreamField containing form field blocks
Raises:
AttributeError: If form_fields attribute is not defined on instance
"""
try:
return self.form_fields # type: ignore
except AttributeError as err:
raise AttributeError(
"form_fields attribute not found. "
"Make sure form_fields is defined on the implementing class."
) from err
|
get_form_fields(draft=False, form_data=None, user=None)
Generate form fields with applied logic and grouping.
Parameters:
-
draft
–
Whether this is a draft form. When True, fields that are not
marked as ApplicationMustIncludeFieldBlock will have their
required flag set to False, allowing incomplete form submissions
to be saved as drafts.
-
form_data
–
-
user
–
Returns:
-
–
OrderedDict of form fields
Source code in hypha/apply/stream_forms/models.py
| def get_form_fields(self, draft=False, form_data=None, user=None):
"""Generate form fields with applied logic and grouping.
Args:
draft: Whether this is a draft form. When True, fields that are not
marked as ApplicationMustIncludeFieldBlock will have their
required flag set to False, allowing incomplete form submissions
to be saved as drafts.
form_data: Existing form data
user: User completing the form
Returns:
OrderedDict of form fields
"""
if form_data is None:
form_data = {}
form_fields = OrderedDict()
field_blocks = self.get_defined_fields()
group_counter = 1
is_in_group = False
# If true option 1 is selected
grouped_fields_visible = False
for struct_child in field_blocks:
block = struct_child.block
struct_value = struct_child.value
if isinstance(block, FormFieldBlock):
field_from_block = block.get_field(struct_value)
disabled_help_text = _(
"You are logged in so this information is fetched from your user account."
)
if isinstance(block, FullNameBlock) and user and user.is_authenticated:
if user.full_name:
field_from_block.disabled = True
field_from_block.initial = user.full_name
field_from_block.help_text = disabled_help_text
else:
field_from_block.help_text = _(
"You are logged in but your user account does not have a "
"full name. We'll update your user account with the name you provide here."
)
if isinstance(block, EmailBlock) and user and user.is_authenticated:
field_from_block.disabled = True
field_from_block.initial = user.email
field_from_block.help_text = disabled_help_text
if draft and not issubclass(
block.__class__, ApplicationMustIncludeFieldBlock
):
field_from_block.required = False
field_from_block.group_number = group_counter if is_in_group else 1
if isinstance(block, GroupToggleBlock) and not is_in_group:
field_from_block.group_number = 1
field_from_block.grouper_for = group_counter + 1
group_counter += 1
is_in_group = True
grouped_fields_visible = (
form_data.get(struct_child.id) == field_from_block.choices[0][0]
)
if isinstance(block, TextFieldBlock):
field_from_block.word_limit = struct_value.get("word_limit")
if isinstance(block, MultiInputCharFieldBlock):
number_of_inputs = struct_value.get("number_of_inputs")
for index in range(number_of_inputs):
form_fields[struct_child.id + "_" + str(index)] = (
field_from_block
)
field_from_block.multi_input_id = struct_child.id
field_from_block.add_button_text = struct_value.get(
"add_button_text"
)
if (
index == number_of_inputs - 1
): # Add button after last input field
field_from_block.multi_input_add_button = True
# Index for field until which fields will be visible to applicant.
# Initially only the first field with id UUID_0 will be visible.
field_from_block.visibility_index = 0
field_from_block.max_index = index
if index != 0:
field_from_block.multi_input_field = True
field_from_block.required = False
field_from_block.initial = None
field_from_block = copy.copy(field_from_block)
else:
if is_in_group and not isinstance(block, GroupToggleBlock):
field_from_block.required_when_visible = (
field_from_block.required
)
field_from_block.required = (
field_from_block.required & grouped_fields_visible
)
field_from_block.visible = grouped_fields_visible
form_fields[struct_child.id] = field_from_block
elif isinstance(block, GroupToggleEndBlock):
# Group toggle end block is used only to group fields and not used in actual form.
# Todo: Use streamblock to create nested form field blocks, a more elegant method to group form fields.
is_in_group = False
else:
field_wrapper = BlockFieldWrapper(struct_child)
field_wrapper.group_number = group_counter if is_in_group else 1
form_fields[struct_child.id] = field_wrapper
return form_fields
|
get_form_class(draft=False, form_data=None, user=None)
Dynamically creates and returns a form class based on the field configuration.
Creates a new form class that inherits from submission_form_class (PageStreamBaseForm)
and includes all the dynamically generated form fields.
Parameters:
-
draft
–
Whether this is a draft form
-
form_data
–
Existing form data for pre-populating form fields
-
user
–
User completing the form, used for auto-populating user fields.
Returns:
-
–
A dynamically generated form class
Source code in hypha/apply/stream_forms/models.py
| def get_form_class(self, draft=False, form_data=None, user=None):
"""Dynamically creates and returns a form class based on the field configuration.
Creates a new form class that inherits from submission_form_class (PageStreamBaseForm)
and includes all the dynamically generated form fields.
Args:
draft: Whether this is a draft form
form_data: Existing form data for pre-populating form fields
user: User completing the form, used for auto-populating user fields.
Returns:
A dynamically generated form class
"""
return type(
"WagtailStreamForm",
(self.submission_form_class,),
self.get_form_fields(draft=draft, form_data=form_data, user=user),
)
|
Bases: ReportAccessMixin
, PrivateMediaView
View for handling private media files attached to reports.
Ensures proper access control and redirects users to the latest report version
if they try to access an outdated document.
permission_denied_message = gettext('You do not have permission to access this report.')
dispatch(*args, **kwargs)
Source code in hypha/apply/projects/reports/views.py
| def dispatch(self, *args, **kwargs):
report_pk = self.kwargs["pk"]
self.report = get_object_or_404(Report, pk=report_pk)
file_pk = kwargs.get("file_pk")
self.document = get_object_or_404(
ReportPrivateFiles.objects, report__report=self.report, pk=file_pk
)
if not hasattr(self.document.report, "live_for_report"):
# this is not a document in the live report
# send the user to the report page to see latest version
return redirect(self.report.get_absolute_url())
return super().dispatch(*args, **kwargs)
|
get_media(*args, **kwargs)
Source code in hypha/apply/projects/reports/views.py
| def get_media(self, *args, **kwargs):
return self.document.document
|
Source code in hypha/apply/utils/storage.py
| def get(self, *args, **kwargs):
file_to_serve = self.get_media(*args, **kwargs)
return FileResponse(file_to_serve)
|
Test whether the current user has access to the report.
Returns:
-
bool
–
bool | None: True if user has permission to view the report, False otherwise.
Source code in hypha/apply/projects/reports/views.py
| def test_func(self) -> bool:
"""
Test whether the current user has access to the report.
Returns:
bool | None: True if user has permission to view the report, False otherwise.
"""
return has_object_permission(
"view_report", self.request.user, self.get_object()
)
|
ReportSkipView
Bases: SingleObjectMixin
, View
View for marking a report as skipped.
Only staff can skip reports, and only unsubmitted reports that aren't
the current due report can be skipped.
model
class-attribute
instance-attribute
post
Handle POST requests to toggle the skipped status of a report.
Parameters:
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
HttpResponseClientRefresh
–
A response that refreshes the client.
Source code in hypha/apply/projects/reports/views.py
| def post(self, *args, **kwargs):
"""
Handle POST requests to toggle the skipped status of a report.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponseClientRefresh: A response that refreshes the client.
"""
report = self.get_object()
unsubmitted = not report.current
not_current = report.project.report_config.current_due_report() != report
if unsubmitted and not_current:
report.skipped = not report.skipped
report.save()
messenger(
MESSAGES.SKIPPED_REPORT,
request=self.request,
user=self.request.user,
source=report.project,
related=report,
)
return HttpResponseClientRefresh()
|
ReportFrequencyUpdate
Bases: View
View for updating the reporting frequency configuration for a project.
Allows staff to set when reports are due and to enable/disable reporting
for a project.
model
class-attribute
instance-attribute
template_name
class-attribute
instance-attribute
template_name = 'reports/modals/report_frequency_config.html'
permission_denied_message
class-attribute
instance-attribute
permission_denied_message = gettext('You do not have permission to update reporting configurations.')
dispatch
dispatch(request, *args, **kwargs)
Set up the project and report configuration objects.
Parameters:
-
request
–
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
HttpResponse
–
The response from the parent class's dispatch method.
Source code in hypha/apply/projects/reports/views.py
| def dispatch(self, request, *args, **kwargs):
"""
Set up the project and report configuration objects.
Args:
request: The HttpRequest object.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponse: The response from the parent class's dispatch method.
"""
self.project = get_object_or_404(Project, submission__id=kwargs.get("pk"))
if not has_object_permission(
"update_report_config", self.request.user, self.project
):
raise PermissionDenied(self.permission_denied_message)
self.object = self.project.report_config
return super().dispatch(request, *args, **kwargs)
|
get_due_report_data
Get data about the current due report for the project.
Returns:
-
dict
–
Data containing start date and project end date if reporting is enabled,
empty dict otherwise.
Source code in hypha/apply/projects/reports/views.py
| def get_due_report_data(self):
"""
Get data about the current due report for the project.
Returns:
dict: Data containing start date and project end date if reporting is enabled,
empty dict otherwise.
"""
report_data = {}
if not self.object.disable_reporting:
project_end_date = self.project.end_date
if self.object.current_due_report():
start_date = self.object.current_due_report().start_date
else:
start_date = self.object.last_report().start_date
report_data = {"startDate": start_date, "projectEndDate": project_end_date}
return report_data
|
get
Handle GET requests to display the form.
Parameters:
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
HttpResponse
–
Rendered template with form and context data.
Source code in hypha/apply/projects/reports/views.py
| def get(self, *args, **kwargs):
"""
Handle GET requests to display the form.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponse: Rendered template with form and context data.
"""
form = self.get_form()
report_data = self.get_due_report_data()
return render(
self.request,
self.template_name,
context={
"form": form,
"object": self.object,
"report_data": report_data,
},
)
|
get_form_kwargs(**kwargs)
Get the keyword arguments for instantiating the form.
Sets initial start date based on current reporting configuration.
Parameters:
-
**kwargs
–
Additional keyword arguments.
Returns:
-
dict
–
The keyword arguments for the form.
Source code in hypha/apply/projects/reports/views.py
| def get_form_kwargs(self, **kwargs):
"""
Get the keyword arguments for instantiating the form.
Sets initial start date based on current reporting configuration.
Args:
**kwargs: Additional keyword arguments.
Returns:
dict: The keyword arguments for the form.
"""
kwargs = kwargs or {}
kwargs["instance"] = self.object
if not self.object.disable_reporting:
# Current due report can be none for ONE_TIME(does not repeat),
# In case of ONE_TIME, either reporting is already completed(last_report exists)
# or there should be a current_due_report.
if self.object.current_due_report():
kwargs["initial"] = {
"start": self.object.current_due_report().end_date,
}
else:
kwargs["initial"] = {
"start": self.object.last_report().end_date,
}
else:
kwargs["initial"] = {
"start": self.project.start_date,
}
return kwargs
|
get_form(*args, **kwargs)
Source code in hypha/apply/projects/reports/views.py
| def get_form(self, *args, **kwargs):
if self.project.is_in_progress:
return self.form_class(*args, **(self.get_form_kwargs(**kwargs)))
return None
|
post
Handle POST requests to update reporting configuration.
Parameters:
-
*args
–
Variable length argument list.
-
**kwargs
–
Arbitrary keyword arguments.
Returns:
-
HttpResponse
–
Client refresh response or form with errors.
Source code in hypha/apply/projects/reports/views.py
| def post(self, *args, **kwargs):
"""
Handle POST requests to update reporting configuration.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponse: Client refresh response or form with errors.
"""
form = self.get_form(self.request.POST)
if form.is_valid():
if "disable-reporting" in self.request.POST:
form.instance.disable_reporting = True
form.instance.schedule_start = None
form.save()
messages.success(self.request, _("Reporting disabled"))
else:
form.instance.disable_reporting = False
form.instance.schedule_start = form.cleaned_data["start"]
form.save()
messenger(
MESSAGES.REPORT_FREQUENCY_CHANGED,
request=self.request,
user=self.request.user,
source=self.project,
related=self.object,
)
return HttpResponseClientRefresh()
report_data = self.get_due_report_data()
return render(
self.request,
self.template_name,
context={
"form": form,
"object": self.object,
"report_data": report_data,
},
)
|
ReportListView
Bases: SingleTableMixin
, FilterView
View for displaying a table of submitted reports.
Only accessible to staff and finance users.
queryset
class-attribute
instance-attribute
filterset_class
class-attribute
instance-attribute
table_class
class-attribute
instance-attribute
template_name
class-attribute
instance-attribute
template_name = 'reports/report_list.html'
ReportingView
Bases: SingleTableMixin
, FilterView
View for displaying a table of projects with reporting information.
Only accessible to staff and finance users.
queryset
class-attribute
instance-attribute
queryset = for_reporting_table()
filterset_class
class-attribute
instance-attribute
table_class
class-attribute
instance-attribute
template_name
class-attribute
instance-attribute
template_name = 'reports/reporting.html'