diff --git a/tests/unit/sponsors/test_init.py b/tests/unit/sponsors/test_init.py index 582a25ea890c..fa2fd29da488 100644 --- a/tests/unit/sponsors/test_init.py +++ b/tests/unit/sponsors/test_init.py @@ -3,10 +3,8 @@ import pretend from celery.schedules import crontab -from sqlalchemy import true from warehouse import sponsors -from warehouse.sponsors.models import Sponsor from warehouse.sponsors.tasks import update_pypi_sponsors from ...common.db.sponsors import SponsorFactory @@ -24,6 +22,7 @@ def test_includeme(): assert config.add_request_method.calls == [ pretend.call(sponsors._sponsors, name="sponsors", reify=True), + pretend.call(sponsors._footer_sponsors, name="footer_sponsors", reify=True), ] assert config.add_periodic_task.calls == [ pretend.call(crontab(minute=10), update_pypi_sponsors), @@ -42,16 +41,40 @@ def test_do_not_schedule_sponsor_api_integration_if_no_token(): assert config.add_request_method.calls == [ pretend.call(sponsors._sponsors, name="sponsors", reify=True), + pretend.call(sponsors._footer_sponsors, name="footer_sponsors", reify=True), ] assert not config.add_periodic_task.calls def test_list_sponsors(db_request): - SponsorFactory.create_batch(5) + expected = SponsorFactory.create_batch(5) SponsorFactory.create_batch(3, is_active=False) result = sponsors._sponsors(db_request) - expected = db_request.db.query(Sponsor).filter(Sponsor.is_active == true()).all() - assert result == expected assert len(result) == 5 + assert set(result) == set(expected) + + +def test_sponsors_ordered_by_level_then_name_then_infra(db_request): + c = SponsorFactory.create + infra = c(name="AWS", infra_sponsor=True, level_order=0) + vis_b = c(name="Bravo", infra_sponsor=False, level_order=1) + vis_a = c(name="Alpha", infra_sponsor=False, level_order=1) + sus = c(name="Charlie", infra_sponsor=False, level_order=2) + + result = sponsors._sponsors(db_request) + + assert result == [vis_a, vis_b, sus, infra] + + +def test_footer_sponsors_ordering(db_request): + c = SponsorFactory.create + infra = c(name="AWS", infra_sponsor=True, footer=False, level_order=0) + vis_b = c(name="Bravo", footer=True, infra_sponsor=False, level_order=1) + vis_a = c(name="Alpha", footer=True, infra_sponsor=False, level_order=1) + sus = c(name="Charlie", footer=True, infra_sponsor=False, level_order=2) + c(name="Nobody", footer=False, infra_sponsor=False, level_order=5) + + db_request.sponsors = sponsors._sponsors(db_request) + assert sponsors._footer_sponsors(db_request) == [vis_a, vis_b, sus, infra] diff --git a/tests/unit/sponsors/test_models.py b/tests/unit/sponsors/test_models.py index 919d261cc244..263c327d2d8f 100644 --- a/tests/unit/sponsors/test_models.py +++ b/tests/unit/sponsors/test_models.py @@ -5,7 +5,10 @@ def test_sponsor_color_logo_img_tag(db_request): sponsor = SponsorFactory.create() - expected = f'' + expected = ( + '' + ) assert sponsor.color_logo_img == expected diff --git a/tests/unit/sponsors/test_tasks.py b/tests/unit/sponsors/test_tasks.py index 698f75d58c71..4e376af02d12 100644 --- a/tests/unit/sponsors/test_tasks.py +++ b/tests/unit/sponsors/test_tasks.py @@ -196,6 +196,58 @@ def test_update_remote_sponsor_with_same_slug_with_new_logo( assert db_sponsor.service == "Sponsor description" +@pytest.mark.parametrize( + ("level_name", "expected_footer"), + [ + ("Visionary", True), + ("Sustainability", True), + ("Partner", False), + ], +) +def test_footer_set_based_on_level( + monkeypatch, + db_request, + fake_task_request, + sponsor_api_data, + level_name, + expected_footer, +): + sponsor_api_data[0]["level_name"] = level_name + response = pretend.stub( + raise_for_status=lambda: None, json=lambda: sponsor_api_data + ) + requests = pretend.stub( + get=pretend.call_recorder(lambda url, headers, timeout: response) + ) + monkeypatch.setattr(tasks, "requests", requests) + + fake_task_request.db = db_request.db + tasks.update_pypi_sponsors(fake_task_request) + + db_sponsor = db_request.db.query(Sponsor).one() + assert db_sponsor.footer is expected_footer + + +def test_white_logo_synced_when_provided( + monkeypatch, db_request, fake_task_request, sponsor_api_data +): + sponsor_api_data[0]["white_logo"] = "https://logourl.com/white.png" + response = pretend.stub( + raise_for_status=lambda: None, json=lambda: sponsor_api_data + ) + monkeypatch.setattr( + tasks, + "requests", + pretend.stub(get=pretend.call_recorder(lambda *a, **kw: response)), + ) + fake_task_request.db = db_request.db + tasks.update_pypi_sponsors(fake_task_request) + assert ( + db_request.db.query(Sponsor).one().white_logo_url + == "https://logourl.com/white.png" + ) + + def test_flag_existing_psf_sponsor_to_false_if_not_present_in_api_response( monkeypatch, db_request, fake_task_request, sponsor_api_data ): diff --git a/warehouse/sponsors/__init__.py b/warehouse/sponsors/__init__.py index 1fe5f8e62d33..b888de75bb52 100644 --- a/warehouse/sponsors/__init__.py +++ b/warehouse/sponsors/__init__.py @@ -8,12 +8,32 @@ def _sponsors(request): - return request.db.query(Sponsor).filter(Sponsor.is_active == true()).all() + return ( + request.db.query(Sponsor) + .filter(Sponsor.is_active == true()) + .order_by(Sponsor.infra_sponsor, Sponsor.level_order, Sponsor.name) + .all() + ) + + +def _footer_sponsors(request): + """Return footer sponsors: PSF by level then name, infra by name.""" + all_sponsors = request.sponsors + psf = sorted( + (s for s in all_sponsors if s.footer and not s.infra_sponsor), + key=lambda s: (s.level_order or 0, s.name), + ) + infra = sorted( + (s for s in all_sponsors if s.infra_sponsor), + key=lambda s: s.name, + ) + return psf + infra def includeme(config): # Add a request method which will allow to list sponsors config.add_request_method(_sponsors, name="sponsors", reify=True) + config.add_request_method(_footer_sponsors, name="footer_sponsors", reify=True) # Add a periodic task to update sponsors table if config.registry.settings.get("pythondotorg.api_token"): diff --git a/warehouse/sponsors/models.py b/warehouse/sponsors/models.py index 34940013aaac..46f0325b47a2 100644 --- a/warehouse/sponsors/models.py +++ b/warehouse/sponsors/models.py @@ -38,7 +38,10 @@ class Sponsor(db.Model): @property def color_logo_img(self): - return f'' + return ( + '' + ) @property def white_logo_img(self): diff --git a/warehouse/sponsors/tasks.py b/warehouse/sponsors/tasks.py index 60bdbb142fe2..87aa4927ce3e 100644 --- a/warehouse/sponsors/tasks.py +++ b/warehouse/sponsors/tasks.py @@ -50,8 +50,10 @@ def update_pypi_sponsors(request): sponsor.service = sponsor_info["description"] sponsor.link_url = sponsor_info["sponsor_url"] sponsor.color_logo_url = sponsor_info["logo"] + sponsor.white_logo_url = sponsor_info.get("white_logo") sponsor.level_name = sponsor_info["level_name"] sponsor.level_order = sponsor_info["level_order"] sponsor.is_active = True sponsor.psf_sponsor = True + sponsor.footer = sponsor_info["level_name"] in {"Visionary", "Sustainability"} sponsor.origin = "remote" diff --git a/warehouse/static/sass/blocks/_sponsors.scss b/warehouse/static/sass/blocks/_sponsors.scss index ff061acd3603..ec76917d0dec 100644 --- a/warehouse/static/sass/blocks/_sponsors.scss +++ b/warehouse/static/sass/blocks/_sponsors.scss @@ -50,6 +50,16 @@ height: 0; } + &__group-label { + flex-basis: 100%; + color: color.adjust(colours.$white, $alpha: -0.4); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 4px 0 0; + padding: 0; + } + &__sponsor { display: inline-grid; grid-template-rows: fit-content(40%); @@ -109,4 +119,27 @@ display: none; } } + + &__sponsor--infra { + opacity: 0.75; + + .sponsors__image { + max-width: 80px; + opacity: 0.7; + } + + .sponsors__name, + .sponsors__service { + max-width: 80px; + font-size: 0.68rem; + } + + &:hover { + opacity: 0.95; + + .sponsors__image { + opacity: 0.9; + } + } + } } diff --git a/warehouse/templates/includes/sponsors-footer.html b/warehouse/templates/includes/sponsors-footer.html index 9c033c991fda..c23534cf97d5 100644 --- a/warehouse/templates/includes/sponsors-footer.html +++ b/warehouse/templates/includes/sponsors-footer.html @@ -14,28 +14,25 @@

Supported by

- {% for sponsor in request.sponsors | sort(attribute='name') %} - {# Short-circuit if we don't have an image for them #} - {% if sponsor.white_logo_url %} - {# Check if they belong in the footer #} - {% if sponsor.infra_sponsor or sponsor.footer %} - - {{ sponsor.white_logo_img|camoify|safe }} - {{ sponsor.name }} - - {# If they're an infra sponsor, we should have a service for them, - otherwise they're a PSF sponsor #} - {% if sponsor.infra_sponsor %} - {{ sponsor.service }} - {% elif sponsor.footer %} - PSF Sponsor - {% endif %} - - - {% endif %} + {% for sponsor in request.footer_sponsors %} + {% if sponsor.infra_sponsor and loop.previtem is defined and not loop.previtem.infra_sponsor %} +
+

Infrastructure partners

+
{% endif %} + + {% if sponsor.white_logo_url %}{{ sponsor.white_logo_img|camoify|safe }}{% endif %} + {{ sponsor.name }} + + {% if sponsor.infra_sponsor %} + {{ sponsor.service }} + {% else %} + PSF Sponsor + {% endif %} + + {% endfor %}
diff --git a/warehouse/templates/pages/sponsors.html b/warehouse/templates/pages/sponsors.html index 90821deaa3ce..8510593ee7b3 100644 --- a/warehouse/templates/pages/sponsors.html +++ b/warehouse/templates/pages/sponsors.html @@ -164,7 +164,7 @@

Infrastructure sponsors