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

@@ -32,15 +32,11 @@ def merge_model_instances(primary_object, alias_objects):
"""
# get related fields
related_fields = list(filter(
lambda x: x.is_relation is True,
primary_object._meta.get_fields()))
related_fields = list(filter(lambda x: x.is_relation is True, primary_object._meta.get_fields()))
many_to_many_fields = list(filter(
lambda x: x.many_to_many is True, related_fields))
many_to_many_fields = list(filter(lambda x: x.many_to_many is True, related_fields))
related_fields = list(filter(
lambda x: x.many_to_many is False, related_fields))
related_fields = list(filter(lambda x: x.many_to_many is False, related_fields))
# Loop through all alias objects and migrate their references to the
# primary object
@@ -69,10 +65,7 @@ def merge_model_instances(primary_object, alias_objects):
through_model_instances = through_model.objects.filter(**kwargs)
for instance in through_model_instances:
# Re-attach the through model to the primary_object
setattr(
instance,
many_to_many_field.m2m_field_name(),
primary_object)
setattr(instance, many_to_many_field.m2m_field_name(), primary_object)
instance.save()
# TODO: Here, try to delete duplicate instances that are
# disallowed by a unique_together constraint
@@ -109,31 +102,34 @@ def merge_model_instances(primary_object, alias_objects):
class AssociateAdmin(VersionAdmin):
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
search_fields = ["id", "name"]
list_display_links = ["id", "name"]
actions = ["merge"]
def get_queryset(self, request):
return super().get_queryset(request).annotate(event_count=Count('event'))
return super().get_queryset(request).annotate(event_count=Count("event"))
def number_of_events(self, obj):
return obj.latest_events.count()
number_of_events.admin_order_field = 'event_count'
number_of_events.admin_order_field = "event_count"
def merge(self, request, queryset):
if request.POST.get('post'): # Has the user confirmed which is the master record?
if request.POST.get("post"): # Has the user confirmed which is the master record?
try:
master_object_pk = request.POST.get('master')
master_object_pk = request.POST.get("master")
master_object = queryset.get(pk=master_object_pk)
except ObjectDoesNotExist:
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all())
reversion.set_comment('Merging Objects')
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(
master_object, queryset.exclude(pk=master_object_pk).all()
)
reversion.set_comment("Merging Objects")
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
else: # Present the confirmation screen
class TempForm(ModelForm):
class Meta:
model = queryset.model
@@ -144,39 +140,51 @@ class AssociateAdmin(VersionAdmin):
forms.append(TempForm(instance=obj))
context = {
'title': _("Are you sure?"),
'queryset': queryset,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
"title": _("Are you sure?"),
"queryset": queryset,
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
"forms": forms,
}
return TemplateResponse(request, 'admin_associate_merge.html', context)
return TemplateResponse(request, "admin_associate_merge.html", context)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login', 'date_joined')
list_display_links = ['username']
list_filter = UserAdmin.list_filter + ('is_approved', 'date_joined')
list_display = (
"username",
"name",
"is_approved",
"is_superuser",
"is_supervisor",
"number_of_events",
"last_login",
"date_joined",
)
list_display_links = ["username"]
list_filter = UserAdmin.list_filter + ("is_approved", "date_joined")
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
(None, {"fields": ("username", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name", "email", "initials", "phone")}),
(
_("Permissions"),
{"fields": ("is_approved", "is_active", "is_staff", "is_superuser", "groups", "user_permissions")},
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
(
None,
{
"classes": ("wide",),
"fields": ("username", "password1", "password2"),
},
),
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = ['approve_user', 'merge']
actions = ["approve_user", "merge"]
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
merge_fields = ["username", "first_name", "last_name", "initials", "email", "phone", "is_supervisor"]
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
@@ -184,32 +192,32 @@ class ProfileAdmin(UserAdmin, AssociateAdmin):
@admin.register(models.Person)
class PersonAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes']
list_display = ("id", "name", "phone", "email", "number_of_events")
merge_fields = ["name", "phone", "email", "address", "notes"]
@admin.register(models.Venue)
class VenueAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
list_display = ("id", "name", "phone", "email", "number_of_events")
merge_fields = ["name", "phone", "email", "address", "notes", "three_phase_available"]
@admin.register(models.Organisation)
class OrganisationAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
list_display = ("id", "name", "phone", "email", "number_of_events")
merge_fields = ["name", "phone", "email", "address", "notes", "union_account"]
@admin.register(models.RiskAssessment)
class RiskAssessmentAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
list_display = ("id", "event", "reviewed_at", "reviewed_by")
@admin.register(models.EventChecklist)
class EventChecklistAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
list_display = ("id", "event", "reviewed_at", "reviewed_by")
@admin.register(models.PowerTestRecord)
class EventChecklistAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
class PowerTestRecordAdmin(VersionAdmin):
list_display = ("id", "event", "reviewed_at", "reviewed_by")