Port to Django 5.2 (#635)

* Port to Django 5.2

* Pin pluggy to 1.2.0

Any newer and the mystery importlib metadata error appears. Weird! >_>

* Update for premailer changed default

* Update view logic for is_ajax being changed to a template context processor

* Port a few more tests to pytest proper

Having two distinct test flavours is giving me a headache

* Version 1 dockerfile

Makes a VERY big image, I suspect we can optimise this a lot...

* Optimise dockerfile a little lot a bit

* fix(users): change logout link to POST request

* fix(tests): fix some syntax errors in test code

still got lots of failed tests :(

* fix: replace deprecated Django APIs

* ci: update Dockerfile

* ci: update Dockerfile

* fix(dependencies+tests): update EoL/vulnerable dependencies and improve test stability

* Upgrade Python from 3.10 to 3.12
* Update frontend dependencies, replace node-sass with sass

* chore(logging): ignore dangling obj reference warning from pypdf

* ci: update compose.yml for prod deployment

- Add Nginx as a reverse proxy
- Add cert-selfsign.sh for generating self-signed certificates
- Add certbot-issue.sh for Let's Encrypt certificate issuance and renewal
- Add cron-install.sh and cron-uninstall.sh for system cron management
- Add .env.example as an environment variable template

* style: reformat code

* ci: add more sleep trying to pass tests

* ci: ignore browser-based tests during ci testing

* fix: create home for the new user during Dockerfile building

* chore: remove heroku conf file

* fix: RIGS not franken anymore

* fix: restore is_ajax as a boolean and split out the context processor

* test: remove Selenium interaction tests and their CI workarounds

* fix: harden production settings (CSRF origins, env casts, ADMINS) and drop dead debug toolbar code

* deploy: serve static files from nginx, pin PGDATA, and make cert scripts loud on failure

* build: switch .dockerignore to an allowlist

* build: drop unused dependencies, soft-pin the rest and target Python 3.14

* build: move image to Python 3.14 / Node 24 and force DEBUG off

* ci: add dependabot config for uv, npm, docker and actions

* build: narrow Sass deprecation silencing to @import and require Node 24

* build: provide placeholder env for collectstatic now that DEBUG is off in the image

* deploy: add plain-HTTP nginx config for local development and use it in compose

* deploy: remove self-signed cert script

* build: make the image multi-arch with official node and python base images

* ci: lint and test on PRs, build the image on PRs and push it to GHCR on master

* docs: add local running and sample data instructions

* fix: report a form error instead of crashing when big power has no Power MIC

* fix: upgrade pypdf and urllib3 to patched releases

* fix: apply non-breaking npm audit fixes

* fix: strip whitespace in ALLOWED_HOSTS and drop misleading HSTS preload

* deploy: add a pyrigs healthcheck and make nginx wait for it

* build: pin the uv image version

* fix: correct the check-in person picker condition and use BeautifulSoup's string argument

* refactor: replace unique_together with UniqueConstraint

* build: replace pycodestyle with ruff and fix what it found

Removes unused imports and variables, and fixes a few real problems it
surfaced:
- EventCheckIn.active() referenced an undefined name and raised NameError;
  it now returns whether the check-in has no end time
- RIGS.admin defined EventChecklistAdmin twice; the second is now
  PowerTestRecordAdmin
- RIGS/tests/conftest.py used date/timedelta without importing them
- the signal-registering imports in apps.py are kept with noqa

pycodestyle config in setup.cfg is dropped.

* style: format the codebase with ruff

* style: normalise line endings, trailing whitespace and end-of-file newlines

* ci: run ruff and file hygiene through prek, and document it

---------

Co-authored-by: Hang <me@hangxu.me>
Co-authored-by: Joe Banks <joe@jb3.dev>
This commit is contained in:
2026-10-02 00:43:41 +02:00
committed by GitHub
parent 4cd68f5a0f
commit 08171767a0
148 changed files with 6573 additions and 9514 deletions

View File

@@ -1,4 +1,3 @@
import logging
from diff_match_patch import diff_match_patch
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
@@ -15,7 +14,7 @@ class RevisionMixin:
@property
def current_version(self):
version = RIGSVersion.objects.get_for_object(self).select_related('revision').first()
version = RIGSVersion.objects.get_for_object(self).select_related("revision").first()
return version
@property
@@ -51,7 +50,11 @@ class FieldComparison:
self._new = new
def display_value(self, value):
if isinstance(self.field, (IntegerField, CharField)) and self.field.choices is not None and len(self.field.choices) > 0:
if (
isinstance(self.field, (IntegerField, CharField))
and self.field.choices is not None
and len(self.field.choices) > 0
):
choice = [x[1] for x in self.field.choices if x[0] == value]
if len(choice) > 0:
return choice[0]
@@ -92,18 +95,18 @@ class FieldComparison:
outputDiffs = []
for (op, data) in diffs:
for op, data in diffs:
if op == dmp.DIFF_INSERT:
outputDiffs.append({'type': 'insert', 'text': data})
outputDiffs.append({"type": "insert", "text": data})
elif op == dmp.DIFF_DELETE:
outputDiffs.append({'type': 'delete', 'text': data})
outputDiffs.append({"type": "delete", "text": data})
elif op == dmp.DIFF_EQUAL:
outputDiffs.append({'type': 'equal', 'text': data})
outputDiffs.append({"type": "equal", "text": data})
return outputDiffs
class ModelComparison:
def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=['date_joined']):
def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=["date_joined"]):
# recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects
try:
self.fields = old._meta.get_fields()
@@ -151,12 +154,20 @@ class ModelComparison:
if self.follow and self.version.object is not None:
from RIGS.models import EventAuthorisation
from training.models import TrainingLevelQualification, TrainingItemQualification
item_type = ContentType.objects.get_for_model(self.version.object)
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(EventAuthorisation))
comparisonParams = {'excluded_keys': ['id', 'event', 'order', 'checklist', 'level', '_order', 'date_joined']}
item_type = ContentType.objects.get_for_model(self.version.object)
old_item_versions = (
self.version.parent.revision.version_set.exclude(content_type=item_type)
.exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification))
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
)
new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(
content_type=ContentType.objects.get_for_model(EventAuthorisation)
)
comparisonParams = {
"excluded_keys": ["id", "event", "order", "checklist", "level", "_order", "date_joined"]
}
# Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk
@@ -178,7 +189,7 @@ class ModelComparison:
item_dict[version.object_id] = compare # update the dictionary with the changes
changes = []
for (_, compare) in list(item_dict.items()):
for _, compare in list(item_dict.items()):
if compare.fields_changed:
changes.append(compare)
@@ -188,7 +199,7 @@ class ModelComparison:
def name(self):
obj = self.new if self.new else self.old
if (hasattr(obj, 'activity_feed_string')):
if hasattr(obj, "activity_feed_string"):
return obj.activity_feed_string
else:
return str(obj)
@@ -208,8 +219,13 @@ class RIGSVersionManager(VersionQuerySet):
for model in model_array:
content_types.append(ContentType.objects.get_for_model(model))
return self.filter(content_type__in=content_types).select_related("revision",).order_by(
"-revision__date_created")
return (
self.filter(content_type__in=content_types)
.select_related(
"revision",
)
.order_by("-revision__date_created")
)
class RIGSVersion(Version):
@@ -223,11 +239,14 @@ class RIGSVersion(Version):
def parent(self):
thisId = self.object_id
versions = RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId).select_related(
"revision", "revision__user").all()
versions = (
RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId)
.select_related("revision", "revision__user")
.all()
)
try:
previousVersion = versions.filter(revision_id__lt=self.revision_id).latest('revision__date_created')
previousVersion = versions.filter(revision_id__lt=self.revision_id).latest("revision_id")
except ObjectDoesNotExist:
return False
@@ -239,7 +258,7 @@ class RIGSVersion(Version):
version=self,
new=self._object_version.object,
old=self.parent._object_version.object if self.parent else None,
follow=True
follow=True,
)
@property
@@ -248,7 +267,7 @@ class RIGSVersion(Version):
@property
def display_name(self):
if hasattr(self.changes.new, 'display_id'):
if hasattr(self.changes.new, "display_id"):
id = self.changes.new.display_id
else:
id = self.changes.new.pk