-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable exporting (registered) submission metadata #4962
Open
sergei-maertens
wants to merge
4
commits into
master
Choose a base branch
from
feature/4930-bulk-export-submission-stats
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+506
−4
Open
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
e0ff975
:sparkles: [#4930] Add custom admin view for submission export view
sergei-maertens 33b6e2e
:sparkles: [#4930] Implement the actual export
sergei-maertens 10f5300
:white_check_mark: [#4930] Add test for export view
sergei-maertens 5627a76
:white_check_mark: [#4930] Test the export form implementation
sergei-maertens File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
from .form_definition import FormDefinitionForm # noqa | ||
from .form_definition import FormDefinitionForm | ||
from .form_statistics import ExportStatisticsForm | ||
|
||
__all__ = ["FormDefinitionForm", "ExportStatisticsForm"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from __future__ import annotations | ||
|
||
from datetime import date | ||
|
||
from django import forms | ||
from django.contrib.admin.widgets import AdminDateWidget | ||
from django.utils import timezone | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from dateutil.relativedelta import relativedelta | ||
from tablib import Dataset | ||
|
||
from ..models import Form | ||
from ..statistics import export_registration_statistics | ||
|
||
|
||
def get_first_of_current_month() -> date: | ||
now = timezone.now() | ||
return now.replace(day=1).date() | ||
|
||
|
||
def get_first_of_next_month() -> date: | ||
now = timezone.now() | ||
one_month_later = now + relativedelta(months=1) | ||
return one_month_later.replace(day=1).date() | ||
|
||
|
||
class ExportStatisticsForm(forms.Form): | ||
start_date = forms.DateField( | ||
label=_("From"), | ||
required=True, | ||
initial=get_first_of_current_month, | ||
help_text=_( | ||
"Export form submission that were submitted on or after this date." | ||
), | ||
widget=AdminDateWidget, | ||
) | ||
end_date = forms.DateField( | ||
label=_("Until"), | ||
required=True, | ||
initial=get_first_of_next_month, | ||
help_text=_("Export form submission that were submitted before this date."), | ||
widget=AdminDateWidget, | ||
) | ||
limit_to_forms = forms.ModelMultipleChoiceField( | ||
label=_("Forms"), | ||
required=False, | ||
queryset=Form.objects.filter(_is_deleted=False), | ||
help_text=_( | ||
"Limit the export to the selected forms, if specified. Leave the field " | ||
"empty to export all forms. Hold CTRL (or COMMAND on Mac) to select " | ||
"multiple options." | ||
), | ||
) | ||
|
||
def export(self) -> Dataset: | ||
start_date: date = self.cleaned_data["start_date"] | ||
end_date: date = self.cleaned_data["end_date"] | ||
return export_registration_statistics( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a bit picky, but why do you define the dates as variables? Is it (only) to specify the date typing? |
||
start_date, | ||
end_date, | ||
self.cleaned_data["limit_to_forms"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
from datetime import date, datetime, time | ||
|
||
from django.contrib.contenttypes.models import ContentType | ||
from django.db import models | ||
from django.utils.timezone import make_aware | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from tablib import Dataset | ||
|
||
from openforms.logging import logevent | ||
from openforms.logging.models import TimelineLogProxy | ||
from openforms.submissions.models import Submission | ||
|
||
from .models import Form | ||
|
||
|
||
def export_registration_statistics( | ||
start_date: date, | ||
end_date: date, | ||
limit_to_forms: models.QuerySet[Form] | None = None, | ||
) -> Dataset: | ||
dataset = Dataset( | ||
headers=( | ||
_("Public reference"), | ||
_("Form name (public)"), | ||
_("Form name (internal)"), | ||
_("Submitted on"), | ||
_("Registered on"), | ||
), | ||
title=_("Successfully registered submissions between {start} and {end}").format( | ||
start=start_date.isoformat(), | ||
end=end_date.isoformat(), | ||
), | ||
) | ||
|
||
_start_date = make_aware(datetime.combine(start_date, time.min)) | ||
_end_date = make_aware(datetime.combine(end_date, time.max)) | ||
|
||
log_records = TimelineLogProxy.objects.filter( | ||
content_type=ContentType.objects.get_for_model(Submission), | ||
timestamp__gte=_start_date, | ||
timestamp__lt=_end_date, | ||
# see openforms.logging.logevent for the data structure of the extra_data | ||
# JSONField | ||
extra_data__log_event=logevent.REGISTRATION_SUCCESS_EVENT, | ||
).order_by("timestamp") | ||
|
||
if limit_to_forms: | ||
form_ids = list(limit_to_forms.values_list("pk", flat=True)) | ||
log_records = log_records.filter(extra_data__form_id__in=form_ids) | ||
|
||
for record in log_records.iterator(): | ||
extra_data = record.extra_data | ||
# GFKs will be broken when the submissions are pruned, so prefer extracting | ||
# information from the extra_data snapshot | ||
submission: Submission | None = record.content_object | ||
dataset.append( | ||
( | ||
# public reference | ||
extra_data.get( | ||
"public_reference", | ||
( | ||
submission.public_registration_reference | ||
if submission | ||
else "-unknown-" | ||
), | ||
), | ||
# public form name | ||
extra_data.get( | ||
"form_name", submission.form.name if submission else "-unknown-" | ||
), | ||
# internal form name | ||
extra_data.get( | ||
"internal_form_name", | ||
submission.form.internal_name if submission else "-unknown-", | ||
), | ||
# when the user submitted the form | ||
extra_data.get( | ||
"submitted_on", | ||
submission.completed_on.isoformat() if submission else None, | ||
), | ||
# when the registration succeeeded - this must be close to when it was logged | ||
record.timestamp.isoformat(), | ||
) | ||
) | ||
|
||
return dataset |
11 changes: 11 additions & 0 deletions
11
src/openforms/forms/templates/admin/forms/formstatistics/change_list_object_tools.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{% extends "admin/change_list_object_tools.html" %} | ||
{% load i18n %} | ||
|
||
{% block object-tools-items %} | ||
<li> | ||
<a href="{% url 'admin:formstatistics_export' %}" class="viewlink"> | ||
{% trans "Export submission statistics" %} | ||
</a> | ||
</li> | ||
{{ block.super }} | ||
{% endblock %} |
66 changes: 66 additions & 0 deletions
66
src/openforms/forms/templates/admin/forms/formstatistics/export_form.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
{% extends "admin/base_site.html" %} | ||
{% load static i18n django_admin_index %} | ||
|
||
{% block extrahead %}{{ block.super }} | ||
<script src="{% url 'admin:jsi18n' %}"></script> | ||
{{ media }} | ||
{% endblock %} | ||
|
||
{% block extrastyle %}{{ block.super }} | ||
<link rel="stylesheet" href="{% static "admin/css/forms.css" %}"> | ||
<link rel="stylesheet" href="{% static "admin/css/admin-index.css" %}">{% endblock %} | ||
|
||
{% block nav-global %}{% include "django_admin_index/includes/app_list.html" %}{% endblock nav-global %} | ||
|
||
{% block title %} {% trans "Export submission statistics" %} {{ block.super }} {% endblock %} | ||
|
||
{% block breadcrumbs %} | ||
<div class="breadcrumbs"> | ||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> | ||
› <a href="{% url 'admin:forms_formstatistics_changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a> | ||
› {% trans 'Export submission statistics' %} | ||
</div> | ||
{% endblock %} | ||
|
||
{% block content %} | ||
<h1>{% trans 'Export submission statistics' %}</h1> | ||
|
||
<div id="content-main"> | ||
<form action="." method="post"> | ||
{% csrf_token %} | ||
|
||
<fieldset class="module aligned"> | ||
<div class="description">{% blocktrans trimmed %} | ||
<p>Here you can create an export of successfully registered form submissions. The | ||
export file contains the following columns: public reference, form name, | ||
form internal name, the submission datetime and the timestamp of registration.</p> | ||
|
||
<p>You can use the filters below to limit the result set in the export.</p> | ||
{% endblocktrans %}</div> | ||
|
||
{# TODO: doesn't handle checkboxes, see admin/includes/fieldset.html for when this is necessary #} | ||
{% for field in form_fields %} | ||
<div class="form-row {% if field.errors %}errors{% endif %}"> | ||
{{ field.errors }} | ||
<div> | ||
<div class="flex-container {% if field.errors %}errors{% endif %}"> | ||
{{ field.label_tag }} | ||
{{ field.field }} | ||
</div> | ||
</div> | ||
|
||
{% if field.field.help_text %} | ||
<div class="help" {% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}> | ||
<div>{{ field.field.help_text|safe }}</div> | ||
</div> | ||
{% endif %} | ||
</div> | ||
{% endfor %} | ||
</fieldset> | ||
|
||
<div class="submit-row"> | ||
<input type="submit" class="default" value="{% trans 'Export' %}"> | ||
</div> | ||
</form> | ||
</div> | ||
{% endblock %} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be nice to add the form names, if the limit is used