Skip to content
Open
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
2 changes: 0 additions & 2 deletions apps/agreements/tests/test_link_privacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def test_token_pages_do_not_load_tracking_scripts_in_any_signing_state(self) ->
self.assertEqual(response.status_code, 410 if state == "used" else 400 if state == "invalid" else 200)
scripts = ScriptSources(response.content.decode())
self.assertNotIn("analytics.python.org", scripts.hosts)
self.assertNotIn("media.ethicalads.io", scripts.hosts)

def test_token_download_and_terms_errors_do_not_load_trackers(self) -> None:
officer = make_officer()
Expand All @@ -70,7 +69,6 @@ def test_token_download_and_terms_errors_do_not_load_trackers(self) -> None:
self.assertEqual(response.status_code, 404)
scripts = ScriptSources(response.content.decode())
self.assertNotIn("analytics.python.org", scripts.hosts)
self.assertNotIn("media.ethicalads.io", scripts.hosts)

def test_public_pages_retain_analytics(self) -> None:
terms = make_terms()
Expand Down
3 changes: 2 additions & 1 deletion apps/sponsors/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.contrib.admin.models import ADDITION, CHANGE, LogEntry
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.core.mail import EmailMessage
from django.template.loader import render_to_string

Expand Down Expand Up @@ -222,7 +223,7 @@ class RefreshSponsorshipsCache:
def notify(self, *args, **kwargs):
"""Delete the cached sponsors list to force a refresh."""
# clean up cached used by "sponsors/partials/sponsors-list.html"
cache.delete("CACHED_SPONSORS_LIST")
cache.delete(make_template_fragment_key("SPONSORS_PAGE_LIST"))


class AssetCloseToDueDateNotificationToSponsors(BaseEmailSponsorshipNotification):
Expand Down
16 changes: 8 additions & 8 deletions apps/sponsors/templates/sponsors/partials/sponsors-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ <h3 class="widget-title">Job Board Sponsors</h3>

{% elif logo_place == "sponsors" %}
{% comment %}cache for 1 day{% endcomment %}
{% cache 86400 CACHED_SPONSORS_LIST %}
{% cache 86400 SPONSORS_PAGE_LIST %}

