Skip to content

Commit de5afa0

Browse files
JacobCoffeeclaude
andauthored
allow white logos for pypi footer (#2979)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 362788f commit de5afa0

File tree

9 files changed

+179
-94
lines changed

9 files changed

+179
-94
lines changed

apps/sponsors/admin.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ class SponsorshipAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
559559
"get_sponsor_description",
560560
"get_sponsor_landing_page_url",
561561
"get_sponsor_web_logo",
562+
"get_sponsor_white_logo",
562563
"get_sponsor_print_logo",
563564
"get_sponsor_primary_phone",
564565
"get_sponsor_mailing_address",
@@ -630,6 +631,7 @@ def get_readonly_fields(self, request, obj):
630631
"get_sponsor_description",
631632
"get_sponsor_landing_page_url",
632633
"get_sponsor_web_logo",
634+
"get_sponsor_white_logo",
633635
"get_sponsor_print_logo",
634636
"get_sponsor_primary_phone",
635637
"get_sponsor_mailing_address",
@@ -749,6 +751,22 @@ def get_sponsor_web_logo(self, obj):
749751
context = Context({"img": img})
750752
return mark_safe(template.render(context)) # noqa: S308
751753

754+
@admin.display(description="White Logo")
755+
def get_sponsor_white_logo(self, obj):
756+
"""Render and return the sponsor's white logo as a thumbnail image."""
757+
img = obj.sponsor.white_logo
758+
if not img:
759+
return "---"
760+
if img.name and img.name.lower().endswith(".svg"):
761+
return format_html(
762+
'<img src="{}" style="max-width:150px;max-height:150px;background:#333"/>',
763+
img.url,
764+
)
765+
html = "{% load thumbnail %}{% thumbnail img '150x150' format='PNG' quality=100 as im %}<img src='{{ im.url}}' style='background:#333'/>{% endthumbnail %}"
766+
template = Template(html)
767+
context = Context({"img": img})
768+
return mark_safe(template.render(context)) # noqa: S308
769+
752770
@admin.display(description="Print Logo")
753771
def get_sponsor_print_logo(self, obj):
754772
"""Render and return the sponsor's print logo as a thumbnail image."""

apps/sponsors/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def get(self, request, *args, **kwargs):
5353
"level_order": sponsorship.package.order,
5454
"description": sponsor.description,
5555
"logo": sponsor.web_logo.url,
56+
"white_logo": sponsor.white_logo.url if sponsor.white_logo else None,
5657
"sponsor_url": sponsor.landing_page_url,
5758
"start_date": sponsorship.start_date,
5859
"end_date": sponsorship.end_date,

apps/sponsors/forms.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ class SponsorshipApplicationForm(forms.Form):
229229
help_text="For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than 256px",
230230
required=False,
231231
)
232+
white_logo = forms.ImageField(
233+
label="Sponsor white logo",
234+
help_text="For display on dark backgrounds (e.g. PyPI footer). Transparent PNG, smallest dimension no less than 256px",
235+
required=False,
236+
)
232237
print_logo = forms.FileField(
233238
label="Sponsor print logo",
234239
help_text="For printed materials, signage, and projection. SVG or EPS",
@@ -396,6 +401,7 @@ def save(self):
396401
landing_page_url=self.cleaned_data.get("landing_page_url", ""),
397402
twitter_handle=self.cleaned_data["twitter_handle"],
398403
linked_in_page_url=self.cleaned_data["linked_in_page_url"],
404+
white_logo=self.cleaned_data.get("white_logo"),
399405
print_logo=self.cleaned_data.get("print_logo"),
400406
country_of_incorporation=self.cleaned_data.get("country_of_incorporation", ""),
401407
state_of_incorporation=self.cleaned_data.get("state_of_incorporation", ""),
@@ -606,6 +612,11 @@ class SponsorUpdateForm(forms.ModelForm):
606612
help_text="For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than 256px",
607613
required=False,
608614
)
615+
white_logo = forms.ImageField(
616+
widget=forms.widgets.FileInput,
617+
help_text="For display on dark backgrounds (e.g. PyPI footer). Transparent PNG, smallest dimension no less than 256px",
618+
required=False,
619+
)
609620
print_logo = forms.FileField(
610621
widget=forms.widgets.FileInput,
611622
help_text="For printed materials, signage, and projection. SVG or EPS",
@@ -647,6 +658,7 @@ class Meta:
647658
"twitter_handle",
648659
"linked_in_page_url",
649660
"web_logo",
661+
"white_logo",
650662
"print_logo",
651663
"primary_phone",
652664
"mailing_address_line_1",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.11 on 2026-04-06 18:04
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("sponsors", "0103_alter_benefitfeature_polymorphic_ctype_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="sponsor",
14+
name="white_logo",
15+
field=models.ImageField(
16+
blank=True,
17+
help_text="For display on dark backgrounds (e.g. PyPI footer). Transparent PNG, smallest dimension no less than 256px",
18+
null=True,
19+
upload_to="sponsor_white_logos",
20+
verbose_name="White logo",
21+
),
22+
),
23+
]

