From a93ca214826b9db8bfd48f53cb3d8da1e5bcd5bf Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Wed, 3 Apr 2024 17:05:44 +0200
Subject: [PATCH] refactor sponsors page with yearly coverage

---
 scipost_django/organizations/managers.py      |  47 +++++
 .../templates/sponsors/_sponsor_card.html     |   4 +-
 .../sponsors/templates/sponsors/sponsors.html | 172 ++++++++++--------
 scipost_django/sponsors/views.py              |  32 ++--
 4 files changed, 158 insertions(+), 97 deletions(-)

diff --git a/scipost_django/organizations/managers.py b/scipost_django/organizations/managers.py
index e4c5725cd..a8c96d0f7 100644
--- a/scipost_django/organizations/managers.py
+++ b/scipost_django/organizations/managers.py
@@ -5,8 +5,10 @@ __license__ = "AGPL v3"
 import datetime
 
 from django.db import models
+from django.db.models import Q
 
 from finances.constants import SUBSIDY_PROMISED, SUBSIDY_INVOICED, SUBSIDY_RECEIVED
+from finances.models.subsidy import Subsidy
 
 
 class OrganizationQuerySet(models.QuerySet):
@@ -62,3 +64,48 @@ class OrganizationQuerySet(models.QuerySet):
             .annotate(total=models.Sum("subsidy__amount"))
             .order_by("-total")
         )
+
+    def order_by_yearly_coverage(self, year_start=None, year_end=None):
+        """
+        Order by average yearly coverage between year_start and year_end.
+        If either year_start or year_end is None, assume interminable coverage of that end.
+        """
+        subsidy_filter = Q(organization=models.OuterRef("pk"))
+
+        # If year_start and year_end are both None, no filtering is needed.
+        # If year_start is None, filter such that date_until <= year_end.
+        # If year_end is None, filter such that year_start <= date_from.
+        # If both are specified, filter such that
+        # date_from <= year_start <= year_end <= date_until.
+        match (year_start, year_end):
+            case (None, None):
+                pass
+            case (None, _):
+                subsidy_filter &= Q(date_until__year__lte=year_end)
+            case (_, None):
+                subsidy_filter &= Q(date_from__year__gte=year_start)
+            case (_, _):
+                subsidy_filter &= Q(date_from__year__lte=year_start)
+                subsidy_filter &= Q(date_until__year__gte=year_end)
+
+        return (
+            self.annotate(
+                total_yearly_coverage=models.Subquery(
+                    Subsidy.objects.obtained()
+                    .filter(subsidy_filter)
+                    .annotate(
+                        yearly_coverage=models.F("amount")
+                        / (
+                            1
+                            + models.F("date_until__year")
+                            - models.F("date_from__year")
+                        )
+                    )
+                    .values("organization")
+                    .annotate(total=models.Sum("yearly_coverage"))
+                    .values("total")
+                )
+            )
+            .order_by(models.F("total_yearly_coverage").desc(nulls_last=True))
+            .distinct()
+        )
diff --git a/scipost_django/sponsors/templates/sponsors/_sponsor_card.html b/scipost_django/sponsors/templates/sponsors/_sponsor_card.html
index 0104fee73..cba410c3f 100644
--- a/scipost_django/sponsors/templates/sponsors/_sponsor_card.html
+++ b/scipost_django/sponsors/templates/sponsors/_sponsor_card.html
@@ -6,9 +6,9 @@
      hx-push-url="true"
      hx-target="body">
 
-  <div class="bg-white m-2 h-100 d-flex" style="min-height: 100px;">
+  <div class="bg-white m-2 h-100 d-flex">
 
-    <img class="m-auto p-4 {{ sponsor.css_class }}" style="max-height: 16rem; max-width: 100%; object-fit: contain" 
+    <img class="m-auto p-4 w-100 {{ sponsor.css_class }}" style="max-height: 200px; object-fit: contain" 
       {% if sponsor.logo %} src="{{ sponsor.logo.url }}" {% else %} src="{% static 'organizations/no_logo.jpg' %}" {% endif %}
        alt="{{ sponsor.name }} logo" />
     </div>
diff --git a/scipost_django/sponsors/templates/sponsors/sponsors.html b/scipost_django/sponsors/templates/sponsors/sponsors.html
index c9ef4fc0a..7bbbf7d17 100644
--- a/scipost_django/sponsors/templates/sponsors/sponsors.html
+++ b/scipost_django/sponsors/templates/sponsors/sponsors.html
@@ -2,14 +2,17 @@
 
 {% load render_bundle from webpack_loader %}
 
