From ce1c3910d94554ca9c734ad8e5798d098fdb0786 Mon Sep 17 00:00:00 2001 From: Joe Carey Date: Thu, 12 Sep 2024 09:07:08 -0600 Subject: [PATCH 1/2] give admin user a preview of sponsorship package revenue splits (#2529) * Give the admin an indication of how revenue for sponsorships in this package will be divvied up * implement Ee's feedback --- sponsors/admin.py | 29 +++++++++++++++++++++++++++-- sponsors/models/sponsorship.py | 12 ++++++++++++ sponsors/tests/test_models.py | 17 +++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) 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) From aefbaa1c5462dad1f9ab13e2868bab0786630190 Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Thu, 12 Sep 2024 11:20:18 -0400 Subject: [PATCH 2/2] Fix missing change from #2529 (#2531) A small, hard to notice change was present in my suggestion that wasn't picked up. --- sponsors/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sponsors/admin.py b/sponsors/admin.py index 251384370..e16cffbc6 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -212,7 +212,7 @@ def get_benefit_split(self, obj: SponsorshipPackage) -> str: for i, (name, pct) in enumerate(split): pct_str = f"{pct:.0f}%" widths.append(pct_str) - spans.append(f"{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