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'),