Skip to content

give admin user a preview of sponsorship package revenue splits #2529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions sponsors/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -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"<span title='{name}' style='background-color:var(--{colors[i]})'>{pct_str}</span>")
# 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"<div style='{style}'>{''.join(spans)}</div>"
return mark_safe(html)

get_benefit_split.short_description = "Revenue split"


class SponsorContactInline(admin.TabularInline):
model = SponsorContact
Expand Down
12 changes: 12 additions & 0 deletions sponsors/models/sponsorship.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
17 changes: 17 additions & 0 deletions sponsors/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import date, timedelta
import random

from django.core.cache import cache
from django.db import IntegrityError
Expand Down Expand Up @@ -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)
Expand Down