-{% block meta_description %}{{ block.super }} Sponsors{% endblock meta_description %}
-{% block pagetitle %}: Sponsors{% endblock pagetitle %}
+{% block meta_description %}
+  {{ block.super }} Sponsors
+{% endblock meta_description %}
+
+{% block pagetitle %}
+  : Sponsors
+{% endblock pagetitle %}
 
 {% load static %}
 
-{% block breadcrumb_items %}
-  {{ block.super }}
-{% endblock %}
+{% block breadcrumb_items %}{{ block.super }}{% endblock %}
 
 {% block content %}
 
@@ -22,16 +25,18 @@
   <div class="row">
     <div class="col-12">
       <h4>
-	<strong>We cordially invite organizations worldwide to join our Sponsorship scheme, and make <a href="{% url 'scipost:about' %}#GOA">Genuine Open Access</a> a reality.</strong>
+        <strong>We cordially invite organizations worldwide to join our Sponsorship scheme, and make <a href="{% url 'scipost:about' %}#GOA">Genuine Open Access</a> a reality.</strong>
       </h4>
-      <br/>
+      <br />
       <p>
-	Is your organization benefitting from SciPost's activities (check our <a href="{% url 'organizations:organizations' %}">organizations page</a>), and does it not appear in our list of Sponsors below? Then consider helping SciPost:
-	<br/>
-	<strong>Are you a scientist?</strong><br/>
-	Please petition your local librarian/director/... to consider sponsoring us. You can use this email <a href="mailto:?subject=Petition to support SciPost&body={% autoescape on %}{% include 'sponsors/sponsor_petition_email.html' %}{% endautoescape %}&cc=sponsors@{{ request.get_host }}">template</a>.
-	<strong>Are you a librarian, funding agency representative or other potential supporter?</strong><br/>
-	Take a look at our <a href="{% static 'sponsors/SciPost_Sponsorship_Agreement.pdf' %}">Sponsorship Agreement</a> template, and contact us at <a href="mailto:sponsors@{{ request.get_host }}?subject=Sponsors enquiry">sponsors@{{ request.get_host }}</a> to enquire about further details or initiate your sponsorship.
+        Is your organization benefitting from SciPost's activities (check our <a href="{% url 'organizations:organizations' %}">organizations page</a>), and does it not appear in our list of Sponsors below? Then consider helping SciPost:
+        <br />
+        <strong>Are you a scientist?</strong>
+        <br />
+        Please petition your local librarian/director/... to consider sponsoring us. You can use this email <a href="mailto:?subject=Petition to support SciPost&body={% autoescape on %}{% include 'sponsors/sponsor_petition_email.html' %}{% endautoescape %}&cc=sponsors@{{ request.get_host }}">template</a>.
+        <strong>Are you a librarian, funding agency representative or other potential supporter?</strong>
+        <br />
+        Take a look at our <a href="{% static 'sponsors/SciPost_Sponsorship_Agreement.pdf' %}">Sponsorship Agreement</a> template, and contact us at <a href="mailto:sponsors@{{ request.get_host }}?subject=Sponsors enquiry">sponsors@{{ request.get_host }}</a> to enquire about further details or initiate your sponsorship.
       </p>
     </div>
   </div>
@@ -39,99 +44,106 @@
   <div class="row">
     <div class="col-md-4">
       <div class="card">
-	<div class="card-header">
-	  <h3>Community service; no user fees</h3>
-	</div>
-	<div class="card-body">
-	  <p>SciPost does not charge any subscription fees or article processing charges (APCs): all our operations are performed as a community service, with no user-facing charges;</p>
-	  <p>Our initiative's scope is resolutely international: we do not impose any geographical or institutional restrictions on the delivery of our services;</p>
-	  <p>SciPost is dedicated to serving the academic community, with no further competing interests.</p>
-	</div>
+        <div class="card-header">
+          <h3>Community service; no user fees</h3>
+        </div>
+        <div class="card-body">
+          <p>
+            SciPost does not charge any subscription fees or article processing charges (APCs): all our operations are performed as a community service, with no user-facing charges;
+          </p>
+          <p>
+            Our initiative's scope is resolutely international: we do not impose any geographical or institutional restrictions on the delivery of our services;
+          </p>
+          <p>SciPost is dedicated to serving the academic community, with no further competing interests.</p>
+        </div>
       </div>
     </div>
     <div class="col-md-4">
       <div class="card">
