Upgrade django to 5.2, python to 3.13 (#1915)

This commit is contained in:
daveoconnor
2025-10-22 13:24:26 -07:00
committed by GitHub
parent 1ec3cba5e3
commit 5f022aca0a
26 changed files with 207 additions and 130 deletions

View File

@@ -25,7 +25,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:12 image: postgres:16
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
@@ -44,10 +44,10 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python 3.11 - name: Set up Python 3.13
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
@@ -133,10 +133,10 @@ jobs:
run: | run: |
git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 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 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |

View File

@@ -15,7 +15,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:12 image: postgres:16
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
@@ -34,10 +34,10 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python 3.11 - name: Set up Python 3.13
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
@@ -85,10 +85,10 @@ jobs:
run: | run: |
git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 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 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |

View File

@@ -1,9 +1,14 @@
default_language_version: default_language_version:
python: python3.11 python: python3.13
exclude: .*migrations\/.*|static\/img\/.*|static\/animations\/.*|static\/js\/boost-gecko\/.*|kube\/boost\/templates\/.*\.yaml exclude: .*migrations\/.*|static\/img\/.*|static\/animations\/.*|static\/js\/boost-gecko\/.*|kube\/boost\/templates\/.*\.yaml
repos: 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 - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v5.0.0
hooks: hooks:

View File

@@ -13,7 +13,7 @@ Links:
## Local Development Setup ## 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). Instructions to install those packages are included in [development_setup_notes.md](docs/development_setup_notes.md).

View File

@@ -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 https://developers.google.com/calendar/api/v3/reference/events/list
""" """
if not min_time: if not min_time:
min_time = ( # 'Z' indicates UTC time
datetime.datetime.utcnow().isoformat() + "Z" min_time = datetime.datetime.now(datetime.timezone.utc).isoformat() + "Z"
) # 'Z' indicates UTC time
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}" 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"} headers = {"Accept": "application/json"}

View File

