From 4caa86ec37051296306463a2217594bec9135c2f Mon Sep 17 00:00:00 2001 From: David Fischer Date: Mon, 3 Jun 2024 17:47:52 -0700 Subject: [PATCH 1/2] Upgrade to Django 4.2 - Stop using pytz and use zoneinfo - Small changes to storages configuration - USE_L10N is deprecated (default on) and will be always on starting with Django 5 --- Dockerfile | 2 +- config/settings/base.py | 34 +++++++++++++++++++--------------- config/settings/prod.py | 16 ++++++++-------- config/settings/test.py | 14 ++++++++++++++ config/wsgi.py | 2 +- pythonsd/views.py | 11 ++++++----- requirements/common.txt | 5 ++--- 7 files changed, 51 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4631600..24ee55e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ COPY . /code/ RUN --mount=type=cache,target=/root/.npm npm install RUN npm run build -RUN python manage.py collectstatic --noinput +RUN python manage.py collectstatic --noinput --clear # Run the container unprivileged RUN addgroup www && useradd -g www www diff --git a/config/settings/base.py b/config/settings/base.py index c401d30..fddb73c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -2,10 +2,10 @@ Django settings for pythonsd project. For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ +https://docs.djangoproject.com/en/4.2/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ +https://docs.djangoproject.com/en/4.2/ref/settings/ """ import json @@ -20,7 +20,7 @@ # Quick-start development settings - unsuitable for production -# https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ +# https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: don't run with debug turned on in production! @@ -82,14 +82,14 @@ # Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # -------------------------------------------------------------------------- DATABASES = {"default": dj_database_url.config(default="sqlite:///db.sqlite3")} DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ +# https://docs.djangoproject.com/en/4.2/topics/i18n/ # -------------------------------------------------------------------------- LANGUAGE_CODE = "en-us" @@ -97,31 +97,35 @@ USE_I18N = True -USE_L10N = True - USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ +# https://docs.djangoproject.com/en/4.2/howto/static-files/ +# https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-STORAGES # -------------------------------------------------------------------------- STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") STATIC_URL = "/static-files/" - -# Due to a bug relating to the manifest not being generated before the tests run -# We can't use CompressedManifestStaticFilesStorage (yet) -STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" STATICFILES_DIRS = [ os.path.join(BASE_DIR, "assets", "dist"), os.path.join(BASE_DIR, "pythonsd", "static"), ] +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + MEDIA_URL = os.environ.get("MEDIA_URL", default="/media/") MEDIA_ROOT = os.path.join(BASE_DIR, "media") # Email -# https://docs.djangoproject.com/en/3.2/topics/email/ +# https://docs.djangoproject.com/en/4.2/topics/email/ # -------------------------------------------------------------------------- EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" DEFAULT_FROM_EMAIL = "noreply@sandiegopython.org" @@ -132,8 +136,8 @@ # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. -# http://docs.djangoproject.com/en/3.2/topics/logging -# https://docs.djangoproject.com/en/3.2/ref/settings/#logging +# http://docs.djangoproject.com/en/4.2/topics/logging +# https://docs.djangoproject.com/en/4.2/ref/settings/#logging # -------------------------------------------------------------------------- LOGGING = { "version": 1, diff --git a/config/settings/prod.py b/config/settings/prod.py index ad790d4..6f9fc26 100644 --- a/config/settings/prod.py +++ b/config/settings/prod.py @@ -12,7 +12,7 @@ from .base import * # noqa -# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ DEBUG = False SECRET_KEY = os.environ["SECRET_KEY"] @@ -47,11 +47,11 @@ # The endpoint URL is necessary for Cloudflare R2 AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", default=None) if AWS_S3_ACCESS_KEY_ID and AWS_S3_SECRET_ACCESS_KEY and AWS_STORAGE_BUCKET_NAME: - DEFAULT_FILE_STORAGE = "storages.backends.s3.S3Storage" + STORAGES["default"]["BACKEND"] = "storages.backends.s3.S3Storage" # Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # -------------------------------------------------------------------------- DATABASES = {"default": dj_database_url.config()} DATABASES["default"]["ATOMIC_REQUESTS"] = True @@ -59,7 +59,7 @@ # Caching -# https://docs.djangoproject.com/en/3.2/ref/settings/#caches +# https://docs.djangoproject.com/en/4.2/ref/settings/#caches # http://niwinz.github.io/django-redis/ # -------------------------------------------------------------------------- if "REDIS_URL" in os.environ: @@ -76,7 +76,7 @@ # Security -# https://docs.djangoproject.com/en/3.2/topics/security/ +# https://docs.djangoproject.com/en/4.2/topics/security/ # -------------------------------------------------------------------------- if "SECURE_SSL_HOST" in os.environ: SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") @@ -99,14 +99,14 @@ # Sessions -# https://docs.djangoproject.com/en/3.2/topics/http/sessions/ +# https://docs.djangoproject.com/en/4.2/topics/http/sessions/ # Don't put sessions in the database SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" # Email -# https://docs.djangoproject.com/en/3.2/topics/email/ +# https://docs.djangoproject.com/en/4.2/topics/email/ # https://anymail.readthedocs.io/en/stable/ # -------------------------------------------------------------------------- if "SENDGRID_API_KEY" in os.environ: @@ -116,7 +116,7 @@ # Logging -# http://docs.djangoproject.com/en/3.2/topics/logging +# http://docs.djangoproject.com/en/4.2/topics/logging # -------------------------------------------------------------------------- LOGGING["loggers"][""]["level"] = "INFO" LOGGING["loggers"]["pythonsd"]["level"] = "INFO" diff --git a/config/settings/test.py b/config/settings/test.py index 5bb3d3a..8381ba7 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -7,5 +7,19 @@ TESTING = True +STORAGES = { + # In-memory storage makes tests marginally faster! + "default": { + "BACKEND": "django.core.files.storage.InMemoryStorage", + }, + # Whitenoise relies on the manifest being present. + # Which may not be there in testing + # unless you run `collectstatic` before running tests + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage", + }, +} + + # Ignore whitenoise message about no static directory warnings.filterwarnings("ignore", message="No directory at", module="whitenoise.base") diff --git a/config/wsgi.py b/config/wsgi.py index 25ef1ca..4b08a92 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -4,7 +4,7 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ """ import os diff --git a/pythonsd/views.py b/pythonsd/views.py index 04436c3..42f636d 100644 --- a/pythonsd/views.py +++ b/pythonsd/views.py @@ -1,4 +1,5 @@ from datetime import datetime +import zoneinfo import logging from django.conf import settings @@ -6,7 +7,6 @@ from django.views.generic import TemplateView from django.utils.decorators import method_decorator -import pytz import requests from defusedxml import ElementTree @@ -69,9 +69,10 @@ def get_upcoming_events(self): "link": e["link"], "name": e["name"], # Always show time in local San Diego time - "datetime": datetime.utcfromtimestamp(e["time"] // 1000) - .replace(tzinfo=pytz.utc) - .astimezone(pytz.timezone(settings.TIME_ZONE)), + "datetime": datetime.fromtimestamp( + e["time"] // 1000, + tz=zoneinfo.ZoneInfo(key=settings.TIME_ZONE), + ), "venue": e["venue"]["name"] if "venue" in e else None, } for e in resp.json() @@ -132,7 +133,7 @@ def get_recent_videos(self): # the stream was initialized in youtube, not when it was live "datetime": datetime.fromisoformat( entry.find("atom:updated", ns).text - ).astimezone(pytz.timezone(settings.TIME_ZONE)), + ).astimezone(zoneinfo.ZoneInfo(key=settings.TIME_ZONE)), } ) else: diff --git a/requirements/common.txt b/requirements/common.txt index e5d612e..e17faf1 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,10 +1,9 @@ # Helper for transforming a database URL envvar # to a Django connection string -dj-database-url==2.1.0 +dj-database-url==2.2.0 # This is a Django app -Django==3.2.25 -pytz==2022.7.1 +Django==4.2.13 # A zero dependency WSGI server gunicorn==22.0.0 From 0cc04aa068248cb6cfee124b8f0dc7e8524681b4 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 4 Jun 2024 08:47:07 -0700 Subject: [PATCH 2/2] Write log entries for requests --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 24ee55e..082273f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,4 +54,4 @@ RUN date -u +'%Y-%m-%dT%H:%M:%SZ' > BUILD_DATE EXPOSE 8000 -CMD ["gunicorn", "--timeout", "15", "--bind", ":8000", "--workers", "2", "--max-requests", "10000", "--max-requests-jitter", "100", "--log-file", "-", "config.wsgi"] +CMD ["gunicorn", "--timeout", "15", "--bind", ":8000", "--workers", "2", "--max-requests", "10000", "--max-requests-jitter", "100", "--log-file", "-", "--access-logfile", "-", "config.wsgi"]