From 8ed4099a4880d8559a27e67f46f8b11f8e0fe441 Mon Sep 17 00:00:00 2001 From: Sylvain Le Bon <sylvain@startinblox.com> Date: Sat, 25 May 2024 13:10:17 +0200 Subject: [PATCH 1/4] feature: csv export in admin --- djangoldp/admin.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/djangoldp/admin.py b/djangoldp/admin.py index 9211d407..a8136c97 100644 --- a/djangoldp/admin.py +++ b/djangoldp/admin.py @@ -1,6 +1,8 @@ +from csv import DictWriter from django.contrib import admin -from guardian.admin import GuardedModelAdmin from django.contrib.auth.admin import UserAdmin +from django.http import HttpResponse +from guardian.admin import GuardedModelAdmin from djangoldp.models import Activity, ScheduledActivity, Follower from djangoldp.activities.services import ActivityQueueService @@ -10,10 +12,21 @@ class DjangoLDPAdmin(GuardedModelAdmin): An admin model representing a federated object. Inherits from GuardedModelAdmin to provide Django-Guardian object-level permissions ''' - pass + actions = ['export_csv'] + + @admin.action(description="Export CSV") + def export_csv(self, request, queryset): + response = HttpResponse(content_type="text/csv") + response['Content-Disposition'] = f'attachment; filename="{self.model.__name__}.csv"' + headers = {field.name:field.verbose_name for field in self.model._meta.fields if field.name in self.list_display} + + writer = DictWriter(response, fieldnames=headers.keys()) + writer.writerow(headers) + writer.writerows(queryset.values(*headers.keys())) + return response -class DjangoLDPUserAdmin(UserAdmin, GuardedModelAdmin): +class DjangoLDPUserAdmin(UserAdmin, DjangoLDPAdmin): '''An extension of UserAdmin providing the functionality of DjangoLDPAdmin''' list_display = ('urlid', 'email', 'first_name', 'last_name', 'date_joined', 'last_login', 'is_staff') -- GitLab From bd7d2311108b9e4c69ba46134d66f0681269ad8c Mon Sep 17 00:00:00 2001 From: Sylvain Le Bon <sylvain@startinblox.com> Date: Sat, 25 May 2024 17:54:52 +0200 Subject: [PATCH 2/4] feature: configurable csv export --- djangoldp/admin.py | 10 +++++++++- docs/create_model.md | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/djangoldp/admin.py b/djangoldp/admin.py index a8136c97..e7542d4a 100644 --- a/djangoldp/admin.py +++ b/djangoldp/admin.py @@ -13,12 +13,20 @@ class DjangoLDPAdmin(GuardedModelAdmin): object-level permissions ''' actions = ['export_csv'] + export_fields = [] + + def resolve_verbose_name(self, field_path): + field = self + for field_name in field_path.split('__'): + field = field.model._meta.get_field(field_name) + return field.verbose_name @admin.action(description="Export CSV") def export_csv(self, request, queryset): response = HttpResponse(content_type="text/csv") response['Content-Disposition'] = f'attachment; filename="{self.model.__name__}.csv"' - headers = {field.name:field.verbose_name for field in self.model._meta.fields if field.name in self.list_display} + field_list = self.export_fields or self.list_display + headers = {field:self.resolve_verbose_name(field) for field in field_list} writer = DictWriter(response, fieldnames=headers.keys()) writer.writerow(headers) diff --git a/docs/create_model.md b/docs/create_model.md index 92c0aec7..9297ba52 100644 --- a/docs/create_model.md +++ b/docs/create_model.md @@ -217,6 +217,15 @@ LDPUser.circles = lambda self: Circle.objects.filter(members__user=self) LDPUser.circles.field = DynamicNestedField(Circle, 'circles') ``` +### Configuring CSV export + +DjangoLDP automaticallly provides CSV export on the admin site. By default, it exports the columns given in the `list_display` attribute. This can be overridden with the attribute `export_fields`. This setting can include fields of related object using `__`. + +```python +class CustomAdmin(DjangoLDPAdmin): + export_fields = ['email', 'account__slug'] +``` + ### Improving Performance On certain endpoints, you may find that you only need a subset of fields on a model, and serializing them all is expensive (e.g. if I only need the `name` and `id` of each group chat, then why serialize all of their members?). To optimise the fields serialized, you can pass a custom header in the request, `Accept-Model-Fields`, with a `list` value of desired fields e.g. `['@id', 'name']` -- GitLab From dc436ff4a179d5f0f0a8048c54ee034a01701321 Mon Sep 17 00:00:00 2001 From: Sylvain Le Bon <sylvain@startinblox.com> Date: Sat, 25 May 2024 17:59:30 +0200 Subject: [PATCH 3/4] syntax: use the field_list variable --- djangoldp/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangoldp/admin.py b/djangoldp/admin.py index e7542d4a..7f296379 100644 --- a/djangoldp/admin.py +++ b/djangoldp/admin.py @@ -28,9 +28,9 @@ class DjangoLDPAdmin(GuardedModelAdmin): field_list = self.export_fields or self.list_display headers = {field:self.resolve_verbose_name(field) for field in field_list} - writer = DictWriter(response, fieldnames=headers.keys()) + writer = DictWriter(response, fieldnames=field_list) writer.writerow(headers) - writer.writerows(queryset.values(*headers.keys())) + writer.writerows(queryset.values(*field_list)) return response -- GitLab From 15b7b3efe8f0355ce9e099fa39abf4900507e333 Mon Sep 17 00:00:00 2001 From: Sylvain Le Bon <sylvain@startinblox.com> Date: Mon, 27 May 2024 11:34:54 +0200 Subject: [PATCH 4/4] bugfix: exclude fields that can't be resolved --- djangoldp/admin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/djangoldp/admin.py b/djangoldp/admin.py index 7f296379..673eb8c7 100644 --- a/djangoldp/admin.py +++ b/djangoldp/admin.py @@ -1,6 +1,7 @@ from csv import DictWriter from django.contrib import admin from django.contrib.auth.admin import UserAdmin +from django.core.exceptions import FieldDoesNotExist from django.http import HttpResponse from guardian.admin import GuardedModelAdmin from djangoldp.models import Activity, ScheduledActivity, Follower @@ -18,14 +19,18 @@ class DjangoLDPAdmin(GuardedModelAdmin): def resolve_verbose_name(self, field_path): field = self for field_name in field_path.split('__'): - field = field.model._meta.get_field(field_name) + try: + field = field.model._meta.get_field(field_name) + except FieldDoesNotExist: + return None return field.verbose_name @admin.action(description="Export CSV") def export_csv(self, request, queryset): response = HttpResponse(content_type="text/csv") response['Content-Disposition'] = f'attachment; filename="{self.model.__name__}.csv"' - field_list = self.export_fields or self.list_display + # only keep fields that can be resolved, keep only urlid if none + field_list = list(filter(self.resolve_verbose_name, self.export_fields or self.list_display)) or ['urlid'] headers = {field:self.resolve_verbose_name(field) for field in field_list} writer = DictWriter(response, fieldnames=field_list) -- GitLab