From e1e47ea823914eb12ee0e6181ad0c8b0f76fa1d2 Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Mon, 7 Oct 2024 12:15:28 +0200
Subject: [PATCH] add organization-affiliated profile email export

add view permission checks for data export
---
 .../organizations/_organization_card.html     |  7 +-
 scipost_django/organizations/urls.py          |  5 +
 scipost_django/organizations/views.py         | 93 ++++++++++++++++++-
 3 files changed, 98 insertions(+), 7 deletions(-)

diff --git a/scipost_django/organizations/templates/organizations/_organization_card.html b/scipost_django/organizations/templates/organizations/_organization_card.html
index 8d3988944..e1715d7c7 100644
--- a/scipost_django/organizations/templates/organizations/_organization_card.html
+++ b/scipost_django/organizations/templates/organizations/_organization_card.html
@@ -135,9 +135,10 @@
              aria-labelledby="publications-{{ org.id }}-tab">
             
           {% if perms.can_manage_organizations %}
-            <div class="row float-end">
-              <a href="{% url "organizations:download_associated_publications_tex" pk=org.id %}">Download associated publications as LaTeX</a>
-            </div>
+            <ul class="p-0 list-unstyled float-end">
+              <li><a href="{% url "organizations:download_associated_publications_tex" pk=org.id %}">Download associated publications as LaTeX</a></li>
+              <li><a href="#" hx-swap="outerHTML" hx-get="{% url "organizations:_hx_export_associated_profile_emails" pk=org.id %}">Export associated profile emails</a></li>
+            </ul>
           {% endif %}
           <h3>
             {% if org.cf_nr_associated_publications %}{{ org.cf_nr_associated_publications }}{% else %}No{% endif %} 
diff --git a/scipost_django/organizations/urls.py b/scipost_django/organizations/urls.py
index ba2c9d21b..fe9373766 100644
--- a/scipost_django/organizations/urls.py
+++ b/scipost_django/organizations/urls.py
@@ -39,6 +39,11 @@ urlpatterns = [
         views.download_associated_publications_tex,
         name="download_associated_publications_tex",
     ),
+    path(
+        "<int:pk>/_hx_associated_profile_emails",
+        views._hx_export_associated_profile_emails,
+        name="_hx_export_associated_profile_emails",
+    ),
     path(
         "get_organization_detail",
         views.get_organization_detail,
diff --git a/scipost_django/organizations/views.py b/scipost_django/organizations/views.py
index 2838fd09a..f023f0203 100644
--- a/scipost_django/organizations/views.py
+++ b/scipost_django/organizations/views.py
@@ -7,15 +7,17 @@ from django.contrib import messages
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.mixins import UserPassesTestMixin
 from django.core.exceptions import PermissionDenied
-from django.http import QueryDict
+from django.http import HttpResponse
+from django.db.models.functions import Lower
 from django.template.response import TemplateResponse
 from django.urls import reverse_lazy
 from django.db import transaction
-from django.db.models import Q, Exists, OuterRef, Prefetch
+from django.db.models import Q, Count, Exists, OuterRef
 from django.shortcuts import get_object_or_404, render, redirect
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.html import format_html
+from django.utils.timezone import timedelta
 from django.views.generic.detail import DetailView
 from django.views.generic.edit import CreateView, UpdateView, DeleteView
 from django.views.generic.list import ListView
@@ -23,8 +25,9 @@ from django.views.generic.list import ListView
 from dal import autocomplete
 from guardian.decorators import permission_required
 
-from finances.constants import SUBSIDY_WITHDRAWN
-from finances.models.subsidy import Subsidy
+from journals.models.publication import PublicationAuthorsTable
+from profiles.models import Profile, ProfileEmail
+from submissions.models.submission import SubmissionAuthorProfile
 
 from .constants import (
     ORGTYPE_PRIVATE_BENEFACTOR,
@@ -301,6 +304,87 @@ def get_organization_detail(request):
     return redirect(reverse("organizations:organizations"))
 
 
+@permission_required_htmx("scipost.can_manage_organizations")
+def _hx_export_associated_profile_emails(request, pk):
+    """
+    Export a list of associated profile emails.
+    The list includes all non-deprecated emails of profiles that have:
+    - accepted (unsolicited) SciPost emails
+    - been active in the last year (logged in, published, submitted, or reported)
+    - are affiliated with the organization (via direct declaration or publication/submission metadata)
+    """
+    organization = get_object_or_404(Organization, pk=pk)
+
+    year_ago = timezone.now() - timedelta(days=365)
+    recently_active_profiles = (
+        Profile.objects.all()
+        .annotate(
+            nr_publications=Count(
+                "publicationauthorstable",
+                distinct=True,
+                filter=Q(
+                    publicationauthorstable__publication__publication_date__gte=year_ago
+                ),
+            ),
+            nr_submissions=Count(
+                "contributor__submissions",
+                distinct=True,
+                filter=Q(contributor__submissions__submission_date__gte=year_ago),
+            ),
+            nr_reports=Count(
+                "contributor__reports",
+                distinct=True,
+                filter=Q(contributor__reports__created__gte=year_ago),
+            ),
+            has_published_with_organization=Exists(
+                PublicationAuthorsTable.objects.filter(
+                    profile=OuterRef("pk"),
+                    affiliations__organization=organization,
+                )
+            ),
+            has_submitted_with_organization=Exists(
+                SubmissionAuthorProfile.objects.filter(
+                    profile=OuterRef("pk"),
+                    affiliations__organization=organization,
+                )
+            ),
+            logged_in=Q(contributor__user__last_login__gte=year_ago),
+        )
+        .filter(
+            Q(accepts_SciPost_emails=True)
+            & (
+                Q(nr_publications__gt=0)
+                | Q(nr_submissions__gt=0)
+                | Q(nr_reports__gt=0)
+                | Q(logged_in=True)
+            )
+            & (
+                Q(affiliations__organization=organization)
+                | Q(has_published_with_organization=True)
+                | Q(has_submitted_with_organization=True)
+            ),
+        )
+        .order_by()
+        .distinct()
+    )
+
+    # Remove duplicate emails before exporting
+    profile_emails = (
+        ProfileEmail.objects.filter(
+            profile__in=recently_active_profiles, still_valid=True
+        )
+        .annotate(lower_email=Lower("email"))
+        .values_list("lower_email", flat=True)
+        .order_by("lower_email")
+        .distinct()
+    )
+
+    return HttpResponse(
+        f'<textarea rows="5" style="width: 300px;">{", ".join(profile_emails)}</textarea>'
+    )
+
+
+@permission_required_htmx("scipost.can_manage_organizations")
 def download_associated_publications_tex(request, pk):
     """
     Download a .tex file with the associated publications of an organization.
@@ -320,6 +404,7 @@ def download_associated_publications_tex(request, pk):
     return response
 
 
+@permission_required_htmx("scipost.can_manage_organizations")
 def download_associated_authors_tex(request, pk):
     """
     Download a .tex file with the associated authors of an organization.
-- 
GitLab