From 5f022aca0a9de02afd3f68a86246cec3761ca861 Mon Sep 17 00:00:00 2001 From: daveoconnor Date: Wed, 22 Oct 2025 13:24:26 -0700 Subject: [PATCH] Upgrade django to 5.2, python to 3.13 (#1915) --- .github/workflows/actions-gcp.yaml | 10 +- .github/workflows/actions.yml | 10 +- .pre-commit-config.yaml | 7 +- README.md | 2 +- core/calendar.py | 6 +- core/migrations/0003_sitesettings_and_more.py | 2 +- core/models.py | 2 +- core/views.py | 58 +++++++ docker/Dockerfile | 4 +- docs/dependencies.md | 11 ++ justfile | 5 + marketing/views.py | 8 +- news/feeds.py | 6 +- news/notifications.py | 2 +- news/tests/test_feeds.py | 10 +- pyproject.toml | 2 +- requirements-dev.in | 2 +- requirements-dev.txt | 6 +- requirements.in | 4 +- requirements.txt | 150 +++++++++--------- slack/migrations/0001_initial.py | 4 +- slack/models.py | 4 +- users/admin.py | 4 +- users/views.py | 2 +- versions/feeds.py | 6 +- versions/tests/test_feeds.py | 10 +- 26 files changed, 207 insertions(+), 130 deletions(-) diff --git a/.github/workflows/actions-gcp.yaml b/.github/workflows/actions-gcp.yaml index e66ef932..57fba5ed 100644 --- a/.github/workflows/actions-gcp.yaml +++ b/.github/workflows/actions-gcp.yaml @@ -25,7 +25,7 @@ jobs: services: postgres: - image: postgres:12 + image: postgres:16 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -44,10 +44,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Python 3.11 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 - uses: actions/cache@v4 with: @@ -133,10 +133,10 @@ jobs: run: | git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true - - name: Set up Python 3.11 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 - name: Install Python dependencies run: | diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 3997f581..607b8cb6 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -15,7 +15,7 @@ jobs: services: postgres: - image: postgres:12 + image: postgres:16 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -34,10 +34,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Python 3.11 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 - uses: actions/cache@v4 with: @@ -85,10 +85,10 @@ jobs: run: | git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true - - name: Set up Python 3.11 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 - name: Install Python dependencies run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0c0f116..8ac7766d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,14 @@ default_language_version: - python: python3.11 + python: python3.13 exclude: .*migrations\/.*|static\/img\/.*|static\/animations\/.*|static\/js\/boost-gecko\/.*|kube\/boost\/templates\/.*\.yaml repos: + - repo: https://github.com/adamchainz/django-upgrade + rev: "1.27.0" + hooks: + - id: django-upgrade + args: [--target-version, "5.2"] # Replace with Django version - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: diff --git a/README.md b/README.md index 4fe6feb7..e8dcef84 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Links: ## Local Development Setup -This project will use Python 3.11, Docker, and Docker Compose. +This project will use Python 3.13, Docker, and Docker Compose. Instructions to install those packages are included in [development_setup_notes.md](docs/development_setup_notes.md). diff --git a/core/calendar.py b/core/calendar.py index 80831e9e..d51d6409 100644 --- a/core/calendar.py +++ b/core/calendar.py @@ -20,10 +20,8 @@ def get_calendar(min_time=None, single_events=True, order_by="startTime"): https://developers.google.com/calendar/api/v3/reference/events/list """ if not min_time: - min_time = ( - datetime.datetime.utcnow().isoformat() + "Z" - ) # 'Z' indicates UTC time - + # 'Z' indicates UTC time + min_time = datetime.datetime.now(datetime.timezone.utc).isoformat() + "Z" url = f"https://www.googleapis.com/calendar/v3/calendars/{settings.BOOST_CALENDAR}/events?key={settings.CALENDAR_API_KEY}&timeMin={min_time}&singleEvents={single_events}&orderBy={order_by}" headers = {"Accept": "application/json"} diff --git a/core/migrations/0003_sitesettings_and_more.py b/core/migrations/0003_sitesettings_and_more.py index ce31eb67..7d38c73a 100644 --- a/core/migrations/0003_sitesettings_and_more.py +++ b/core/migrations/0003_sitesettings_and_more.py @@ -28,7 +28,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="sitesettings", constraint=models.CheckConstraint( - check=models.Q(("id", 1)), name="core_sitesettings_single_instance" + condition=models.Q(("id", 1)), name="core_sitesettings_single_instance" ), ), migrations.AlterModelOptions( diff --git a/core/models.py b/core/models.py index 63e0d47e..e74c7595 100644 --- a/core/models.py +++ b/core/models.py @@ -72,7 +72,7 @@ class SiteSettings(models.Model): # check constraint to only allow id=1 to exist models.CheckConstraint( name="%(app_label)s_%(class)s_single_instance", - check=models.Q(id=1), + condition=models.Q(id=1), ), ] verbose_name_plural = "Site Settings" diff --git a/core/views.py b/core/views.py index 4631a7b7..2d408c20 100644 --- a/core/views.py +++ b/core/views.py @@ -1,5 +1,6 @@ import os +import requests from django.utils import timezone from urllib.parse import urljoin @@ -16,11 +17,14 @@ from django.http import ( HttpResponse, HttpResponseNotFound, HttpResponseRedirect, + HttpRequest, ) from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse +from django.utils.decorators import method_decorator from django.views import View +from django.views.decorators.cache import never_cache from django.views.generic import TemplateView from config.settings import ENABLE_DB_CACHE @@ -942,3 +946,57 @@ class RedirectToLibrariesView(BaseRedirectView): if requested_version == "release": new_path = "/libraries/" return HttpResponseRedirect(new_path) + + +@method_decorator(never_cache, name="dispatch") +class QRCodeView(View): + """Handles QR code urls, sending them to Plausible, then redirecting to the desired url. + + QR code urls are formatted /qrc//desired/path/to/content/, and will + result in a redirect to /desired/path/to/content/. + + E.g. https://www.boost.org/qrc/pv-01/library/latest/beast/ will send this full url to Plausible, + then redirect to https://www.boost.org/library/latest/beast/ + """ + + def get(self, request: HttpRequest, campaign_identifier: str, main_path: str = ""): + absolute_url = request.build_absolute_uri(request.path) + referrer = request.headers.get("referer", "") + user_agent = request.headers.get("user-agent", "") + + plausible_payload = { + "name": "pageview", + "domain": "qrc.boost.org", + "url": absolute_url, + "referrer": referrer, + } + + headers = {"Content-Type": "application/json", "User-Agent": user_agent} + + client_ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + client_ip = client_ip or request.META.get("REMOTE_ADDR") + + if client_ip: + headers["X-Forwarded-For"] = client_ip + + try: + requests.post( + "https://plausible.io/api/event", + json=plausible_payload, + headers=headers, + timeout=2.0, + ) + except Exception as e: + # Don’t interrupt the redirect - just log it + logger.error(f"Plausible event post failed: {e}") + + # Now that we've sent the request url to plausible, we can redirect to the main_path + # Preserve the original querystring, if any. + # Example: /qrc/3/library/latest/algorithm/?x=1 -> /library/latest/algorithm/?x=1 + # `main_path` is everything after qrc// thanks to . + redirect_path = "/" + main_path if main_path else "/" + qs = request.META.get("QUERY_STRING") + if qs: + redirect_path = f"{redirect_path}?{qs}" + + return HttpResponseRedirect(redirect_path) diff --git a/docker/Dockerfile b/docker/Dockerfile index f1c6c4ce..d200df23 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:experimental -FROM python:3.11-slim AS builder-py +FROM python:3.13-slim AS builder-py ARG LOCAL_DEVELOPMENT @@ -42,7 +42,7 @@ RUN yarn build # Final image. -FROM python:3.11-slim AS release +FROM python:3.13-slim AS release RUN apt update && apt install -y git libpq-dev ruby ruby-dev && rm -rf /var/lib/apt/lists/* diff --git a/docs/dependencies.md b/docs/dependencies.md index 20f77514..d48c9bb2 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -7,3 +7,14 @@ 1. Run `just pip-compile`, which will add the dependency to `requirements.txt` 1. Run `just rebuild` to rebuild your Docker image to include the new dependencies 2. Run `just up` and continue with development + +## Upgrading dependencies + +To upgrade all dependencies to their latest versions, run: + +1. `just pip-compile-upgrade`. +2. Get the django version from requirements.txt and set the `DJANGO_VERSION` value in /justfile +3. Update the `--target-version` args value for django-upgrade in .pre-commit-config.yaml to match +3. In a venv with installed packages run `just run-django-upgrade` to upgrade python code. +4. `just build` to create new docker images. +5. Tear down docker containers and restart with the newly built images, then test. diff --git a/justfile b/justfile index 7b4156ff..1c1b8c1e 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,7 @@ set dotenv-load := false COMPOSE_FILE := "docker-compose.yml" ENV_FILE := ".env" +DJANGO_VERSION := "5.2" @_default: just --list @@ -122,6 +123,10 @@ alias shell := console fi @cd development-tofu; direnv allow && tofu destroy +@run-django-upgrade: + [ -n "${VIRTUAL_ENV-}" ] || { echo "❌ Activate your venv first."; exit 1; } + -git ls-files -z -- '*.py' | xargs -0r django-upgrade --target {{DJANGO_VERSION}} + # Dependency management @pip-compile ARGS='': ## rebuilds our pip requirements docker compose run --rm web uv pip compile {{ ARGS }} ./requirements.in --no-strip-extras --output-file ./requirements.txt diff --git a/marketing/views.py b/marketing/views.py index f59aabb5..473184f1 100644 --- a/marketing/views.py +++ b/marketing/views.py @@ -28,8 +28,8 @@ class PlausibleRedirectView(View): def get(self, request: HttpRequest, campaign_identifier: str, main_path: str = ""): absolute_url = request.build_absolute_uri(request.path) - referrer = request.META.get("HTTP_REFERER", "") - user_agent = request.META.get("HTTP_USER_AGENT", "") + referrer = request.headers.get("referer", "") + user_agent = request.headers.get("user-agent", "") plausible_payload = { "name": "pageview", @@ -40,7 +40,7 @@ class PlausibleRedirectView(View): headers = {"Content-Type": "application/json", "User-Agent": user_agent} - client_ip = request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0].strip() + client_ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip() client_ip = client_ip or request.META.get("REMOTE_ADDR") if client_ip: @@ -85,7 +85,7 @@ class WhitePaperView(SuccessMessageMixin, CreateView): if original_referrer := self.request.session.get("original_referrer", ""): self.referrer = original_referrer else: - self.referrer = self.request.META.get("HTTP_REFERER", "") + self.referrer = self.request.headers.get("referer", "") return super().dispatch(request, *args, **kwargs) def get_template_names(self): diff --git a/news/feeds.py b/news/feeds.py index a234d128..1dddf0c1 100644 --- a/news/feeds.py +++ b/news/feeds.py @@ -1,7 +1,7 @@ -from datetime import datetime +from datetime import datetime, timezone from django.contrib.syndication.views import Feed from django.utils.feedgenerator import Atom1Feed -from django.utils.timezone import make_aware, utc +from django.utils.timezone import make_aware from django.utils.html import urlize, linebreaks from .models import Entry @@ -22,7 +22,7 @@ class RSSNewsFeed(Feed): publish_date = item.publish_at if publish_date: datetime_obj = datetime.combine(publish_date, datetime.min.time()) - aware_datetime_obj = make_aware(datetime_obj, timezone=utc) + aware_datetime_obj = make_aware(datetime_obj, timezone=timezone.utc) return aware_datetime_obj def item_description(self, item): diff --git a/news/notifications.py b/news/notifications.py index 90fad54a..4b945d7b 100644 --- a/news/notifications.py +++ b/news/notifications.py @@ -60,7 +60,7 @@ def generate_magic_approval_link(entry_slug: str, moderator_id: int): def send_email_news_needs_moderation(request, entry): recipient_list = [ u - for u in moderators().select_related("preferences").only("email") + for u in moderators().select_related("preferences").only("email", "preferences") if entry.tag in u.preferences.allow_notification_others_news_needs_moderation ] if not recipient_list: diff --git a/news/tests/test_feeds.py b/news/tests/test_feeds.py index 66c48f6c..e1714d1e 100644 --- a/news/tests/test_feeds.py +++ b/news/tests/test_feeds.py @@ -1,5 +1,5 @@ -from datetime import datetime, timedelta -from django.utils.timezone import make_aware, now, utc +from datetime import datetime, timedelta, timezone +from django.utils.timezone import make_aware, now from model_bakery import baker from ..feeds import RSSNewsFeed, AtomNewsFeed @@ -22,7 +22,8 @@ def test_item_pubdate(make_entry): feed = RSSNewsFeed() published_entry = make_entry(moderator=baker.make("users.User"), approved_at=now()) expected_datetime = make_aware( - datetime.combine(published_entry.publish_at, datetime.min.time()), timezone=utc + datetime.combine(published_entry.publish_at, datetime.min.time()), + timezone=timezone.utc, ) assert feed.item_pubdate(published_entry) == expected_datetime @@ -51,6 +52,7 @@ def test_item_pubdate_atom(make_entry): feed = AtomNewsFeed() published_entry = make_entry(moderator=baker.make("users.User"), approved_at=now()) expected_datetime = make_aware( - datetime.combine(published_entry.publish_at, datetime.min.time()), timezone=utc + datetime.combine(published_entry.publish_at, datetime.min.time()), + timezone=timezone.utc, ) assert feed.item_pubdate(published_entry) == expected_datetime diff --git a/pyproject.toml b/pyproject.toml index 136dc41a..41fdea45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ whitelist-regex = ["test_.*"] [tool.ruff] line-length = 88 -target-version = "py311" +target-version = "py313" [tool.black] line-length = 88 diff --git a/requirements-dev.in b/requirements-dev.in index 0082442a..31ef8921 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,3 +1,3 @@ -c requirements.txt django-debug-toolbar -pydevd-pycharm==243.26053.29 # pinned to appropriate version for current pycharm +pydevd-pycharm==252.26830.99 # pinned to appropriate version for current pycharm diff --git a/requirements-dev.txt b/requirements-dev.txt index f1a54f7b..6a027f89 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,16 +1,16 @@ # This file was autogenerated by uv via the following command: # uv pip compile ./requirements-dev.in --no-strip-extras --output-file ./requirements-dev.txt -asgiref==3.9.1 +asgiref==3.10.0 # via # -c ./requirements.txt # django -django==4.2.24 +django==5.2.7 # via # -c ./requirements.txt # django-debug-toolbar django-debug-toolbar==6.0.0 # via -r ./requirements-dev.in -pydevd-pycharm==243.26053.29 +pydevd-pycharm==252.26830.99 # via -r ./requirements-dev.in sqlparse==0.5.3 # via diff --git a/requirements.in b/requirements.in index eadb2768..7653c1ec 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,4 @@ -Django>=4.0, <5.0 +Django>=5.0, <6.0 bumpversion django-admin-env-notice django-allauth @@ -12,7 +12,7 @@ django-health-check django-imagekit django-oauth-toolkit django-redis -django-rest-auth +django-upgrade django-widget-tweaks djangorestframework environs[django] diff --git a/requirements.txt b/requirements.txt index 2a7e14a0..86770c13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,17 +2,17 @@ # uv pip compile ./requirements.in --no-strip-extras --output-file ./requirements.txt aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.15 +aiohttp==3.13.1 # via algoliasearch aiosignal==1.4.0 # via aiohttp -algoliasearch==4.27.0 +algoliasearch==4.30.0 # via -r ./requirements.in amqp==5.3.1 # via kombu annotated-types==0.7.0 # via pydantic -anyio==4.10.0 +anyio==4.11.0 # via # httpx # openai @@ -22,7 +22,7 @@ argon2-cffi==25.1.0 # via minio argon2-cffi-bindings==25.1.0 # via argon2-cffi -asgiref==3.9.1 +asgiref==3.10.0 # via # django # django-allauth @@ -32,21 +32,21 @@ asttokens==3.0.0 # via stack-data async-timeout==5.0.1 # via algoliasearch -attrs==25.3.0 +attrs==25.4.0 # via # aiohttp # interrogate -beautifulsoup4==4.13.5 +beautifulsoup4==4.14.2 # via -r ./requirements.in -billiard==4.2.1 +billiard==4.2.2 # via celery -black==25.1.0 +black==25.9.0 # via -r ./requirements.in -boto3==1.40.24 +boto3==1.40.56 # via # -r ./requirements.in # django-bakery -botocore==1.40.24 +botocore==1.40.56 # via # boto3 # s3transfer @@ -56,14 +56,14 @@ bumpversion==0.6.0 # via -r ./requirements.in celery==5.5.3 # via -r ./requirements.in -certifi==2025.8.3 +certifi==2025.10.5 # via # elasticsearch # httpcore # httpx # minio # requests -cffi==1.17.1 +cffi==2.0.0 # via # argon2-cffi-bindings # cryptography @@ -71,9 +71,9 @@ cfgv==3.4.0 # via pre-commit chardet==5.2.0 # via -r ./requirements.in -charset-normalizer==3.4.3 +charset-normalizer==3.4.4 # via requests -click==8.2.1 +click==8.3.0 # via # black # celery @@ -92,9 +92,9 @@ colorama==0.4.6 # via interrogate contourpy==1.3.3 # via matplotlib -coverage[toml]==7.10.6 +coverage[toml]==7.11.0 # via pytest-cov -cryptography==45.0.7 +cryptography==46.0.3 # via # -r ./requirements.in # jwcrypto @@ -107,11 +107,11 @@ distlib==0.4.0 # via virtualenv distro==1.9.0 # via openai -dj-database-url==2.2.0 +dj-database-url==3.0.1 # via environs dj-email-url==1.0.6 # via environs -django==4.2.24 +django==5.2.7 # via # -r ./requirements.in # dj-database-url @@ -126,13 +126,12 @@ django==4.2.24 # django-js-asset # django-oauth-toolkit # django-redis - # django-rest-auth # django-storages # djangorestframework # model-bakery django-admin-env-notice==1.0.1 # via -r ./requirements.in -django-allauth[socialaccount]==65.11.1 +django-allauth[socialaccount]==65.12.1 # via -r ./requirements.in django-anymail[mailgun]==13.1 # via -r ./requirements.in @@ -144,7 +143,7 @@ django-cache-url==3.4.5 # via environs django-click==2.4.1 # via -r ./requirements.in -django-cors-headers==4.7.0 +django-cors-headers==4.9.0 # via -r ./requirements.in django-countries==7.6.1 # via -r ./requirements.in @@ -156,53 +155,51 @@ django-haystack==3.3.0 # via -r ./requirements.in django-health-check==3.20.0 # via -r ./requirements.in -django-imagekit==5.0.0 +django-imagekit==6.0.0 # via -r ./requirements.in django-js-asset==3.1.2 # via django-mptt django-mptt==0.14.0 # via -r ./requirements.in -django-oauth-toolkit==3.0.1 +django-oauth-toolkit==3.1.0 # via -r ./requirements.in django-redis==6.0.0 # via -r ./requirements.in -django-rest-auth==0.9.5 - # via -r ./requirements.in django-storages==1.14.6 # via -r ./requirements.in django-test-plus==2.3.0 # via -r ./requirements.in django-tracer==0.9.3 # via -r ./requirements.in +django-upgrade==1.29.0 + # via -r ./requirements.in django-widget-tweaks==1.5.0 # via -r ./requirements.in djangorestframework==3.16.1 - # via - # -r ./requirements.in - # django-rest-auth -elasticsearch==7.17.12 + # via -r ./requirements.in +elasticsearch==7.9.1 # via -r ./requirements.in environs[django]==14.3.0 # via -r ./requirements.in executing==2.2.1 # via stack-data -faker==37.6.0 +faker==37.11.0 # via -r ./requirements.in -fastcore==1.8.8 +fastcore==1.8.13 # via ghapi -filelock==3.19.1 +filelock==3.20.0 # via virtualenv -fonttools==4.59.2 +fonttools==4.60.1 # via matplotlib -frozenlist==1.7.0 +frozenlist==1.8.0 # via # aiohttp # aiosignal fs==2.4.16 # via django-bakery -gevent==25.8.2 +gevent==25.9.1 # via -r ./requirements.in -ghapi==1.0.6 +ghapi==1.0.8 # via -r ./requirements.in greenlet==3.2.4 # via @@ -216,19 +213,19 @@ httpcore==1.0.9 # via httpx httpx==0.28.1 # via openai -identify==2.6.1 +identify==2.6.15 # via pre-commit -idna==3.10 +idna==3.11 # via # anyio # httpx # requests # yarl -iniconfig==2.1.0 +iniconfig==2.3.0 # via pytest interrogate==1.7.0 # via -r ./requirements.in -ipython==9.5.0 +ipython==9.6.0 # via -r ./requirements.in ipython-pygments-lexers==1.1.1 # via ipython @@ -236,7 +233,7 @@ itsdangerous==2.2.0 # via -r ./requirements.in jedi==0.19.2 # via ipython -jiter==0.10.0 +jiter==0.11.1 # via openai jmespath==1.0.1 # via @@ -252,21 +249,21 @@ kiwisolver==1.4.9 # via matplotlib kombu==5.5.4 # via celery -lxml==6.0.1 +lxml==6.0.2 # via -r ./requirements.in marshmallow==4.0.1 # via environs -matplotlib==3.10.6 +matplotlib==3.10.7 # via wordcloud matplotlib-inline==0.1.7 # via ipython -minio==7.2.16 +minio==7.2.18 # via -r ./requirements.in -mistletoe==1.4.0 +mistletoe==1.5.0 # via -r ./requirements.in model-bakery==1.20.5 # via -r ./requirements.in -multidict==6.6.4 +multidict==6.7.0 # via # aiohttp # yarl @@ -274,7 +271,7 @@ mypy-extensions==1.1.0 # via black nodeenv==1.9.1 # via pre-commit -numpy==2.3.2 +numpy==2.3.4 # via # contourpy # matplotlib @@ -283,9 +280,9 @@ oauthlib==3.3.1 # via # django-allauth # django-oauth-toolkit -openai==1.102.0 +openai==2.6.0 # via -r ./requirements.in -packaging==24.1 +packaging==25.0 # via # black # django-haystack @@ -303,13 +300,13 @@ pexpect==4.9.0 # via ipython pilkit==3.0 # via django-imagekit -pillow==11.3.0 +pillow==12.0.0 # via # -r ./requirements.in # matplotlib # pilkit # wordcloud -platformdirs==4.4.0 +platformdirs==4.5.0 # via # black # virtualenv @@ -323,13 +320,13 @@ prompt-toolkit==3.0.52 # via # click-repl # ipython -propcache==0.3.2 +propcache==0.4.1 # via # aiohttp # yarl psycogreen==1.0.2 # via -r ./requirements.in -psycopg2-binary==2.9.10 +psycopg2-binary==2.9.11 # via -r ./requirements.in ptyprocess==0.7.0 # via pexpect @@ -337,33 +334,33 @@ pure-eval==0.2.3 # via stack-data py==1.11.0 # via interrogate -pycparser==2.22 +pycparser==2.23 # via cffi pycryptodome==3.23.0 # via minio -pydantic==2.11.9 +pydantic==2.12.3 # via # algoliasearch # openai -pydantic-core==2.33.2 +pydantic-core==2.41.4 # via pydantic pygments==2.19.2 # via # ipython # ipython-pygments-lexers # pytest -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.1 # via # django-allauth # redis -pyparsing==3.2.0 +pyparsing==3.2.5 # via matplotlib pytest==8.4.2 # via # -r ./requirements.in # pytest-cov # pytest-django -pytest-cov==6.2.1 +pytest-cov==7.0.0 # via -r ./requirements.in pytest-django==4.11.1 # via -r ./requirements.in @@ -378,9 +375,11 @@ python-dotenv==1.1.1 # via environs python-frontmatter==1.1.0 # via -r ./requirements.in -python-json-logger==3.3.0 +python-json-logger==4.0.0 # via -r ./requirements.in -pyyaml==6.0.2 +pytokens==0.2.0 + # via black +pyyaml==6.0.3 # via # pre-commit # python-frontmatter @@ -399,26 +398,24 @@ requests==2.32.5 # responses responses==0.25.8 # via -r ./requirements.in -s3transfer==0.13.1 +s3transfer==0.14.0 # via boto3 setuptools==80.9.0 # via # fs # zope-event - # zope-interface six==1.17.0 # via # django-bakery - # django-rest-auth # fs # python-dateutil -slack-sdk==3.36.0 +slack-sdk==3.37.0 # via -r ./requirements.in sniffio==1.3.1 # via # anyio # openai -soupsieve==2.6 +soupsieve==2.8 # via beautifulsoup4 sqlparse==0.5.3 # via django @@ -428,6 +425,8 @@ structlog==25.4.0 # via -r ./requirements.in tabulate==0.9.0 # via interrogate +tokenize-rt==6.2.0 + # via django-upgrade tqdm==4.67.1 # via openai traitlets==5.14.3 @@ -439,7 +438,6 @@ typing-extensions==4.15.0 # aiosignal # anyio # beautifulsoup4 - # dj-database-url # django-countries # ipython # jwcrypto @@ -448,7 +446,7 @@ typing-extensions==4.15.0 # pydantic # pydantic-core # typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic tzdata==2025.2 # via @@ -456,7 +454,7 @@ tzdata==2025.2 # kombu unidecode==1.4.0 # via -r ./requirements.in -urllib3==1.26.20 +urllib3==2.5.0 # via # algoliasearch # botocore @@ -465,26 +463,26 @@ urllib3==1.26.20 # minio # requests # responses -uv==0.8.15 +uv==0.9.5 # via -r ./requirements.in vine==5.1.0 # via # amqp # celery # kombu -virtualenv==20.34.0 +virtualenv==20.35.3 # via pre-commit -wcwidth==0.2.13 +wcwidth==0.2.14 # via prompt-toolkit wheel==0.45.1 # via -r ./requirements.in -whitenoise==6.9.0 +whitenoise==6.11.0 # via -r ./requirements.in wordcloud==1.9.4 # via -r ./requirements.in -yarl==1.20.1 +yarl==1.22.0 # via aiohttp -zope-event==5.1.1 +zope-event==6.0 # via gevent -zope-interface==7.2 +zope-interface==8.0.1 # via gevent diff --git a/slack/migrations/0001_initial.py b/slack/migrations/0001_initial.py index 87d66484..4da1ebad 100644 --- a/slack/migrations/0001_initial.py +++ b/slack/migrations/0001_initial.py @@ -146,7 +146,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="thread", constraint=models.CheckConstraint( - check=django.db.models.lookups.GreaterThanOrEqual( + condition=django.db.models.lookups.GreaterThanOrEqual( django.db.models.functions.comparison.Cast( "last_update_ts", output_field=models.FloatField() ), @@ -172,7 +172,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="channelupdategap", constraint=models.CheckConstraint( - check=django.db.models.lookups.GreaterThan( + condition=django.db.models.lookups.GreaterThan( django.db.models.functions.comparison.Cast( "newest_message_ts", output_field=models.FloatField() ), diff --git a/slack/models.py b/slack/models.py index 180762f1..28e14848 100644 --- a/slack/models.py +++ b/slack/models.py @@ -61,7 +61,7 @@ class ChannelUpdateGap(models.Model): class Meta: constraints = [ models.CheckConstraint( - check=models.lookups.GreaterThan( + condition=models.lookups.GreaterThan( models.functions.Cast( "newest_message_ts", output_field=models.FloatField() ), @@ -110,7 +110,7 @@ class Thread(models.Model): unique_together = [("channel", "thread_ts")] constraints = [ models.CheckConstraint( - check=models.lookups.GreaterThanOrEqual( + condition=models.lookups.GreaterThanOrEqual( models.functions.Cast( "last_update_ts", output_field=models.FloatField() ), diff --git a/users/admin.py b/users/admin.py index 9890cf62..0e622244 100644 --- a/users/admin.py +++ b/users/admin.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from .models import User +@admin.register(User) class EmailUserAdmin(UserAdmin): fieldsets = ( (None, {"fields": ("email", "password")}), @@ -59,6 +60,3 @@ class EmailUserAdmin(UserAdmin): "claimed", ) search_fields = ("email", "display_name__unaccent") - - -admin.site.register(User, EmailUserAdmin) diff --git a/users/views.py b/users/views.py index 777befe7..5f276332 100644 --- a/users/views.py +++ b/users/views.py @@ -330,7 +330,7 @@ class UserAvatar(TemplateView): ): # check if user is on pages that require CSRF but don't require login # (auth pages where anonymous users submit forms) - referer = self.request.META.get("HTTP_REFERER", "") + referer = self.request.headers.get("referer", "") current_path = self.request.path # paths that anonymous users can access and have forms diff --git a/versions/feeds.py b/versions/feeds.py index 7cc4f70d..dfd27d45 100644 --- a/versions/feeds.py +++ b/versions/feeds.py @@ -1,7 +1,7 @@ -from datetime import datetime +from datetime import datetime, timezone from django.contrib.syndication.views import Feed from django.utils.feedgenerator import Atom1Feed -from django.utils.timezone import make_aware, utc +from django.utils.timezone import make_aware from core.models import RenderedContent from .models import Version @@ -24,7 +24,7 @@ class RSSVersionFeed(Feed): release_date = item.release_date if release_date: datetime_obj = datetime.combine(release_date, datetime.min.time()) - aware_datetime_obj = make_aware(datetime_obj, timezone=utc) + aware_datetime_obj = make_aware(datetime_obj, timezone=timezone.utc) return aware_datetime_obj def item_description(self, item): diff --git a/versions/tests/test_feeds.py b/versions/tests/test_feeds.py index 14fbcc32..25f8d014 100644 --- a/versions/tests/test_feeds.py +++ b/versions/tests/test_feeds.py @@ -1,5 +1,5 @@ -from datetime import datetime -from django.utils.timezone import make_aware, utc +from datetime import datetime, timezone +from django.utils.timezone import make_aware from ..feeds import RSSVersionFeed, AtomVersionFeed @@ -16,7 +16,8 @@ def test_items(version, old_version): def test_item_pubdate(version): feed = RSSVersionFeed() expected_datetime = make_aware( - datetime.combine(version.release_date, datetime.min.time()), timezone=utc + datetime.combine(version.release_date, datetime.min.time()), + timezone=timezone.utc, ) assert feed.item_pubdate(version) == expected_datetime @@ -51,6 +52,7 @@ def test_items_atom(version, old_version): def test_item_pubdate_atom(version): feed = AtomVersionFeed() expected_datetime = make_aware( - datetime.combine(version.release_date, datetime.min.time()), timezone=utc + datetime.combine(version.release_date, datetime.min.time()), + timezone=timezone.utc, ) assert feed.item_pubdate(version) == expected_datetime