-	<div class="card-header">
-	  <h3>Quality through Openness</h3>
-	</div>
-	<div class="card-body">
-	  <p>Our sharp focus on openness through our <a href="/FAQ#pwr">peer-witnessed refereeing</a> procedure equips us with arguably the most stringent editorial quality control system available;</p>
-	  <p>Our <a href="{% url 'scipost:about' %}#editorial_college_physics">Editorial College</a> is composed of a broad selection of top academics;</p>
-	  <p>Our fully professional publishing services meet or surpass best practices in all respects. Our flagship journal SciPost Physics has been awarded the <a href="https://doaj.org">DOAJ</a> Seal.</p>
-	</div>
+        <div class="card-header">
+          <h3>Quality through Openness</h3>
+        </div>
+        <div class="card-body">
+          <p>
+            Our sharp focus on openness through our <a href="/FAQ#pwr">peer-witnessed refereeing</a> procedure equips us with arguably the most stringent editorial quality control system available;
+          </p>
+          <p>
+            Our <a href="{% url 'scipost:about' %}#editorial_college_physics">Editorial College</a> is composed of a broad selection of top academics;
+          </p>
+          <p>
+            Our fully professional publishing services meet or surpass best practices in all respects. Our flagship journal SciPost Physics has been awarded the <a href="https://doaj.org">DOAJ</a> Seal.
+          </p>
+        </div>
       </div>
     </div>
     <div class="col-md-4">
       <div class="card">
-	<div class="card-header">
-	  <h3>Community funded</h3>
-	</div>
-	<div class="card-body">
-	  <p>Our operations are obviously not without cost, but our strictly not-for-profit setup, community-led workflow, streamlined infrastructure and no-frills administration mean that the average per-publication costs are much lower than those of competing services;</p>
-	  <p>Our financing model relies on sponsorship from the organizations which benefit from our activities (see our <a href="{% url 'organizations:organizations' %}">organizations page</a>);</p>
-	  <p>All sponsorship funds are pooled and exclusively used to run our infrastructure and services.</p>
-	</div>
+        <div class="card-header">
+          <h3>Community funded</h3>
+        </div>
+        <div class="card-body">
+          <p>
+            Our operations are obviously not without cost, but our strictly not-for-profit setup, community-led workflow, streamlined infrastructure and no-frills administration mean that the average per-publication costs are much lower than those of competing services;
+          </p>
+          <p>
+            Our financing model relies on sponsorship from the organizations which benefit from our activities (see our <a href="{% url 'organizations:organizations' %}">organizations page</a>);
+          </p>
+          <p>All sponsorship funds are pooled and exclusively used to run our infrastructure and services.</p>
+        </div>
       </div>
     </div>
   </div>
 
   <div class="row">
     <div class="col-12">
-      <h3>We aim to establish a healthier <a href="{% url 'finances:business_model' %}" target="_blank">business model</a> for scientific publishing</h3>
-      <p>We are able to run a fully sustainable infrastructure at an estimated cost of under &euro;400 per publication, much below the current norm for APCs. The more scientists shift their publishing to SciPost, the fewer subscription/article processing charges you will have to pay as an organization. Your sponsorship will help us scale up and make our initiative sustainable.</p>
+      <h3>
+        We aim to establish a healthier <a href="{% url 'finances:business_model' %}" target="_blank">business model</a> for scientific publishing
+      </h3>
+      <p>
+        We are able to run a fully sustainable infrastructure at an estimated cost of under &euro;400 per publication, much below the current norm for APCs. The more scientists shift their publishing to SciPost, the fewer subscription/article processing charges you will have to pay as an organization. Your sponsorship will help us scale up and make our initiative sustainable.
+      </p>
     </div>
   </div>
 
   <div class="row" hx-boost="true">
     <div class="col-12">
