From f1348a1f13757645abad16666b5fc84776253afc Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Thu, 7 Nov 2024 16:23:50 +0000 Subject: [PATCH 01/11] feat: add the pillow library that's required for ImageField --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 720a7b952..ae7518c16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "requests==2.32.3", "sentry-sdk==2.18.0", "six==1.16.0", + "pillow==11.0.0", "django-multiselectfield==0.1.13", ] From a83951f4f08f195cb1ed0c85b16f8f2bfe67c3a2 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Thu, 7 Nov 2024 16:25:53 +0000 Subject: [PATCH 02/11] feat: set up where uploaded files will be saved to and the default storage --- benefits/settings.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/benefits/settings.py b/benefits/settings.py index 127743daa..23d892f6c 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -176,11 +176,11 @@ def RUNTIME_ENVIRONMENT(): WSGI_APPLICATION = "benefits.wsgi.application" -DATABASE_DIR = os.environ.get("DJANGO_DB_DIR", BASE_DIR) +STORAGE_DIR = os.environ.get("DJANGO_STORAGE_DIR", BASE_DIR) DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(DATABASE_DIR, os.environ.get("DJANGO_DB_FILE", "django.db")), + "NAME": os.path.join(STORAGE_DIR, os.environ.get("DJANGO_DB_FILE", "django.db")), } } @@ -234,14 +234,23 @@ def RUNTIME_ENVIRONMENT(): STATICFILES_DIRS = [os.path.join(BASE_DIR, "benefits", "static")] # use Manifest Static Files Storage by default STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, "staticfiles": { "BACKEND": os.environ.get( "DJANGO_STATICFILES_STORAGE", "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" ) - } + }, } STATIC_ROOT = os.path.join(BASE_DIR, "static") +# User-uploaded files + +MEDIA_ROOT = os.path.join(STORAGE_DIR, "uploads/") + +MEDIA_URL = "/media/" + # Logging configuration LOG_LEVEL = os.environ.get("DJANGO_LOG_LEVEL", "DEBUG" if DEBUG else "WARNING") LOGGING = { From 3ecf2e5312a0b5c25ebc4c71f959f33c6ac00492 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Thu, 7 Nov 2024 17:02:37 +0000 Subject: [PATCH 03/11] feat: add a property for a small and large agency logo --- .../migrations/0031_transitagency_logo.py | 36 +++++++++++++++++++ benefits/core/models.py | 28 +++++++++++++++ tests/pytest/core/test_models.py | 22 +++++++++++- 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 benefits/core/migrations/0031_transitagency_logo.py diff --git a/benefits/core/migrations/0031_transitagency_logo.py b/benefits/core/migrations/0031_transitagency_logo.py new file mode 100644 index 000000000..365610c75 --- /dev/null +++ b/benefits/core/migrations/0031_transitagency_logo.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.2 on 2024-11-14 18:05 + +import benefits.core.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0030_enrollmentevent_extra_claims"), + ] + + operations = [ + migrations.AddField( + model_name="transitagency", + name="logo_large", + field=models.ImageField( + blank=True, + default=None, + help_text="The large version of the transit agency's logo.", + null=True, + upload_to=benefits.core.models.agency_logo_large, + ), + ), + migrations.AddField( + model_name="transitagency", + name="logo_small", + field=models.ImageField( + blank=True, + default=None, + help_text="The small version of the transit agency's logo.", + null=True, + upload_to=benefits.core.models.agency_logo_small, + ), + ), + ] diff --git a/benefits/core/models.py b/benefits/core/models.py index 3b8b93079..86e63b6a1 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -5,6 +5,7 @@ from functools import cached_property import importlib import logging +import os import uuid from django.conf import settings @@ -126,6 +127,19 @@ def __str__(self): return self.name +def _agency_logo(instance, filename, size): + base, ext = os.path.splitext(filename) + return f"agencies/{instance.slug}-{size}" + ext + + +def agency_logo_small(instance, filename): + return _agency_logo(instance, filename, "sm") + + +def agency_logo_large(instance, filename): + return _agency_logo(instance, filename, "lg") + + class TransitAgency(models.Model): """An agency offering transit service.""" @@ -191,6 +205,20 @@ class TransitAgency(models.Model): help_text="The group of users who are allowed to do in-person eligibility verification and enrollment.", related_name="+", ) + logo_large = models.ImageField( + default=None, + null=True, + blank=True, + upload_to=agency_logo_large, + help_text="The large version of the transit agency's logo.", + ) + logo_small = models.ImageField( + default=None, + null=True, + blank=True, + upload_to=agency_logo_small, + help_text="The small version of the transit agency's logo.", + ) def __str__(self): return self.long_name diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 8b81f3527..c89992a46 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -6,7 +6,15 @@ import pytest -from benefits.core.models import SecretNameField, EnrollmentFlow, TransitAgency, EnrollmentEvent, EnrollmentMethods +from benefits.core.models import ( + SecretNameField, + EnrollmentFlow, + TransitAgency, + EnrollmentEvent, + EnrollmentMethods, + agency_logo_small, + agency_logo_large, +) import benefits.secrets @@ -413,6 +421,18 @@ def test_TransitAgency_for_user_in_group_not_linked_to_any_agency(): assert TransitAgency.for_user(user) is None +@pytest.mark.django_db +def test_agency_logo_small(model_TransitAgency): + + assert agency_logo_small(model_TransitAgency, "local_filename.png") == "agencies/test-sm.png" + + +@pytest.mark.django_db +def test_agency_logo_large(model_TransitAgency): + + assert agency_logo_large(model_TransitAgency, "local_filename.png") == "agencies/test-lg.png" + + @pytest.mark.django_db def test_EnrollmentEvent_create(model_TransitAgency, model_EnrollmentFlow): ts = timezone.now() From ce0f98a551ab4a5ffc91989196775c016984faed Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Fri, 15 Nov 2024 17:18:50 +0000 Subject: [PATCH 04/11] feat: serve files uploaded by a user in a local dev environment --- benefits/urls.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/benefits/urls.py b/benefits/urls.py index d53555bcb..b84ebab38 100644 --- a/benefits/urls.py +++ b/benefits/urls.py @@ -11,6 +11,7 @@ from django.contrib import admin from django.http import HttpResponse from django.urls import include, path +from django.conf.urls.static import static logger = logging.getLogger(__name__) @@ -37,6 +38,10 @@ def trigger_error(request): urlpatterns.append(path("error/", trigger_error)) + # serve user-uploaded media files + # https://docs.djangoproject.com/en/5.1/howto/static-files/#serving-files-uploaded-by-a-user-during-development + urlpatterns.extend(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)) + # simple route to read a pre-defined "secret" # this "secret" does not contain sensitive information # and is only configured in the dev environment for testing/debugging From 7ca17d37603cf2eef29a734dff948808f9e42863 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Fri, 15 Nov 2024 17:22:49 +0000 Subject: [PATCH 05/11] feat: make logo url properties available to templates via context --- benefits/core/context_processors.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 9ec635b21..a0235dc18 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -15,7 +15,7 @@ def unique_values(original_list): def _agency_context(agency: models.TransitAgency): - return { + agency_context = { "eligibility_index_url": agency.eligibility_index_url, "help_templates": unique_values([f.help_template for f in agency.enrollment_flows.all() if f.help_template]), "info_url": agency.info_url, @@ -25,6 +25,16 @@ def _agency_context(agency: models.TransitAgency): "slug": agency.slug, } + if agency.logo_large and agency.logo_small: + agency_context.update( + { + "logo_small_url": agency.logo_small.url, + "logo_large_url": agency.logo_large.url, + } + ) + + return agency_context + def agency(request): """Context processor adds some information about the active agency to the request context.""" From b8941b48d77a6b0fb23b704066333d518155ff29 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Fri, 15 Nov 2024 17:31:51 +0000 Subject: [PATCH 06/11] feat: update template to use agency logo property --- .../core/templates/core/includes/modal--agency-selector.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benefits/core/templates/core/includes/modal--agency-selector.html b/benefits/core/templates/core/includes/modal--agency-selector.html index 9b1f9ff2f..57389097b 100644 --- a/benefits/core/templates/core/includes/modal--agency-selector.html +++ b/benefits/core/templates/core/includes/modal--agency-selector.html @@ -12,12 +12,12 @@