@@ -28,7 +28,7 @@ class Migration(migrations.Migration):
migrations.AddConstraint( migrations.AddConstraint(
model_name="sitesettings", model_name="sitesettings",
constraint=models.CheckConstraint( 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( migrations.AlterModelOptions(

View File

@@ -72,7 +72,7 @@ class SiteSettings(models.Model):
# check constraint to only allow id=1 to exist # check constraint to only allow id=1 to exist
models.CheckConstraint( models.CheckConstraint(
name="%(app_label)s_%(class)s_single_instance", name="%(app_label)s_%(class)s_single_instance",
check=models.Q(id=1), condition=models.Q(id=1),
), ),
] ]
verbose_name_plural = "Site Settings" verbose_name_plural = "Site Settings"

View File

@@ -1,5 +1,6 @@
import os import os
import requests
from django.utils import timezone from django.utils import timezone
from urllib.parse import urljoin from urllib.parse import urljoin
@@ -16,11 +17,14 @@ from django.http import (
HttpResponse, HttpResponse,
HttpResponseNotFound, HttpResponseNotFound,
HttpResponseRedirect, HttpResponseRedirect,
HttpRequest,
) )
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.cache import never_cache
from django.views.generic import TemplateView from django.views.generic import TemplateView
from config.settings import ENABLE_DB_CACHE from config.settings import ENABLE_DB_CACHE
@@ -942,3 +946,57 @@ class RedirectToLibrariesView(BaseRedirectView):
if requested_version == "release": if requested_version == "release":
new_path = "/libraries/" new_path = "/libraries/"
return HttpResponseRedirect(new_path) 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/<campaign_identifier>/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:
# Dont 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/<campaign>/ thanks to <path:main_path>.
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)

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:experimental # syntax = docker/dockerfile:experimental
FROM python:3.11-slim AS builder-py FROM python:3.13-slim AS builder-py
ARG LOCAL_DEVELOPMENT ARG LOCAL_DEVELOPMENT
@@ -42,7 +42,7 @@ RUN yarn build
# Final image. # 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/* RUN apt update && apt install -y git libpq-dev ruby ruby-dev && rm -rf /var/lib/apt/lists/*

View File

@@ -7,3 +7,14 @@
1. Run `just pip-compile`, which will add the dependency to `requirements.txt` 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 1. Run `just rebuild` to rebuild your Docker image to include the new dependencies
2. Run `just up` and continue with development 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.

View File

@@ -1,6 +1,7 @@
set dotenv-load := false set dotenv-load := false
COMPOSE_FILE := "docker-compose.yml" COMPOSE_FILE := "docker-compose.yml"
ENV_FILE := ".env" ENV_FILE := ".env"
DJANGO_VERSION := "5.2"
@_default: @_default:
just --list just --list
@@ -122,6 +123,10 @@ alias shell := console
fi fi
@cd development-tofu; direnv allow && tofu destroy @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 # Dependency management
@pip-compile ARGS='': ## rebuilds our pip requirements @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 docker compose run --rm web uv pip compile {{ ARGS }} ./requirements.in --no-strip-extras --output-file ./requirements.txt

View File

@@ -28,8 +28,8 @@ class PlausibleRedirectView(View):
def get(self, request: HttpRequest, campaign_identifier: str, main_path: str = ""): def get(self, request: HttpRequest, campaign_identifier: str, main_path: str = ""):
absolute_url = request.build_absolute_uri(request.path) absolute_url = request.build_absolute_uri(request.path)
referrer = request.META.get("HTTP_REFERER", "") referrer = request.headers.get("referer", "")
user_agent = request.META.get("HTTP_USER_AGENT", "") user_agent = request.headers.get("user-agent", "")
plausible_payload = { plausible_payload = {
"name": "pageview", "name": "pageview",
@@ -40,7 +40,7 @@ class PlausibleRedirectView(View):
headers = {"Content-Type": "application/json", "User-Agent": user_agent} 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") client_ip = client_ip or request.META.get("REMOTE_ADDR")
if client_ip: if client_ip:
@@ -85,7 +85,7 @@ class WhitePaperView(SuccessMessageMixin, CreateView):
if original_referrer := self.request.session.get("original_referrer", ""): if original_referrer := self.request.session.get("original_referrer", ""):
self.referrer = original_referrer self.referrer = original_referrer
else: else:
self.referrer = self.request.META.get("HTTP_REFERER", "") self.referrer = self.request.headers.get("referer", "")
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_template_names(self): def get_template_names(self):

View File

@@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime, timezone
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed 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 django.utils.html import urlize, linebreaks
from .models import Entry from .models import Entry
@@ -22,7 +22,7 @@ class RSSNewsFeed(Feed):
publish_date = item.publish_at publish_date = item.publish_at
if publish_date: if publish_date:
datetime_obj = datetime.combine(publish_date, datetime.min.time()) 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 return aware_datetime_obj
def item_description(self, item): def item_description(self, item):

View File

@@ -60,7 +60,7 @@ def generate_magic_approval_link(entry_slug: str, moderator_id: int):
def send_email_news_needs_moderation(request, entry): def send_email_news_needs_moderation(request, entry):
recipient_list = [ recipient_list = [
u 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 entry.tag in u.preferences.allow_notification_others_news_needs_moderation
] ]
if not recipient_list: if not recipient_list:

View File

@@ -1,5 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from django.utils.timezone import make_aware, now, utc from django.utils.timezone import make_aware, now
from model_bakery import baker from model_bakery import baker
from ..feeds import RSSNewsFeed, AtomNewsFeed from ..feeds import RSSNewsFeed, AtomNewsFeed
@@ -22,7 +22,8 @@ def test_item_pubdate(make_entry):
feed = RSSNewsFeed() feed = RSSNewsFeed()
published_entry = make_entry(moderator=baker.make("users.User"), approved_at=now()) published_entry = make_entry(moderator=baker.make("users.User"), approved_at=now())
expected_datetime = make_aware( 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 assert feed.item_pubdate(published_entry) == expected_datetime
@@ -51,6 +52,7 @@ def test_item_pubdate_atom(make_entry):
feed = AtomNewsFeed() feed = AtomNewsFeed()
published_entry = make_entry(moderator=baker.make("users.User"), approved_at=now()) published_entry = make_entry(moderator=baker.make("users.User"), approved_at=now())
expected_datetime = make_aware( 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 assert feed.item_pubdate(published_entry) == expected_datetime

View File

@@ -6,7 +6,7 @@ whitelist-regex = ["test_.*"]
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
target-version = "py311" target-version = "py313"
[tool.black] [tool.black]
line-length = 88 line-length = 88

View File

@@ -1,3 +1,3 @@
-c requirements.txt -c requirements.txt
django-debug-toolbar 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

View File

@@ -1,16 +1,16 @@
# This file was autogenerated by uv via the following command: # This file was autogenerated by uv via the following command:
# uv pip compile ./requirements-dev.in --no-strip-extras --output-file ./requirements-dev.txt # uv pip compile ./requirements-dev.in --no-strip-extras --output-file ./requirements-dev.txt
asgiref==3.9.1 asgiref==3.10.0
# via # via
# -c ./requirements.txt # -c ./requirements.txt
# django # django
django==4.2.24 django==5.2.7
# via # via
# -c ./requirements.txt # -c ./requirements.txt
# django-debug-toolbar # django-debug-toolbar
django-debug-toolbar==6.0.0 django-debug-toolbar==6.0.0
# via -r ./requirements-dev.in # via -r ./requirements-dev.in
pydevd-pycharm==243.26053.29 pydevd-pycharm==252.26830.99
# via -r ./requirements-dev.in # via -r ./requirements-dev.in
sqlparse==0.5.3 sqlparse==0.5.3
# via # via

View File

@@ -1,4 +1,4 @@
Django>=4.0, <5.0 Django>=5.0, <6.0
bumpversion bumpversion
django-admin-env-notice django-admin-env-notice
django-allauth django-allauth
@@ -12,7 +12,7 @@ django-health-check
django-imagekit django-imagekit
django-oauth-toolkit django-oauth-toolkit
django-redis django-redis
django-rest-auth django-upgrade
django-widget-tweaks django-widget-tweaks
djangorestframework djangorestframework
environs[django] environs[django]

View File

@@ -2,17 +2,17 @@
# uv pip compile ./requirements.in --no-strip-extras --output-file ./requirements.txt # uv pip compile ./requirements.in --no-strip-extras --output-file ./requirements.txt
aiohappyeyeballs==2.6.1 aiohappyeyeballs==2.6.1
# via aiohttp # via aiohttp
aiohttp==3.12.15 aiohttp==3.13.1
# via algoliasearch # via algoliasearch
aiosignal==1.4.0 aiosignal==1.4.0
# via aiohttp # via aiohttp
algoliasearch==4.27.0 algoliasearch==4.30.0
# via -r ./requirements.in # via -r ./requirements.in
amqp==5.3.1 amqp==5.3.1
# via kombu # via kombu
annotated-types==0.7.0 annotated-types==0.7.0
# via pydantic # via pydantic
anyio==4.10.0 anyio==4.11.0
# via # via
# httpx # httpx
# openai # openai
@@ -22,7 +22,7 @@ argon2-cffi==25.1.0
# via minio # via minio
argon2-cffi-bindings==25.1.0 argon2-cffi-bindings==25.1.0
# via argon2-cffi # via argon2-cffi
asgiref==3.9.1 asgiref==3.10.0
# via # via
# django # django
# django-allauth # django-allauth
@@ -32,21 +32,21 @@ asttokens==3.0.0
# via stack-data # via stack-data
async-timeout==5.0.1 async-timeout==5.0.1
# via algoliasearch # via algoliasearch
attrs==25.3.0 attrs==25.4.0
# via # via
# aiohttp # aiohttp
# interrogate # interrogate
beautifulsoup4==4.13.5 beautifulsoup4==4.14.2
# via -r ./requirements.in # via -r ./requirements.in
billiard==4.2.1 billiard==4.2.2
# via celery # via celery
black==25.1.0 black==25.9.0
# via -r ./requirements.in # via -r ./requirements.in
boto3==1.40.24 boto3==1.40.56
# via # via
# -r ./requirements.in # -r ./requirements.in
# django-bakery # django-bakery
botocore==1.40.24 botocore==1.40.56
# via # via
# boto3 # boto3
# s3transfer # s3transfer
@@ -56,14 +56,14 @@ bumpversion==0.6.0
# via -r ./requirements.in # via -r ./requirements.in
celery==5.5.3 celery==5.5.3
# via -r ./requirements.in # via -r ./requirements.in
certifi==2025.8.3 certifi==2025.10.5
# via # via
# elasticsearch # elasticsearch
# httpcore # httpcore
# httpx # httpx
# minio # minio
# requests # requests
cffi==1.17.1 cffi==2.0.0
# via # via
# argon2-cffi-bindings # argon2-cffi-bindings
# cryptography # cryptography
@@ -71,9 +71,9 @@ cfgv==3.4.0
# via pre-commit # via pre-commit
chardet==5.2.0 chardet==5.2.0
# via -r ./requirements.in # via -r ./requirements.in
charset-normalizer==3.4.3 charset-normalizer==3.4.4
# via requests # via requests
click==8.2.1 click==8.3.0
# via # via
# black # black
# celery # celery
@@ -92,9 +92,9 @@ colorama==0.4.6
# via interrogate # via interrogate
contourpy==1.3.3 contourpy==1.3.3
# via matplotlib # via matplotlib
coverage[toml]==7.10.6 coverage[toml]==7.11.0
# via pytest-cov # via pytest-cov
cryptography==45.0.7 cryptography==46.0.3
# via # via
# -r ./requirements.in # -r ./requirements.in
# jwcrypto # jwcrypto
@@ -107,11 +107,11 @@ distlib==0.4.0
# via virtualenv # via virtualenv
distro==1.9.0 distro==1.9.0
# via openai # via openai
dj-database-url==2.2.0 dj-database-url==3.0.1
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==4.2.24 django==5.2.7
# via # via
# -r ./requirements.in # -r ./requirements.in
# dj-database-url # dj-database-url
@@ -126,13 +126,12 @@ django==4.2.24
# django-js-asset # django-js-asset
# django-oauth-toolkit # django-oauth-toolkit
# django-redis # django-redis
# django-rest-auth
# django-storages # django-storages
# djangorestframework # djangorestframework
# model-bakery # model-bakery
django-admin-env-notice==1.0.1 django-admin-env-notice==1.0.1
# via -r ./requirements.in # via -r ./requirements.in
django-allauth[socialaccount]==65.11.1 django-allauth[socialaccount]==65.12.1
# via -r ./requirements.in # via -r ./requirements.in
django-anymail[mailgun]==13.1 django-anymail[mailgun]==13.1
# via -r ./requirements.in # via -r ./requirements.in
@@ -144,7 +143,7 @@ django-cache-url==3.4.5
# via environs # via environs
django-click==2.4.1 django-click==2.4.1
# via -r ./requirements.in # via -r ./requirements.in
django-cors-headers==4.7.0 django-cors-headers==4.9.0
# via -r ./requirements.in # via -r ./requirements.in
django-countries==7.6.1 django-countries==7.6.1
# via -r ./requirements.in # via -r ./requirements.in
@@ -156,53 +155,51 @@ django-haystack==3.3.0
# via -r ./requirements.in # via -r ./requirements.in
django-health-check==3.20.0 django-health-check==3.20.0
# via -r ./requirements.in # via -r ./requirements.in
django-imagekit==5.0.0 django-imagekit==6.0.0
# via -r ./requirements.in # via -r ./requirements.in
django-js-asset==3.1.2 django-js-asset==3.1.2
# via django-mptt # via django-mptt
django-mptt==0.14.0 django-mptt==0.14.0
# via -r ./requirements.in # via -r ./requirements.in
django-oauth-toolkit==3.0.1 django-oauth-toolkit==3.1.0
# via -r ./requirements.in # via -r ./requirements.in
django-redis==6.0.0 django-redis==6.0.0
# via -r ./requirements.in # via -r ./requirements.in
django-rest-auth==0.9.5
# via -r ./requirements.in
django-storages==1.14.6 django-storages==1.14.6
# via -r ./requirements.in # via -r ./requirements.in
django-test-plus==2.3.0 django-test-plus==2.3.0
# via -r ./requirements.in # via -r ./requirements.in
django-tracer==0.9.3 django-tracer==0.9.3
# via -r ./requirements.in # via -r ./requirements.in
django-upgrade==1.29.0
# via -r ./requirements.in
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
# via -r ./requirements.in # via -r ./requirements.in
djangorestframework==3.16.1 djangorestframework==3.16.1
# via # via -r ./requirements.in
# -r ./requirements.in elasticsearch==7.9.1
# django-rest-auth
elasticsearch==7.17.12
# via -r ./requirements.in # via -r ./requirements.in
environs[django]==14.3.0 environs[django]==14.3.0
# via -r ./requirements.in # via -r ./requirements.in
executing==2.2.1 executing==2.2.1
# via stack-data # via stack-data
faker==37.6.0 faker==37.11.0
# via -r ./requirements.in # via -r ./requirements.in
fastcore==1.8.8 fastcore==1.8.13
# via ghapi # via ghapi
filelock==3.19.1 filelock==3.20.0
# via virtualenv # via virtualenv
fonttools==4.59.2 fonttools==4.60.1
# via matplotlib # via matplotlib
frozenlist==1.7.0 frozenlist==1.8.0
# via # via
# aiohttp # aiohttp
# aiosignal # aiosignal
fs==2.4.16 fs==2.4.16
# via django-bakery # via django-bakery
gevent==25.8.2 gevent==25.9.1
# via -r ./requirements.in # via -r ./requirements.in
ghapi==1.0.6 ghapi==1.0.8
# via -r ./requirements.in # via -r ./requirements.in
greenlet==3.2.4 greenlet==3.2.4
# via # via
@@ -216,19 +213,19 @@ httpcore==1.0.9
# via httpx # via httpx
httpx==0.28.1 httpx==0.28.1
# via openai # via openai
identify==2.6.1 identify==2.6.15
# via pre-commit # via pre-commit
idna==3.10 idna==3.11
# via # via
# anyio # anyio
# httpx # httpx
# requests # requests
# yarl # yarl
iniconfig==2.1.0 iniconfig==2.3.0
# via pytest # via pytest
interrogate==1.7.0 interrogate==1.7.0
# via -r ./requirements.in # via -r ./requirements.in
ipython==9.5.0 ipython==9.6.0
# via -r ./requirements.in # via -r ./requirements.in
ipython-pygments-lexers==1.1.1 ipython-pygments-lexers==1.1.1
# via ipython # via ipython
@@ -236,7 +233,7 @@ itsdangerous==2.2.0
# via -r ./requirements.in # via -r ./requirements.in
jedi==0.19.2 jedi==0.19.2
# via ipython # via ipython
jiter==0.10.0 jiter==0.11.1
# via openai # via openai
jmespath==1.0.1 jmespath==1.0.1
# via # via
@@ -252,21 +249,21 @@ kiwisolver==1.4.9
# via matplotlib # via matplotlib
kombu==5.5.4 kombu==5.5.4
# via celery # via celery
lxml==6.0.1 lxml==6.0.2
# via -r ./requirements.in # via -r ./requirements.in
marshmallow==4.0.1 marshmallow==4.0.1
# via environs # via environs
matplotlib==3.10.6 matplotlib==3.10.7
# via wordcloud # via wordcloud
matplotlib-inline==0.1.7 matplotlib-inline==0.1.7
# via ipython # via ipython
minio==7.2.16 minio==7.2.18
# via -r ./requirements.in # via -r ./requirements.in
mistletoe==1.4.0 mistletoe==1.5.0
# via -r ./requirements.in # via -r ./requirements.in
model-bakery==1.20.5 model-bakery==1.20.5
# via -r ./requirements.in # via -r ./requirements.in
multidict==6.6.4 multidict==6.7.0
# via # via
# aiohttp # aiohttp
# yarl # yarl
@@ -274,7 +271,7 @@ mypy-extensions==1.1.0
# via black # via black
nodeenv==1.9.1 nodeenv==1.9.1
# via pre-commit # via pre-commit
numpy==2.3.2 numpy==2.3.4
# via # via
# contourpy # contourpy
# matplotlib # matplotlib
@@ -283,9 +280,9 @@ oauthlib==3.3.1
# via # via
# django-allauth # django-allauth
# django-oauth-toolkit # django-oauth-toolkit
openai==1.102.0 openai==2.6.0
# via -r ./requirements.in # via -r ./requirements.in
packaging==24.1 packaging==25.0
# via # via
# black # black
# django-haystack # django-haystack
@@ -303,13 +300,13 @@ pexpect==4.9.0
# via ipython # via ipython
pilkit==3.0 pilkit==3.0
# via django-imagekit # via django-imagekit
pillow==11.3.0 pillow==12.0.0
# via # via
# -r ./requirements.in # -r ./requirements.in
# matplotlib # matplotlib
# pilkit # pilkit
# wordcloud # wordcloud
platformdirs==4.4.0 platformdirs==4.5.0
# via # via
# black # black
# virtualenv # virtualenv
@@ -323,13 +320,13 @@ prompt-toolkit==3.0.52
# via # via
# click-repl # click-repl
# ipython # ipython
propcache==0.3.2 propcache==0.4.1
# via # via
# aiohttp # aiohttp
# yarl # yarl
psycogreen==1.0.2 psycogreen==1.0.2
# via -r ./requirements.in # via -r ./requirements.in
psycopg2-binary==2.9.10 psycopg2-binary==2.9.11
# via -r ./requirements.in # via -r ./requirements.in
ptyprocess==0.7.0 ptyprocess==0.7.0
# via pexpect # via pexpect
@@ -337,33 +334,33 @@ pure-eval==0.2.3
# via stack-data # via stack-data
py==1.11.0 py==1.11.0
# via interrogate # via interrogate
pycparser==2.22 pycparser==2.23
# via cffi # via cffi
pycryptodome==3.23.0 pycryptodome==3.23.0
# via minio # via minio
pydantic==2.11.9 pydantic==2.12.3
# via # via
# algoliasearch # algoliasearch
# openai # openai
pydantic-core==2.33.2 pydantic-core==2.41.4
# via pydantic # via pydantic
pygments==2.19.2 pygments==2.19.2
# via # via
# ipython # ipython
# ipython-pygments-lexers # ipython-pygments-lexers
# pytest # pytest
pyjwt[crypto]==2.9.0 pyjwt[crypto]==2.10.1
# via # via
# django-allauth # django-allauth
# redis # redis
pyparsing==3.2.0 pyparsing==3.2.5
# via matplotlib # via matplotlib
pytest==8.4.2 pytest==8.4.2
# via # via
# -r ./requirements.in # -r ./requirements.in
# pytest-cov # pytest-cov
# pytest-django # pytest-django
pytest-cov==6.2.1 pytest-cov==7.0.0
# via -r ./requirements.in # via -r ./requirements.in
pytest-django==4.11.1 pytest-django==4.11.1
# via -r ./requirements.in # via -r ./requirements.in
@@ -378,9 +375,11 @@ python-dotenv==1.1.1
# via environs # via environs
python-frontmatter==1.1.0 python-frontmatter==1.1.0
# via -r ./requirements.in # via -r ./requirements.in
python-json-logger==3.3.0 python-json-logger==4.0.0
# via -r ./requirements.in # via -r ./requirements.in
pyyaml==6.0.2 pytokens==0.2.0
# via black
pyyaml==6.0.3
# via # via
# pre-commit # pre-commit
# python-frontmatter # python-frontmatter
@@ -399,26 +398,24 @@ requests==2.32.5
# responses # responses
responses==0.25.8 responses==0.25.8
# via -r ./requirements.in # via -r ./requirements.in
s3transfer==0.13.1 s3transfer==0.14.0
# via boto3 # via boto3
setuptools==80.9.0 setuptools==80.9.0
# via # via
# fs # fs
# zope-event # zope-event
# zope-interface
six==1.17.0 six==1.17.0
# via # via
# django-bakery # django-bakery
# django-rest-auth
# fs # fs
# python-dateutil # python-dateutil
slack-sdk==3.36.0 slack-sdk==3.37.0
# via -r ./requirements.in # via -r ./requirements.in
sniffio==1.3.1 sniffio==1.3.1
# via # via
# anyio # anyio
# openai # openai
soupsieve==2.6 soupsieve==2.8
# via beautifulsoup4 # via beautifulsoup4
sqlparse==0.5.3 sqlparse==0.5.3
# via django # via django
@@ -428,6 +425,8 @@ structlog==25.4.0
# via -r ./requirements.in # via -r ./requirements.in
tabulate==0.9.0 tabulate==0.9.0
# via interrogate # via interrogate
tokenize-rt==6.2.0
# via django-upgrade
tqdm==4.67.1 tqdm==4.67.1
# via openai # via openai
traitlets==5.14.3 traitlets==5.14.3
@@ -439,7 +438,6 @@ typing-extensions==4.15.0
# aiosignal # aiosignal
# anyio # anyio
# beautifulsoup4 # beautifulsoup4
# dj-database-url
# django-countries # django-countries
# ipython # ipython
# jwcrypto # jwcrypto
@@ -448,7 +446,7 @@ typing-extensions==4.15.0
# pydantic # pydantic
# pydantic-core # pydantic-core
# typing-inspection # typing-inspection
typing-inspection==0.4.1 typing-inspection==0.4.2
# via pydantic # via pydantic
tzdata==2025.2 tzdata==2025.2
# via # via
@@ -456,7 +454,7 @@ tzdata==2025.2
# kombu # kombu
unidecode==1.4.0 unidecode==1.4.0
# via -r ./requirements.in # via -r ./requirements.in
urllib3==1.26.20 urllib3==2.5.0
# via # via
# algoliasearch # algoliasearch
# botocore # botocore
@@ -465,26 +463,26 @@ urllib3==1.26.20
# minio # minio
# requests # requests
# responses # responses
uv==0.8.15 uv==0.9.5
# via -r ./requirements.in # via -r ./requirements.in
vine==5.1.0 vine==5.1.0
# via # via
# amqp # amqp
# celery # celery
# kombu # kombu
virtualenv==20.34.0 virtualenv==20.35.3
# via pre-commit # via pre-commit
wcwidth==0.2.13 wcwidth==0.2.14
# via prompt-toolkit # via prompt-toolkit
wheel==0.45.1 wheel==0.45.1
# via -r ./requirements.in # via -r ./requirements.in
whitenoise==6.9.0 whitenoise==6.11.0
# via -r ./requirements.in # via -r ./requirements.in
wordcloud==1.9.4 wordcloud==1.9.4
# via -r ./requirements.in # via -r ./requirements.in
yarl==1.20.1 yarl==1.22.0
# via aiohttp # via aiohttp
zope-event==5.1.1 zope-event==6.0
# via gevent # via gevent
zope-interface==7.2 zope-interface==8.0.1
# via gevent # via gevent

View File

@@ -146,7 +146,7 @@ class Migration(migrations.Migration):
migrations.AddConstraint( migrations.AddConstraint(
model_name="thread", model_name="thread",
constraint=models.CheckConstraint( constraint=models.CheckConstraint(
check=django.db.models.lookups.GreaterThanOrEqual( condition=django.db.models.lookups.GreaterThanOrEqual(
django.db.models.functions.comparison.Cast( django.db.models.functions.comparison.Cast(
"last_update_ts", output_field=models.FloatField() "last_update_ts", output_field=models.FloatField()
), ),
@@ -172,7 +172,7 @@ class Migration(migrations.Migration):
migrations.AddConstraint( migrations.AddConstraint(
model_name="channelupdategap", model_name="channelupdategap",
constraint=models.CheckConstraint( constraint=models.CheckConstraint(
check=django.db.models.lookups.GreaterThan( condition=django.db.models.lookups.GreaterThan(
django.db.models.functions.comparison.Cast( django.db.models.functions.comparison.Cast(
"newest_message_ts", output_field=models.FloatField() "newest_message_ts", output_field=models.FloatField()
), ),

View File

@@ -61,7 +61,7 @@ class ChannelUpdateGap(models.Model):
class Meta: class Meta:
constraints = [ constraints = [
models.CheckConstraint( models.CheckConstraint(
check=models.lookups.GreaterThan( condition=models.lookups.GreaterThan(
models.functions.Cast( models.functions.Cast(
"newest_message_ts", output_field=models.FloatField() "newest_message_ts", output_field=models.FloatField()
), ),
@@ -110,7 +110,7 @@ class Thread(models.Model):
unique_together = [("channel", "thread_ts")] unique_together = [("channel", "thread_ts")]
constraints = [ constraints = [
models.CheckConstraint( models.CheckConstraint(
check=models.lookups.GreaterThanOrEqual( condition=models.lookups.GreaterThanOrEqual(
models.functions.Cast( models.functions.Cast(
"last_update_ts", output_field=models.FloatField() "last_update_ts", output_field=models.FloatField()
), ),

View File

@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
from .models import User from .models import User
@admin.register(User)
class EmailUserAdmin(UserAdmin): class EmailUserAdmin(UserAdmin):
fieldsets = ( fieldsets = (
(None, {"fields": ("email", "password")}), (None, {"fields": ("email", "password")}),
@@ -59,6 +60,3 @@ class EmailUserAdmin(UserAdmin):
"claimed", "claimed",
) )
search_fields = ("email", "display_name__unaccent") search_fields = ("email", "display_name__unaccent")
admin.site.register(User, EmailUserAdmin)

View File

@@ -330,7 +330,7 @@ class UserAvatar(TemplateView):
): ):
# check if user is on pages that require CSRF but don't require login # check if user is on pages that require CSRF but don't require login
# (auth pages where anonymous users submit forms) # (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 current_path = self.request.path
# paths that anonymous users can access and have forms # paths that anonymous users can access and have forms

View File

@@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime, timezone
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed 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 core.models import RenderedContent
from .models import Version from .models import Version
@@ -24,7 +24,7 @@ class RSSVersionFeed(Feed):
release_date = item.release_date release_date = item.release_date
if release_date: if release_date:
datetime_obj = datetime.combine(release_date, datetime.min.time()) 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 return aware_datetime_obj
def item_description(self, item): def item_description(self, item):

View File

@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime, timezone
from django.utils.timezone import make_aware, utc from django.utils.timezone import make_aware
from ..feeds import RSSVersionFeed, AtomVersionFeed from ..feeds import RSSVersionFeed, AtomVersionFeed
@@ -16,7 +16,8 @@ def test_items(version, old_version):
def test_item_pubdate(version): def test_item_pubdate(version):
feed = RSSVersionFeed() feed = RSSVersionFeed()
expected_datetime = make_aware( 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 assert feed.item_pubdate(version) == expected_datetime
@@ -51,6 +52,7 @@ def test_items_atom(version, old_version):
def test_item_pubdate_atom(version): def test_item_pubdate_atom(version):
feed = AtomVersionFeed() feed = AtomVersionFeed()
expected_datetime = make_aware( 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 assert feed.item_pubdate(version) == expected_datetime