-      <h1 class="highlight">Our current Sponsors</h1>
-
-      <h3 class="highlight">&euro;20k and above:</h3>
-      <div class="d-grid gap-3" style="grid-template-columns: repeat(3, minmax(0, 1fr));">
-	{% for sponsor in sponsors_20kplus %}
-	    {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
-	{% endfor %}
-      </div>
-
-      <h3 class="highlight mt-4">&euro;10k and above:</h3>
-      <div class="d-grid gap-3" style="grid-template-columns: repeat(3, minmax(0, 1fr));">
-	{% for sponsor in sponsors_10kplus %}
-	    {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
-	{% endfor %}
-      </div>
+      <h1 class="highlight">Our recent Sponsors</h1>
 
-      <h3 class="highlight mt-4">&euro;5k and above:</h3>
-      <div class="d-grid gap-3" style="grid-template-columns: repeat(3, minmax(0, 1fr));">
-	{% for sponsor in sponsors_5kplus %}
-	    {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
-	{% endfor %}
-      </div>
-
-      <h3 class="highlight mt-4">Our other current Sponsors:</h3>
-      <div class="d-grid gap-3" style="grid-template-columns: repeat(3, minmax(0, 1fr));">
-	{% for sponsor in current_sponsors %}
-	    {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
-	{% endfor %}
+      <hgroup class="p-2 highlight d-flex align-items-center justify-content-between">
+        <h3 class="m-0">Current Sponsors</h3>
+      </hgroup>
+      <div class="d-grid gap-3"
+           style="grid-template-columns: repeat(3, minmax(0, 1fr))">
+        {% for sponsor in current_sponsors %}
+          {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
+        {% endfor %}
       </div>
 
-      <h1 class="highlight">Our recent-past Sponsors</h1>
-
-      <hgroup class="p-2 highlight d-flex align-items-center justify-content-between">
-        <h3 class="m-0"> 
-          Last year's Sponsors:
-        </h3> 
-        <span class="text-muted">
-          (excludes current sponsors)
-        </span>
+      <hgroup class="p-3 mt-4 highlight d-flex align-items-center justify-content-between">
+        <h3 class="m-0">Last year's Sponsors:</h3>
+        <span class="text-muted">(excludes current sponsors)</span>
       </hgroup>
-      <div class="d-grid gap-3" style="grid-template-columns: repeat(3, minmax(0, 1fr));">
-	{% for sponsor in last_year_sponsors %}
-	    {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
-	{% endfor %}
+      <div class="d-grid gap-3"
+           style="grid-template-columns: repeat(3, minmax(0, 1fr))">
+        {% for sponsor in last_year_sponsors %}
+          {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
+        {% endfor %}
       </div>
 
+      <details class="mt-4">
+        <summary class="highlight list-triangle p-3">
+          <h1 class="m-0">Our past Sponsors</h1>
+        </summary>
+
+        <div class="d-grid gap-3"
+             style="grid-template-columns: repeat(3, minmax(0, 1fr))">
+          {% for sponsor in past_sponsors %}
+            {% include 'sponsors/_sponsor_card.html' with sponsor=sponsor %}
+          {% endfor %}
+        </div>
+      </details>
+ 
     </div>
   </div>
 
-
 {% endblock content %}
diff --git a/scipost_django/sponsors/views.py b/scipost_django/sponsors/views.py
index b4684c6e6..3dde57110 100644
--- a/scipost_django/sponsors/views.py
+++ b/scipost_django/sponsors/views.py
@@ -9,26 +9,28 @@ from organizations.models import Organization
 
 
 def sponsors(request):
-    sponsors_20kplus = Organization.objects.with_subsidy_above_and_up_to(20000)
-    sponsors_10kplus = Organization.objects.with_subsidy_above_and_up_to(10000, 20000)
-    sponsors_5kplus = Organization.objects.with_subsidy_above_and_up_to(5000, 10000)
-    current_sponsors = (
-        Organization.objects.current_sponsors().with_subsidy_above_and_up_to(0, 5000)
-    )
+    year = datetime.date.today().year
+
+    current_sponsors = Organization.objects.current_sponsors()
     last_year_sponsors = (
         Organization.objects.all_sponsors()
         .filter(
-            subsidy__date_until__year__lte=datetime.date.today().year - 1,
-            subsidy__date_until__gt=datetime.date.today()
-            - datetime.timedelta(days=365),
+            subsidy__date_from__year__lte=year - 1,
+            subsidy__date_until__year__gte=year - 1,
         )
-        .exclude(pk__in=current_sponsors.values_list("pk", flat=True))
+        .exclude(id__in=current_sponsors.values_list("id", flat=True))
     )
+    past_sponsors = (
+        Organization.objects.all_sponsors()
+        .exclude(id__in=current_sponsors.values_list("id", flat=True))
+        .exclude(id__in=last_year_sponsors.values_list("id", flat=True))
+    )
+
     context = {
-        "sponsors_20kplus": sponsors_20kplus,
-        "sponsors_10kplus": sponsors_10kplus,
-        "sponsors_5kplus": sponsors_5kplus,
-        "current_sponsors": current_sponsors.order_by_total_amount_received(),
-        "last_year_sponsors": last_year_sponsors.order_by_total_amount_received(),
+        "current_sponsors": current_sponsors.order_by_yearly_coverage(year, year),
+        "last_year_sponsors": last_year_sponsors.order_by_yearly_coverage(
+            year - 1, year - 1
+        ),
+        "past_sponsors": past_sponsors.order_by_yearly_coverage(None, year - 2),
     }
     return render(request, "sponsors/sponsors.html", context)
-- 
GitLab