{% for package, placement_info in sponsorships_by_package.items %}
{% if placement_info.sponsorships %}
Expand All @@ -59,12 +59,12 @@ <h1 style="font-size: {% if forloop.first %}350%{% else %}300%{% endif %}">{{ pl
<div style="display: grid; gap: 2.5rem; grid-template-columns: repeat(auto-fit, minmax({{ dimension }}px, 1fr)); align-items: center; justify-content: center;">
{% for sponsorship in placement_info.sponsorships %}
<div id="{{ sponsorship.sponsor.slug }}" data-internal-year="{{ sponsorship.year|default_if_none:'' }}">
<div
data-ea-publisher="psf"
data-ea-type="psf-image-only"
data-ea-force-ad="{{ sponsorship.sponsor.slug }}-psf-sponsors"
style="max-width:{{ sponsorship.sponsor.web_logo|ideal_size:dimension }}px;height:auto;width:auto;"
></div>
{% sponsor_logo sponsorship.sponsor.web_logo dimension as logo %}
{% if logo %}
{% if sponsorship.sponsor.landing_page_url %}<a href="{{ sponsorship.sponsor.landing_page_url }}" rel="sponsored noopener" target="_blank" class="plausible-event-name=Sponsor+Click plausible-event-sponsor={{ sponsorship.sponsor.slug }}" style="border-bottom: 0;">{% endif %}
<img src="{{ logo.src }}" srcset="{{ logo.srcset }}" sizes="{{ logo.width }}px" width="{{ logo.width }}" height="{{ logo.height }}" alt="{{ sponsorship.sponsor.name }} logo" loading="{% if forloop.parentloop.first %}eager{% else %}lazy{% endif %}" decoding="async" style="max-width:100%;height:auto;">
{% if sponsorship.sponsor.landing_page_url %}</a>{% endif %}
{% endif %}
<p>{{ sponsorship.sponsor.name }}</p>
</div>
{% endfor %}
Expand All @@ -74,5 +74,5 @@ <h1 style="font-size: {% if forloop.first %}350%{% else %}300%{% endif %}">{{ pl
</div>
{% endif %}
{% endfor %}
{% endcache CACHED_SPONSORS_LIST %}
{% endcache SPONSORS_PAGE_LIST %}
{% endif %}
48 changes: 37 additions & 11 deletions apps/sponsors/templatetags/sponsors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

from django import template
from django.utils.safestring import mark_safe
from sorl.thumbnail import default as thumbnail_default
from sorl.thumbnail import get_thumbnail
from sorl.thumbnail.images import ImageFile

from apps.sponsors.models import Sponsorship, SponsorshipPackage, TieredBenefitConfiguration
from apps.sponsors.models.enums import LogoPlacementChoices, PublisherChoices
Expand Down Expand Up @@ -91,16 +94,39 @@ def benefit_name_for_display(benefit, package):
return benefit.name_for_display(package=package)


@register.filter
def ideal_size(image, ideal_dimension):
"""Scale an image width to fit within the given ideal dimension area."""
@register.simple_tag
def sponsor_logo(image, ideal_dimension):
"""Size a logo so every sponsor in a tier gets the same visual area, with 1x and 2x PNG renditions.

The source size comes from sorl's key-value store: reading ``image.width`` would download
the full original from S3 on every render, while sorl records the size once per image.
Returns ``None`` when no file is associated with the field.
"""
if not image:
return None
ideal_dimension = int(ideal_dimension)
try:
w, h = image.width, image.height
except (FileNotFoundError, ValueError):
# FileNotFoundError: local dev doesn't have all images if DB is a copy from prod environment.
# ValueError: no file is associated with the field.
# Size as a square logo would be instead of erroring.
w, h = ideal_dimension, ideal_dimension

return int(w * math.sqrt((100 * ideal_dimension) / (w * h)))
source_width, source_height = thumbnail_default.kvstore.get_or_set(ImageFile(image)).size
except FileNotFoundError:
# local dev doesn't have all images if DB is a copy from prod environment;
# size it as a square logo would be instead of erroring.
width = int(math.sqrt(100 * ideal_dimension))
return {"src": image.url, "srcset": f"{image.url} {width}w", "width": width, "height": width}

# Equal area per logo, but no wider than the tier's grid column (ideal_dimension px).
width = min(int(math.sqrt(100 * ideal_dimension * source_width / source_height)), ideal_dimension)
# Never upscale: past the original's size a bigger file adds bytes, not detail.
one_x, two_x = (get_thumbnail(image, str(width * scale), format="PNG", upscale=False) for scale in (1, 2))
if source_width > width:
# sorl can land 1px off the requested width; drawing at the rendition's exact size
# keeps 1x screens on the 1x file instead of fetching 2x.
width, height = one_x.width, one_x.height
else:
height = round(width * source_height / source_width)
renditions = {im.width: im.url for im in (one_x, two_x)}
return {
"src": one_x.url,
"srcset": ", ".join(f"{url} {im_width}w" for im_width, url in renditions.items()),
"width": width,
"height": height,
}
14 changes: 14 additions & 0 deletions apps/sponsors/tests/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from django.contrib.admin.models import ADDITION, CHANGE, LogEntry
from django.contrib.contenttypes.models import ContentType
from django.core import mail
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.template import Context, Template
from django.template.loader import render_to_string
from django.test import RequestFactory, TestCase
from django.utils import timezone
Expand Down Expand Up @@ -489,3 +492,14 @@ def test_create_log_entry_for_cloned_resource(self):
self.assertEqual(str(self.package), log_entry.object_repr)
self.assertEqual(log_entry.action_flag, ADDITION)
self.assertEqual(log_entry.change_message, "Cloned from 2022 sponsorship application config")


class RefreshSponsorshipsCacheTests(TestCase):
def test_clears_cached_sponsors_page_fragment(self):
Template('{% load sponsors %}{% list_sponsors "sponsors" %}').render(Context())
key = make_template_fragment_key("SPONSORS_PAGE_LIST")
self.assertIsNotNone(cache.get(key))

notifications.RefreshSponsorshipsCache().notify()

self.assertIsNone(cache.get(key))
106 changes: 87 additions & 19 deletions apps/sponsors/tests/test_templatetags.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import io
import string
import tempfile
from unittest.mock import patch

from django.test import TestCase
from django.core.files.storage import FileSystemStorage
from django.core.files.uploadedfile import SimpleUploadedFile
from django.template import Context, Template
from django.test import TestCase, override_settings
from model_bakery import baker
from PIL import Image

from apps.sponsors.models import Sponsor, SponsorshipBenefit, TieredBenefitConfiguration
from apps.sponsors.templatetags.sponsors import (
benefit_name_for_display,
benefit_quantity_for_package,
escape_markdown,
full_sponsorship,
ideal_size,
list_sponsors,
sponsor_logo,
)


Expand Down Expand Up @@ -93,26 +99,15 @@ def test_display_name_for_display_from_benefit(self, mocked_name_for_display):
mocked_name_for_display.assert_called_once_with(package=package)


class IdealSizeFilterTests(TestCase):
def test_scales_width_to_fit_ideal_area(self):
class Image:
width = 400
height = 200

# int(400 * sqrt(20000 / 80000)) = int(400 * 0.5) = 200
self.assertEqual(ideal_size(Image(), 200), 200)

def test_no_file_associated_is_sized_as_square(self):
logo = Sponsor(web_logo="").web_logo

# int(250 * sqrt(25000 / 62500)) = 158, same as a square logo
self.assertEqual(ideal_size(logo, 250), 158)
class SponsorLogoFallbackTests(TestCase):
def test_no_file_associated_renders_no_logo(self):
self.assertIsNone(sponsor_logo(Sponsor(web_logo="").web_logo, 250))

def test_file_missing_from_storage_is_sized_as_square(self):
logo = Sponsor(web_logo="sponsor_web_logos/does-not-exist.png").web_logo
logo = sponsor_logo(Sponsor(web_logo="sponsor_web_logos/does-not-exist.png").web_logo, 300)

# int(300 * sqrt(30000 / 90000)) = 173, same as a square logo
self.assertEqual(ideal_size(logo, 300), 173)
# int(sqrt(100 * 300)) = 173, same as a square logo
self.assertEqual((logo["width"], logo["height"]), (173, 173))


class EscapePandocMarkdownTests(TestCase):
Expand All @@ -139,3 +134,76 @@ def test_letters_digits_and_spaces_are_untouched(self):

def test_non_string_input_is_coerced(self):
self.assertEqual(escape_markdown(42), "42")


class SponsorLogoTagTests(TestCase):
def setUp(self):
media_root = tempfile.TemporaryDirectory()
self.addCleanup(media_root.cleanup)
settings_override = override_settings(MEDIA_ROOT=media_root.name)
settings_override.enable()
self.addCleanup(settings_override.disable)

def make_logo(self, size, **sponsor_attrs):
buf = io.BytesIO()
Image.new("RGB", size, "red").save(buf, "PNG")
sponsor = baker.make(
"sponsors.Sponsor", web_logo=SimpleUploadedFile("logo.png", buf.getvalue()), **sponsor_attrs
)
return sponsor.web_logo

def srcset_widths(self, logo):
return [int(candidate.split()[1].removesuffix("w")) for candidate in logo["srcset"].split(", ")]

def test_logo_gets_equal_area_size_and_true_2x_rendition(self):
logo = sponsor_logo(self.make_logo((600, 300)), "350")

self.assertEqual((logo["width"], logo["height"]), (264, 132))
self.assertEqual(self.srcset_widths(logo), [264, 528])
for candidate in logo["srcset"].split(", "):
url, descriptor = candidate.split()
with Image.open(FileSystemStorage().path(url.removeprefix("/media/"))) as rendition:
self.assertEqual(f"{rendition.width}w", descriptor)
self.assertEqual(rendition.format, "PNG")

def test_very_wide_logo_is_capped_at_column_width(self):
logo = sponsor_logo(self.make_logo((800, 200)), "350")

self.assertEqual((logo["width"], logo["height"]), (350, 88))
self.assertEqual(self.srcset_widths(logo), [350, 700])

def test_display_size_matches_1x_rendition_when_resize_rounds(self):
# 1500x600 at 300 asks sorl for 273px wide; sorl rounds via the height and returns 272.
logo = sponsor_logo(self.make_logo((1500, 600)), "300")

self.assertEqual((logo["width"], logo["height"]), (272, 109))
self.assertEqual(self.srcset_widths(logo)[0], logo["width"])

def test_small_logo_is_not_upscaled_past_its_original(self):
logo = sponsor_logo(self.make_logo((300, 300)), "350")

self.assertEqual((logo["width"], logo["height"]), (187, 187))
self.assertEqual(self.srcset_widths(logo), [187, 300])

def test_repeat_render_does_not_read_the_original(self):
image = self.make_logo((800, 200))
first = sponsor_logo(image, "350")

with patch.object(FileSystemStorage, "open", side_effect=AssertionError("read original")):
self.assertEqual(sponsor_logo(image, "350"), first)

def test_sponsors_page_links_only_logos_with_a_landing_page(self):
package = baker.make("sponsors.SponsorshipPackage", logo_dimension=350)
for name, url in (("Linked", "https://linked.example/"), ("Unlinked", None)):
sponsor = self.make_logo((600, 300), name=name, landing_page_url=url).instance
sponsorship = baker.make_recipe(
"apps.sponsors.tests.finalized_sponsorship", sponsor=sponsor, package=package
)
baker.make_recipe("apps.sponsors.tests.logo_at_sponsors_feature", sponsor_benefit__sponsorship=sponsorship)

html = Template('{% load sponsors %}{% list_sponsors "sponsors" %}').render(Context())

self.assertEqual(html.count("<a "), 1)
self.assertIn('<a href="https://linked.example/"', html)
self.assertIn("plausible-event-sponsor=linked", html)
self.assertIn('alt="Unlinked logo"', html)
2 changes: 0 additions & 2 deletions pydotorg/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,6 @@
SELF,
NONCE,
"https://analytics.python.org",
"https://media.ethicalads.io",
"https://ajax.googleapis.com",
],
"style-src": [
Expand All @@ -342,7 +341,6 @@
SELF,
"https://console.python.org",
"https://analytics.python.org",
"https://server.ethicalads.io",
# Status Page, host included in 'static/js/script.js'
"https://2p66nmmycsj3.statuspage.io",
],
Expand Down
9 changes: 1 addition & 8 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script defer
file-types="bz2,chm,dmg,exe,gz,json,msi,msix,pdf,pkg,tgz,xz,zip"
data-domain="python.org"
src="https://analytics.python.org/js/script.file-downloads.outbound-links.js"></script>
src="https://analytics.python.org/js/script.file-downloads.outbound-links.tagged-events.js"></script>
{% endif %}

<meta charset="utf-8">
Expand All @@ -23,13 +23,6 @@
<meta name="HandheldFriendly" content="True">
<meta name="format-detection" content="telephone=no">

{% if not request.disable_tracking %}
<script async
src="https://media.ethicalads.io/media/client/v1.4.0/ethicalads.min.js"
integrity="sha256-U3hKDidudIaxBDEzwGJApJgPEf2mWk6cfMWghrAa6i0= sha384-UcmsCqcNRSLW/dV3Lo1oCi2/VaurXbib6p4HyUEOeIa/4OpsrnucrugAefzVZJfI sha512-q4t1L4xEjGV2R4hzqCa41P8jrgFUS8xTb8rdNv4FGvw7FpydVj/kkxBJHOiaoxHa8olCcx1Slk9K+3sNbsM4ug=="
crossorigin="anonymous"
></script>
{% endif %}
<script src="{{ STATIC_URL }}js/libs/modernizr.js"></script>

{% stylesheet 'style' %}
Expand Down
Loading