diff --git a/dandiapi/api/templates/dashboard/base.html b/dandiapi/api/templates/dashboard/base.html
index 16e353803..3cbceb8be 100644
--- a/dandiapi/api/templates/dashboard/base.html
+++ b/dandiapi/api/templates/dashboard/base.html
@@ -16,6 +16,7 @@
{{ site_header|default:_('D
Dashboard Home
Users
Admin
+ Mailchimp CSV
{% if title %} › {{ title }}{% endif %}
{% endblock %}
diff --git a/dandiapi/api/views/__init__.py b/dandiapi/api/views/__init__.py
index 7254ba465..4e7b90cdc 100644
--- a/dandiapi/api/views/__init__.py
+++ b/dandiapi/api/views/__init__.py
@@ -3,7 +3,7 @@
from .asset import AssetViewSet, NestedAssetViewSet
from .auth import auth_token_view, authorize_view, user_questionnaire_form_view
from .dandiset import DandisetViewSet
-from .dashboard import DashboardView, user_approval_view
+from .dashboard import DashboardView, mailchimp_csv_view, user_approval_view
from .info import info_view
from .root import root_content_view
from .stats import stats_view
@@ -25,6 +25,7 @@
'authorize_view',
'auth_token_view',
'blob_read_view',
+ 'mailchimp_csv_view',
'upload_initialize_view',
'upload_complete_view',
'upload_validate_view',
diff --git a/dandiapi/api/views/dashboard.py b/dandiapi/api/views/dashboard.py
index 6a595e086..4f567cbce 100644
--- a/dandiapi/api/views/dashboard.py
+++ b/dandiapi/api/views/dashboard.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import csv
from typing import TYPE_CHECKING
from django.conf import settings
@@ -7,7 +8,7 @@
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Exists, OuterRef
-from django.http import HttpRequest, HttpResponseRedirect
+from django.http import HttpRequest, HttpResponseRedirect, StreamingHttpResponse
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_http_methods
from django.views.generic.base import TemplateView
@@ -17,6 +18,8 @@
from dandiapi.api.views.users import social_account_to_dict
if TYPE_CHECKING:
+ from collections.abc import Iterator
+
from allauth.socialaccount.models import SocialAccount
@@ -82,6 +85,40 @@ def _users(self):
)
+def mailchimp_csv_view(request: HttpRequest) -> StreamingHttpResponse:
+ """Generate a Mailchimp-compatible CSV file of all active users."""
+ # Exclude the django-guardian anonymous user account.
+ users = User.objects.filter(metadata__status=UserMetadata.Status.APPROVED).exclude(
+ username='AnonymousUser'
+ )
+
+ fieldnames = ['email', 'first_name', 'last_name']
+ data = users.values(*fieldnames).iterator()
+
+ def streaming_output() -> Iterator[str]:
+ """Stream out the header and CSV rows (for consumption by streaming response)."""
+
+ # This class implements a filelike's write() interface to provide a way
+ # for the CSV writer to "return" the CSV lines as strings.
+ class Echo:
+ def write(self, value):
+ return value
+
+ # Yield back the rows of the CSV file.
+ writer = csv.DictWriter(Echo(), fieldnames=fieldnames)
+ yield writer.writeheader()
+ for row in data:
+ yield writer.writerow(row)
+
+ return StreamingHttpResponse(
+ streaming_output(),
+ content_type='text/csv',
+ headers={
+ 'Content-Disposition': 'attachment; filename="dandi_users_mailchimp.csv"',
+ },
+ )
+
+
@require_http_methods(['GET', 'POST'])
def user_approval_view(request: HttpRequest, username: str):
# Redirect user to login if they're not authenticated
diff --git a/dandiapi/urls.py b/dandiapi/urls.py
index 32267b165..55eb92632 100644
--- a/dandiapi/urls.py
+++ b/dandiapi/urls.py
@@ -18,6 +18,7 @@
authorize_view,
blob_read_view,
info_view,
+ mailchimp_csv_view,
root_content_view,
stats_view,
upload_complete_view,
@@ -105,6 +106,7 @@ def to_url(self, value):
path('admin/', admin.site.urls),
path('dashboard/', DashboardView.as_view(), name='dashboard-index'),
path('dashboard/user//', user_approval_view, name='user-approval'),
+ path('dashboard/mailchimp/', mailchimp_csv_view, name='mailchimp-csv'),
# this url overrides the authorize url in oauth2_provider.urls to
# support our user signup workflow
re_path(r'^oauth/authorize/$', authorize_view, name='authorize'),