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