Skip to content

Commit

Permalink
✨ [#4930] Add custom admin view for submission export view
Browse files Browse the repository at this point in the history
  • Loading branch information
sergei-maertens committed Dec 20, 2024
1 parent 9c6c899 commit e0ff975
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 2 deletions.
14 changes: 14 additions & 0 deletions src/openforms/forms/admin/form_statistics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.contrib import admin
from django.urls import path

from ..models import FormStatistics
from .views import ExportSubmissionStatisticsView


@admin.register(FormStatistics)
Expand Down Expand Up @@ -31,3 +33,15 @@ def has_delete_permission(self, request, obj=None):

def has_change_permission(self, request, obj=None):
return False

def get_urls(self):
urls = super().get_urls()
export_view = self.admin_site.admin_view(
ExportSubmissionStatisticsView.as_view(
media=self.media,
) # pyright: ignore[reportArgumentType]
)
custom_urls = [
path("export/", export_view, name="formstatistics_export"),
]
return custom_urls + urls
39 changes: 38 additions & 1 deletion src/openforms/forms/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@

from django import forms
from django.contrib import messages
from django.contrib.admin.helpers import AdminField
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.postgres.forms import SimpleArrayField
from django.http import FileResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic.edit import FormView
Expand All @@ -18,8 +21,9 @@

from openforms.logging import logevent

from ..forms import ExportStatisticsForm
from ..forms.form import FormImportForm
from ..models.form import Form, FormsExport
from ..models import Form, FormsExport, FormStatistics
from ..utils import import_form
from .tasks import process_forms_export, process_forms_import

Expand Down Expand Up @@ -109,3 +113,36 @@ def _bulk_import_forms(self, import_file):
filename = private_media_storage.save(name, import_file)

process_forms_import.delay(filename, self.request.user.id)


@method_decorator(staff_member_required, name="dispatch")
class ExportSubmissionStatisticsView(
LoginRequiredMixin, PermissionRequiredMixin, FormView
):
permission_required = "forms.view_formstatistics"
template_name = "admin/forms/formstatistics/export_form.html"
form_class = ExportStatisticsForm

# must be set by the ModelAdmin
media: forms.Media | None = None

def get_context_data(self, **kwargs):
assert (
self.media is not None
), "You must pass media=self.media in the model admin"
context = super().get_context_data(**kwargs)

form = context["form"]

def form_fields():
for name in form.fields:
yield AdminField(form, name, is_first=False)

context.update(
{
"opts": FormStatistics._meta,
"media": self.media + form.media,
"form_fields": form_fields,
}
)
return context
5 changes: 4 additions & 1 deletion src/openforms/forms/forms/__init__.py
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"]
52 changes: 52 additions & 0 deletions src/openforms/forms/forms/form_statistics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 ..models import Form


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."
),
)
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 %}
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>
&rsaquo; <a href="{% url 'admin:forms_formstatistics_changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% 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 %}
59 changes: 59 additions & 0 deletions src/openforms/forms/tests/admin/test_form_statistics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _

from django_webtest import WebTest
from maykin_2fa.test import disable_admin_mfa

from openforms.accounts.tests.factories import UserFactory


@disable_admin_mfa()
class FormStatisticsExportAdminTests(WebTest):

admin_url = reverse_lazy("admin:formstatistics_export")

def test_access_control_no_access(self):
# various flavours of users do not have access, only if the right permissions
# are set are you allowed in
invalid_users = (
(
"plain user",
UserFactory.create(),
302,
),
(
"staff user without perms",
UserFactory.create(is_staff=True),
403,
),
(
"user with perms no staff",
UserFactory.create(
is_staff=False, user_permissions=["forms.view_formstatistics"]
),
302,
),
)

for label, user, expected_status in invalid_users:
with self.subTest(label, expected_status=expected_status):
response = self.app.get(
self.admin_url,
user=user,
auto_follow=False,
status=expected_status,
)

self.assertEqual(response.status_code, expected_status)

def test_navigate_from_changelist(self):
user = UserFactory.create(
is_staff=True, user_permissions=["forms.view_formstatistics"]
)
changelist = self.app.get(
reverse("admin:forms_formstatistics_changelist"), user=user
)

export_page = changelist.click(_("Export submission statistics"))

self.assertEqual(export_page.request.path, self.admin_url)

0 comments on commit e0ff975

Please sign in to comment.