apps/sponsors/models/sponsors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ class Sponsor(ContentManageable):
4949
help_text="For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than "
5050
"256px",
5151
)
52+
white_logo = models.ImageField(
53+
upload_to="sponsor_white_logos",
54+
blank=True,
55+
null=True,
56+
verbose_name="White logo",
57+
help_text="For display on dark backgrounds (e.g. PyPI footer). Transparent PNG, smallest dimension no less than 256px",
58+
)
5259
print_logo = models.FileField(
5360
upload_to="sponsor_print_logos",
5461
validators=[FileExtensionValidator(["eps", "epsfepsi", "svg", "png"])],

apps/sponsors/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class LogoPlacementSerializer(serializers.Serializer):
1616
sponsor_slug = serializers.CharField()
1717
description = serializers.CharField()
1818
logo = serializers.URLField()
19+
white_logo = serializers.URLField(required=False, allow_null=True)
1920
start_date = serializers.DateField()
2021
end_date = serializers.DateField()
2122
sponsor_url = serializers.URLField()

apps/sponsors/templates/sponsors/new_sponsorship_application_form.html

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -108,70 +108,68 @@ <h2>Basics</h2>
108108
{% endif %}
109109
</p>
110110

111-
<div class="inline_fields">
112-
<div>
113-
<p class="form_field">
114-
<label>{{ form.landing_page_url.label }} <span class="error-message">{% if form.landing_page_url.errors %}{{ form.landing_page_url.errors.as_text }}</span>{% endif %}</label>
115-
{% render_field form.landing_page_url %}
116-
{% if form.landing_page_url.help_text %}
117-
<br/>
118-
<span class="helptext">{{ form.landing_page_url.help_text }}</span>
119-
{% endif %}
120-
</p>
121-
</div>
111+
<p class="form_field">
112+
<label>{{ form.landing_page_url.label }} <span class="error-message">{% if form.landing_page_url.errors %}{{ form.landing_page_url.errors.as_text }}</span>{% endif %}</label>
113+
{% render_field form.landing_page_url %}
114+
{% if form.landing_page_url.help_text %}
115+
<br/>
116+
<span class="helptext">{{ form.landing_page_url.help_text }}</span>
117+
{% endif %}
118+
</p>
122119

123-
<div>
124-
<p class="form_field">
125-
<label>{{ form.twitter_handle.label }} <span class="error-message">{% if form.twitter_handle.errors %}{{ form.twitter_handle.errors.as_text }}</span>{% endif %}</label>
126-
{% render_field form.twitter_handle %}
127-
{% if form.twitter_handle.help_text %}
128-
<br/>
129-
<span class="helptext">{{ form.twitter_handle.help_text }}</span>
130-
{% endif %}
131-
</p>
132-
</div>
120+
<p class="form_field">
121+
<label>{{ form.twitter_handle.label }} <span class="error-message">{% if form.twitter_handle.errors %}{{ form.twitter_handle.errors.as_text }}</span>{% endif %}</label>
122+
{% render_field form.twitter_handle %}
123+
{% if form.twitter_handle.help_text %}
124+
<br/>
125+
<span class="helptext">{{ form.twitter_handle.help_text }}</span>
126+
{% endif %}
127+
</p>
133128

134-
<div>
135-
<p class="form_field">
136-
<label>{{ form.linked_in_page_url.label }} <span class="error-message">{% if form.linked_in_page_url.errors %}{{ form.linked_in_page_url.errors.as_text }}</span>{% endif %}</label>
137-
{% render_field form.linked_in_page_url%}
138-
{% if form.linked_in_page_url.help_text %}
139-
<br/>
140-
<span class="helptext">{{ form.linked_in_page_url.help_text }}</span>
141-
{% endif %}
142-
</p>
143-
</div>
144-
</div>
129+
<p class="form_field">
130+
<label>{{ form.linked_in_page_url.label }} <span class="error-message">{% if form.linked_in_page_url.errors %}{{ form.linked_in_page_url.errors.as_text }}</span>{% endif %}</label>
131+
{% render_field form.linked_in_page_url%}
132+
{% if form.linked_in_page_url.help_text %}
133+
<br/>
134+
<span class="helptext">{{ form.linked_in_page_url.help_text }}</span>
135+
{% endif %}
136+
</p>
145137

146-
<div class="inline_fields">
147-
<div>
148-
<p class="form_field">
138+
<p class="form_field">
149139
<label>{{ form.web_logo.label }} <span class="error-message">{% if form.web_logo.errors %}{{ form.web_logo.errors.as_text }}</span>{% endif %}</label>
150-
{% render_field form.web_logo %}
151-
{% if sponsor.web_logo %}
152-
<p>Currently: <a href="{{ sponsor.web_logo.url }}">{{ sponsor.web_logo.name }}</a></p>
153-
{% endif %}
154-
{% if form.web_logo.help_text %}
155-
<br/>
156-
<span class="helptext">{{ form.web_logo.help_text }}</span>
157-
{% endif %}
158-
</p>
159-
</div>
140+
{% render_field form.web_logo %}
141+
{% if sponsor.web_logo %}
142+
<br/>Currently: <a href="{{ sponsor.web_logo.url }}">{{ sponsor.web_logo.name }}</a>
143+
{% endif %}
144+
{% if form.web_logo.help_text %}
145+
<br/>
146+
<span class="helptext">{{ form.web_logo.help_text }}</span>
147+
{% endif %}
148+
</p>
160149

161-
<div>
162-
<p class="form_field">
163-
<label>{{ form.print_logo.label }} <span class="error-message">{% if form.print_logo.errors %}{{ form.print_logo.errors.as_text }}</span>{% endif %}</label>
164-
{% render_field form.print_logo %}
165-
{% if sponsor.print_logo %}
166-
<p>Currently: <a href="{{ sponsor.print_logo.url }}">{{ sponsor.print_logo.name }}</a></p>
167-
{% endif %}
168-
{% if form.print_logo.help_text %}
169-
<br/>
170-
<span class="helptext">{{ form.print_logo.help_text }}</span>
171-
{% endif %}
172-
</p>
173-
</div>
174-
</div>
150+
<p class="form_field">
151+
<label>{{ form.white_logo.label }} <span class="error-message">{% if form.white_logo.errors %}{{ form.white_logo.errors.as_text }}</span>{% endif %}</label>
152+
{% render_field form.white_logo %}
153+
{% if sponsor.white_logo %}
154+
<br/>Currently: <a href="{{ sponsor.white_logo.url }}">{{ sponsor.white_logo.name }}</a>
155+
{% endif %}
156+
{% if form.white_logo.help_text %}
157+
<br/>
158+
<span class="helptext">{{ form.white_logo.help_text }}</span>
159+
{% endif %}
160+
</p>
161+
162+
<p class="form_field">
163+
<label>{{ form.print_logo.label }} <span class="error-message">{% if form.print_logo.errors %}{{ form.print_logo.errors.as_text }}</span>{% endif %}</label>
164+
{% render_field form.print_logo %}
165+
{% if sponsor.print_logo %}
166+
<br/>Currently: <a href="{{ sponsor.print_logo.url }}">{{ sponsor.print_logo.name }}</a>
167+
{% endif %}
168+
{% if form.print_logo.help_text %}
169+
<br/>
170+
<span class="helptext">{{ form.print_logo.help_text }}</span>
171+
{% endif %}
172+
</p>
175173

176174
<hr>
177175

apps/sponsors/tests/test_api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,30 @@ def tearDown(self):
4242
for sponsor in Sponsor.objects.all():
4343
if sponsor.web_logo:
4444
sponsor.web_logo.delete()
45+
if sponsor.white_logo:
46+
sponsor.white_logo.delete()
4547
if sponsor.print_logo:
4648
sponsor.print_logo.delete()
4749

50+
def test_white_logo_null_when_not_set(self):
51+
response = self.client.get(self.url, headers={"authorization": self.authorization})
52+
data = response.json()
53+
self.assertEqual(200, response.status_code)
54+
for placement in data:
55+
self.assertIn("white_logo", placement)
56+
self.assertIsNone(placement["white_logo"])
57+
58+
def test_white_logo_url_when_set(self):
59+
sponsor = self.sponsors[0]
60+
sponsor.white_logo = SimpleUploadedFile(name="white.png", content=b"img", content_type="image/png")
61+
sponsor.save()
62+
response = self.client.get(self.url, headers={"authorization": self.authorization})
63+
data = response.json()
64+
sponsor_placements = [p for p in data if p["sponsor"] == sponsor.name]
65+
for placement in sponsor_placements:
66+
self.assertIsNotNone(placement["white_logo"])
67+
self.assertIn("white.png", placement["white_logo"])
68+
4869
def test_list_logo_placement_as_expected(self):
4970
response = self.client.get(self.url, headers={"authorization": self.authorization})
5071
data = response.json()

apps/users/templates/users/sponsor_info_update.html

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -71,41 +71,33 @@ <h2>Sponsor Information</h2>
7171
{% render_field form.description %}
7272
</p>
7373

74-
<div class="inline_fields">
75-
<div>
76-
<p class="form_field">
77-
<label>{{ form.landing_page_url.label }} <span class="error-message">{% if form.landing_page_url.errors %}
78-
{{ form.landing_page_url.errors.as_text }}</span>{% endif %}</label>
79-
<span
80-
class="helptext">Landing page URL. The linked page may not contain any sales or marketing information.</span>
81-
{% render_field form.landing_page_url %}
82-
</p>
83-
</div>
74+
<p class="form_field">
75+
<label>{{ form.landing_page_url.label }} <span class="error-message">{% if form.landing_page_url.errors %}
76+
{{ form.landing_page_url.errors.as_text }}</span>{% endif %}</label>
77+
<span
78+
class="helptext">Landing page URL. The linked page may not contain any sales or marketing information.</span>
79+
{% render_field form.landing_page_url %}
80+
</p>
8481

85-
<div>
86-
<p class="form_field">
87-
<label>{{ form.twitter_handle.label }} <span class="error-message">{% if form.twitter_handle.errors %}
88-
{{ form.twitter_handle.errors.as_text }}</span>{% endif %}</label>
89-
{% render_field form.twitter_handle %}
90-
{% if form.twitter_handle.help_text %}
91-
<br/>
92-
<span class="helptext">{{ form.twitter_handle.help_text }}</span>
93-
{% endif %}
94-
</p>
95-
</div>
82+
<p class="form_field">
83+
<label>{{ form.twitter_handle.label }} <span class="error-message">{% if form.twitter_handle.errors %}
84+
{{ form.twitter_handle.errors.as_text }}</span>{% endif %}</label>
85+
{% render_field form.twitter_handle %}
86+
{% if form.twitter_handle.help_text %}
87+
<br/>
88+
<span class="helptext">{{ form.twitter_handle.help_text }}</span>
89+
{% endif %}
90+
</p>
9691

97-
<div>
98-
<p class="form_field">
99-
<label>{{ form.linked_in_page_url.label }} <span class="error-message">{% if form.linked_in_page_url.errors %}
100-
{{ form.linked_in_page_url.errors.as_text }}</span>{% endif %}</label>
101-
{% render_field form.linked_in_page_url%}
102-
{% if form.linked_in_page_url.help_text %}
103-
<br/>
104-
<span class="helptext">{{ form.linked_in_page_url.help_text }}</span>
105-
{% endif %}
106-
</p>
107-
</div>
108-
</div>
92+
<p class="form_field">
93+
<label>{{ form.linked_in_page_url.label }} <span class="error-message">{% if form.linked_in_page_url.errors %}
94+
{{ form.linked_in_page_url.errors.as_text }}</span>{% endif %}</label>
95+
{% render_field form.linked_in_page_url%}
96+
{% if form.linked_in_page_url.help_text %}
97+
<br/>
98+
<span class="helptext">{{ form.linked_in_page_url.help_text }}</span>
99+
{% endif %}
100+
</p>
109101
<div class="inline_fields">
110102
<div>
111103
<p class="form_field">
@@ -227,19 +219,31 @@ <h2>Sponsor Information</h2>
227219
{{ form.web_logo.errors.as_text }}</span>{% endif %}</label>
228220
{% render_field form.web_logo %}
229221
{% if sponsor.web_logo %}
230-
<p>Currently: <a href="{{ sponsor.web_logo.url }}">{{ sponsor.web_logo.name }}</a></p>
222+
<br/>Currently: <a href="{{ sponsor.web_logo.url }}">{{ sponsor.web_logo.name }}</a>
231223
{% endif %}
232224
{% if form.web_logo.help_text %}
233225
<span class="helptext">{{ form.web_logo.help_text }}</span>
234226
{% endif %}
235227
</p>
236228

229+
<p class="form_field">
230+
<label>{{ form.white_logo.label }} <span class="error-message">{% if form.white_logo.errors %}
231+
{{ form.white_logo.errors.as_text }}</span>{% endif %}</label>
232+
{% render_field form.white_logo %}
233+
{% if sponsor.white_logo %}
234+
<br/>Currently: <a href="{{ sponsor.white_logo.url }}">{{ sponsor.white_logo.name }}</a>
235+
{% endif %}
236+
{% if form.white_logo.help_text %}
237+
<span class="helptext">{{ form.white_logo.help_text }}</span>
238+
{% endif %}
239+
</p>
240+
237241
<p class="form_field">
238242
<label>{{ form.print_logo.label }} <span class="error-message">{% if form.print_logo.errors %}
239243
{{ form.print_logo.errors.as_text }}</span>{% endif %}</label>
240244
{% render_field form.print_logo %}
241245
{% if sponsor.print_logo %}
242-
<p>Currently: <a href="{{ sponsor.print_logo.url }}">{{ sponsor.print_logo.name }}</a></p>
246+
<br/>Currently: <a href="{{ sponsor.print_logo.url }}">{{ sponsor.print_logo.name }}</a>
243247
{% endif %}
244248
{% if form.print_logo.help_text %}
245249
<span class="helptext">{{ form.print_logo.help_text }}</span>

0 commit comments

Comments
 (0)