Skip to content

Commit

Permalink
Added button to export admin list as CSV (#1381)
Browse files Browse the repository at this point in the history
* Added button to export admin list as CSV

* Removed un-needed code

* Moved export button to top of page. Set timestamp as part of filename. Now have a line per app rather than per user

* Fixed broken test

* Bumped dependencies
  • Loading branch information
jamesstottmoj authored Nov 7, 2024
1 parent e7bf0a1 commit 45a0b1f
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 15 deletions.
21 changes: 12 additions & 9 deletions controlpanel/frontend/jinja2/webapp-admin-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@

{% block content %}
{% set num_apps = apps|length %}
<h1 class="govuk-heading-xl">{{ page_title }}</h1>
<h1 class="govuk-heading-xl">{{ page_title }}</h1>

<h2 class="govuk-heading-l">Export app admins</h2>
<p class="govuk-body">Export a list of app admins to a CSV file.</p>

<form action="{{ url("app-admin-csv") }}" method="post">
{{ csrf_input }}
<button class="govuk-button js-confirm"
data-confirm-message="Are you sure you want to export a list of app admins?">
Export Admins
</button>
</form>

{% if request.user.has_perm('api.list_app') %}
{{ app_list(apps, request.user) }}

{% if request.user.has_perm('api.create_app') %}
<p class="govuk-body">
<a class="govuk-button" href="{{ url('create-app') }}">
Register an app
</a>
</p>
{% endif %}
{% endif %}
{% endblock %}
1 change: 1 addition & 0 deletions controlpanel/frontend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
path("webapp-data/", views.WebappBucketList.as_view(), name="list-webapp-datasources"),
path("webapps/", views.AppList.as_view(), name="list-apps"),
path("webapps/all/", views.AdminAppList.as_view(), name="list-all-apps"),
path("webapps/all/admin-csv", views.AppAdminCSV.as_view(), name="app-admin-csv"),
path("webapps/new/", views.CreateApp.as_view(), name="create-app"),
path("webapps/<int:pk>/", views.AppDetail.as_view(), name="manage-app"),
path("webapps/<int:pk>/delete/", views.DeleteApp.as_view(), name="delete-app"),
Expand Down
1 change: 1 addition & 0 deletions controlpanel/frontend/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
AddAdmin,
AddCustomers,
AdminAppList,
AppAdminCSV,
AppDetail,
AppList,
CreateApp,
Expand Down
42 changes: 39 additions & 3 deletions controlpanel/frontend/views/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Standard library
import csv
from datetime import datetime
from typing import List

# Third-party
Expand All @@ -8,13 +10,15 @@
from auth0.rest import Auth0Error
from django.conf import settings
from django.contrib import messages
from django.db.models import Prefetch
from django.http import HttpResponseRedirect
from django.contrib.postgres.aggregates import StringAgg
from django.db.models import F, Prefetch
from django.db.models.functions import Coalesce
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import pluralize
from django.urls import reverse_lazy
from django.utils.http import urlencode
from django.views.generic.base import RedirectView
from django.views.generic.base import RedirectView, View
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, FormMixin, UpdateView
from django.views.generic.list import ListView
Expand Down Expand Up @@ -74,6 +78,38 @@ def get_queryset(self):
)


class AppAdminCSV(OIDCLoginRequiredMixin, PermissionRequiredMixin, View):
permission_required = "api.is_superuser"

def post(self, request, *args, **kwargs):
apps = (
App.objects.filter(userapps__is_admin=True)
.annotate(
users=StringAgg("userapps__user__username", delimiter=", "),
emails=StringAgg(
Coalesce("userapps__user__justice_email", "userapps__user__email"),
delimiter=", ",
),
)
.values("name", "users", "emails")
.order_by("name")
)

timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")

response = HttpResponse(
content_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="app_admins_{timestamp}.csv"'},
)

writer = csv.writer(response)
writer.writerow(["App Name", "Admins", "Emails"])
for app in apps:
writer.writerow([app["name"], app["users"], app["emails"]])

return response


class AppDetail(OIDCLoginRequiredMixin, PermissionRequiredMixin, DetailView):
context_object_name = "app"
model = App
Expand Down
2 changes: 1 addition & 1 deletion requirements.dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ black==24.10.0
django-debug-toolbar==4.4.6
flake8==7.0.0
ipdb==0.13.13
ipython==8.27.0
ipython==8.29.0
isort==5.13.2
pandas==2.2.0
pre-commit==3.7.1
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ beautifulsoup4==4.12.3
boto3==1.35.39
botocore==1.35.44 # noqa temporarily pinning this to try to resolve an issue with failing tests
celery[sqs]==5.3.6
channels==4.0.0
channels==4.1.0
channels-redis==4.2.0
daphne==4.1.2
Django==5.1.2
Expand Down Expand Up @@ -34,7 +34,7 @@ python-dotenv==1.0.1
python-jose==3.3.0
pyyaml==6.0.1
rules==3.3
sentry-sdk==2.5.1
sentry-sdk==2.17.0
slackclient==2.9.4
urllib3==2.2.3
uvicorn[standard]==0.28.0
15 changes: 15 additions & 0 deletions tests/frontend/views/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ def list_all(client, *args):
return client.get(reverse("list-all-apps"))


def admin_csv(client, app, *args):
return client.post(reverse("app-admin-csv"))


def detail(client, app, *args):
return client.get(reverse("manage-app", kwargs={"pk": app.id}))

Expand Down Expand Up @@ -299,6 +303,9 @@ def set_textract(client, app, *args):
(list_all, "superuser", status.HTTP_200_OK),
(list_all, "app_admin", status.HTTP_403_FORBIDDEN),
(list_all, "normal_user", status.HTTP_403_FORBIDDEN),
(admin_csv, "superuser", status.HTTP_200_OK),
(admin_csv, "app_admin", status.HTTP_403_FORBIDDEN),
(admin_csv, "normal_user", status.HTTP_403_FORBIDDEN),
(detail, "superuser", status.HTTP_200_OK),
(detail, "app_admin", status.HTTP_200_OK),
(detail, "normal_user", status.HTTP_403_FORBIDDEN),
Expand Down Expand Up @@ -352,6 +359,14 @@ def test_permissions(
assert response.status_code == expected_status


def test_admin_csv(client, app, users):
client.force_login(users["superuser"])
response = admin_csv(client, app)
content = response.content.decode("utf-8")
assert "App Name,Admins,Emails" in content
assert app.name in content


def disconnect_bucket(client, apps3bucket, *args, **kwargs):
return client.post(reverse("revoke-app-access", kwargs={"pk": apps3bucket.id}))

Expand Down

0 comments on commit 45a0b1f

Please sign in to comment.