diff --git a/sponsors/admin.py b/sponsors/admin.py
index 9e271edec..251384370 100644
--- a/sponsors/admin.py
+++ b/sponsors/admin.py
@@ -110,6 +110,7 @@ class ProvidedFileAssetConfigurationInline(StackedPolymorphicInline.Child):
ProvidedFileAssetConfigurationInline,
]
+
@admin.register(SponsorshipBenefit)
class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin):
change_form_template = "sponsors/admin/sponsorshipbenefit_change_form.html"
@@ -179,12 +180,12 @@ def update_related_sponsorships(self, *args, **kwargs):
@admin.register(SponsorshipPackage)
class SponsorshipPackageAdmin(OrderedModelAdmin):
ordering = ("-year", "order",)
- list_display = ["name", "year", "advertise", "allow_a_la_carte", "move_up_down_links"]
+ list_display = ["name", "year", "advertise", "allow_a_la_carte", "get_benefit_split", "move_up_down_links"]
list_filter = ["advertise", "year", "allow_a_la_carte"]
search_fields = ["name"]
def get_readonly_fields(self, request, obj=None):
- readonly = []
+ readonly = ["get_benefit_split"]
if obj:
readonly.append("slug")
if not request.user.is_superuser:
@@ -196,6 +197,30 @@ def get_prepopulated_fields(self, request, obj=None):
return {'slug': ['name']}
return {}
+ def get_benefit_split(self, obj: SponsorshipPackage) -> str:
+ colors = [
+ "#ffde57", # Python Gold
+ "#4584b6", # Python Blue
+ "#646464", # Python Grey
+ ]
+ split = obj.get_default_revenue_split()
+ # rotate colors through our available palette
+ if len(split) > len(colors):
+ colors = colors * (1 + (len(split) // len(colors)))
+ # build some span elements to show the percentages and have the program name in the title (to show on hover)
+ widths, spans = [], []
+ for i, (name, pct) in enumerate(split):
+ pct_str = f"{pct:.0f}%"
+ widths.append(pct_str)
+ spans.append(f"{pct_str}")
+ # define a style that will show our span elements like a single horizontal stacked bar chart
+ style = f'color:#fff;text-align:center;cursor:pointer;display:grid;grid-template-columns:{" ".join(widths)}'
+ # wrap it all up and put a bow on it
+ html = f"
{''.join(spans)}
"
+ return mark_safe(html)
+
+ get_benefit_split.short_description = "Revenue split"
+
class SponsorContactInline(admin.TabularInline):
model = SponsorContact
diff --git a/sponsors/models/sponsorship.py b/sponsors/models/sponsorship.py
index 7443d4d2c..d230e91c3 100644
--- a/sponsors/models/sponsorship.py
+++ b/sponsors/models/sponsorship.py
@@ -117,6 +117,18 @@ def clone(self, year: int):
slug=self.slug, year=year, defaults=defaults
)
+ def get_default_revenue_split(self) -> list[tuple[str, float]]:
+ """
+ Give the admin an indication of how revenue for sponsorships in this package will be divvied up
+ """
+ values, key = {}, "program__name"
+ for benefit in self.benefits.values(key).annotate(amount=Sum("internal_value", default=0)).order_by("-amount"):
+ values[benefit[key]] = values.get(benefit[key], 0) + (benefit["amount"] or 0)
+ total = sum(values.values())
+ if not total:
+ return [] # nothing to split!
+ return [(k, round(v / total * 100, 3)) for k, v in values.items()]
+
class SponsorshipProgram(OrderedModel):
"""
diff --git a/sponsors/tests/test_models.py b/sponsors/tests/test_models.py
index 781e85c09..3566f0b08 100644
--- a/sponsors/tests/test_models.py
+++ b/sponsors/tests/test_models.py
@@ -1,4 +1,5 @@
from datetime import date, timedelta
+import random
from django.core.cache import cache
from django.db import IntegrityError
@@ -433,6 +434,22 @@ def test_clone_does_not_repeate_already_cloned_package(self):
self.assertFalse(created)
self.assertEqual(pkg_2023.pk, repeated_pkg_2023.pk)
+ def test_get_default_revenue_split(self):
+ benefits = baker.make(SponsorshipBenefit, internal_value=int(random.random() * 1000), _quantity=12)
+ program_names = set((b.program.name for b in benefits))
+ pkg1 = baker.make(SponsorshipPackage, year=2024, advertise=True, logo_dimension=300, benefits=benefits[:3])
+ pkg2 = baker.make(SponsorshipPackage, year=2024, advertise=True, logo_dimension=300, benefits=benefits[3:7])
+ pkg3 = baker.make(SponsorshipPackage, year=2024, advertise=True, logo_dimension=300, benefits=benefits[7:])
+ splits = [pkg.get_default_revenue_split() for pkg in (pkg1, pkg2, pkg3)]
+ split_names = set((name for split in splits for name, _ in split))
+ totals = [sum((pct for _, pct in split)) for split in splits]
+ # since the split percentages are rounded, they may not always total exactly 100.000
+ self.assertAlmostEqual(totals[0], 100, delta=0.1)
+ self.assertAlmostEqual(totals[1], 100, delta=0.1)
+ self.assertAlmostEqual(totals[2], 100, delta=0.1)
+ self.assertEqual(split_names, program_names)
+
+
class SponsorContactModelTests(TestCase):
def test_get_primary_contact_for_sponsor(self):
sponsor = baker.make(Sponsor)