mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-10-09 05:55:36 +00:00
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:
@@ -5,18 +5,31 @@ from RIGS.models import Profile
|
||||
|
||||
@pytest.fixture
|
||||
def trainee(db):
|
||||
trainee = Profile.objects.create(username="trainee", first_name="Train", last_name="EE",
|
||||
initials="TRN",
|
||||
email="trainee@example.com", is_active=True, is_approved=True)
|
||||
trainee = Profile.objects.create(
|
||||
username="trainee",
|
||||
first_name="Train",
|
||||
last_name="EE",
|
||||
initials="TRN",
|
||||
email="trainee@example.com",
|
||||
is_active=True,
|
||||
is_approved=True,
|
||||
)
|
||||
yield trainee
|
||||
trainee.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def supervisor(db):
|
||||
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
|
||||
initials="SV",
|
||||
email="supervisor@example.com", is_supervisor=True, is_active=True, is_approved=True)
|
||||
supervisor = Profile.objects.create(
|
||||
username="supervisor",
|
||||
first_name="Super",
|
||||
last_name="Visor",
|
||||
initials="SV",
|
||||
email="supervisor@example.com",
|
||||
is_supervisor=True,
|
||||
is_active=True,
|
||||
is_approved=True,
|
||||
)
|
||||
yield supervisor
|
||||
supervisor.delete()
|
||||
|
||||
@@ -24,7 +37,9 @@ def supervisor(db):
|
||||
@pytest.fixture
|
||||
def training_item(db):
|
||||
training_category = models.TrainingCategory.objects.create(reference_number=1, name="The Basics")
|
||||
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="How Not To Die")
|
||||
training_item = models.TrainingItem.objects.create(
|
||||
category=training_category, reference_number=1, description="How Not To Die"
|
||||
)
|
||||
yield training_item
|
||||
training_category.delete()
|
||||
training_item.delete()
|
||||
@@ -33,7 +48,9 @@ def training_item(db):
|
||||
@pytest.fixture
|
||||
def training_item_2(db):
|
||||
training_category = models.TrainingCategory.objects.create(reference_number=2, name="Sound")
|
||||
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="Fundamentals of Audio")
|
||||
training_item = models.TrainingItem.objects.create(
|
||||
category=training_category, reference_number=1, description="Fundamentals of Audio"
|
||||
)
|
||||
yield training_item
|
||||
training_category.delete()
|
||||
training_item.delete()
|
||||
@@ -41,6 +58,8 @@ def training_item_2(db):
|
||||
|
||||
@pytest.fixture
|
||||
def level(db):
|
||||
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
|
||||
level = models.TrainingLevel.objects.create(
|
||||
description="There is no description.", level=models.TrainingLevel.TECHNICIAN
|
||||
)
|
||||
yield level
|
||||
level.delete()
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
from django.urls import reverse
|
||||
from pypom import Region
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from PyRIGS.tests import regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage
|
||||
|
||||
|
||||
class TraineeDetail(BasePage):
|
||||
URL_TEMPLATE = 'training/trainee/{pk}'
|
||||
|
||||
_name_selector = (By.XPATH, '//h2')
|
||||
|
||||
@property
|
||||
def page_name(self):
|
||||
return self.find_element(*self._name_selector).text
|
||||
|
||||
|
||||
class AddQualification(FormPage):
|
||||
URL_TEMPLATE = 'training/trainee/{pk}/add_qualification/'
|
||||
|
||||
_item_selector = (By.XPATH, '//div[1]/form/div[1]/div')
|
||||
_supervisor_selector = (By.XPATH, '//div[1]/form/div[3]/div')
|
||||
|
||||
form_items = {
|
||||
'depth': (regions.SingleSelectPicker, (By.ID, 'id_depth')),
|
||||
'date': (regions.DatePicker, (By.ID, 'id_date')),
|
||||
'notes': (regions.TextBox, (By.ID, 'id_notes')),
|
||||
}
|
||||
|
||||
@property
|
||||
def item_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._item_selector))
|
||||
|
||||
@property
|
||||
def supervisor_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return 'add' not in self.driver.current_url
|
||||
|
||||
|
||||
class SessionLog(FormPage):
|
||||
URL_TEMPLATE = 'training/session_log'
|
||||
|
||||
_supervisor_selector = (By.CSS_SELECTOR, 'div#supervisor_group>div.bootstrap-select')
|
||||
_trainees_selector = (By.CSS_SELECTOR, 'div#trainees_group>div.bootstrap-select')
|
||||
_training_started_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||
_training_complete_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||
_competency_assessed_selector = (By.XPATH, '//div[1]/div/div/form/div[5]/div')
|
||||
|
||||
form_items = {
|
||||
'date': (regions.DatePicker, (By.ID, 'id_date')),
|
||||
'notes': (regions.TextBox, (By.ID, 'id_notes')),
|
||||
}
|
||||
|
||||
@property
|
||||
def supervisor_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
|
||||
|
||||
@property
|
||||
def trainees_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._trainees_selector))
|
||||
|
||||
@property
|
||||
def training_started_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._training_started_selector))
|
||||
|
||||
@property
|
||||
def training_complete_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._training_complete_selector))
|
||||
|
||||
@property
|
||||
def competency_assessed_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._competency_assessed_selector))
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return 'log' not in self.driver.current_url
|
||||
@@ -1,80 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal
|
||||
from PyRIGS.tests.pages import animation_is_finished
|
||||
from training import models
|
||||
from training.tests import pages
|
||||
|
||||
|
||||
def select_super(page, supervisor):
|
||||
page.supervisor_selector.toggle()
|
||||
assert page.supervisor_selector.is_open
|
||||
page.supervisor_selector.search(supervisor.name[:-6])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
page.supervisor_selector.set_option(supervisor.name, True)
|
||||
assert page.supervisor_selector.options[0].selected
|
||||
|
||||
|
||||
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
|
||||
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
|
||||
# assert page.name in str(trainee)
|
||||
|
||||
page.depth = "Training Started"
|
||||
page.date = date = datetime.date(1984, 1, 1)
|
||||
page.notes = "A note"
|
||||
|
||||
time.sleep(2) # Slow down for javascript
|
||||
|
||||
page.item_selector.toggle()
|
||||
assert page.item_selector.is_open
|
||||
page.item_selector.search(training_item.description)
|
||||
time.sleep(2) # Slow down for javascript
|
||||
# page.item_selector.set_option(training_item.description, True)
|
||||
assert page.item_selector.options[0].selected
|
||||
page.item_selector.toggle()
|
||||
|
||||
select_super(page, supervisor)
|
||||
page.supervisor_selector.toggle()
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
|
||||
assert qualification.supervisor_id == supervisor.pk
|
||||
assert qualification.date == date
|
||||
assert qualification.notes == "A note"
|
||||
assert qualification.depth == models.TrainingItemQualification.STARTED
|
||||
|
||||
|
||||
def test_session_log(logged_in_browser, live_server, trainee, supervisor, training_item, training_item_2):
|
||||
page = pages.SessionLog(logged_in_browser.driver, live_server.url).open()
|
||||
|
||||
page.date = date = datetime.date(2001, 1, 10)
|
||||
page.notes = note = "A general note"
|
||||
|
||||
time.sleep(2) # Slow down for javascript
|
||||
|
||||
select_super(page, supervisor)
|
||||
|
||||
page.trainees_selector.toggle()
|
||||
assert page.trainees_selector.is_open
|
||||
page.trainees_selector.search(trainee.first_name)
|
||||
time.sleep(2) # Slow down for javascript
|
||||
page.trainees_selector.set_option(trainee.name, True)
|
||||
# assert page.trainees_selector.options[0].selected
|
||||
page.trainees_selector.toggle()
|
||||
|
||||
page.training_started_selector.toggle()
|
||||
assert page.training_started_selector.is_open
|
||||
page.training_started_selector.search(training_item.description[:-2])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
# assert page.training_started_selector.options[0].selected
|
||||
page.training_started_selector.toggle()
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
@@ -1,30 +1,43 @@
|
||||
import datetime
|
||||
import pytest
|
||||
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
|
||||
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains, assertURLEqual
|
||||
from pytest_django.asserts import assertFormError, assertContains, assertNotContains
|
||||
|
||||
from training import models
|
||||
from reversion.models import Version, Revision
|
||||
|
||||
|
||||
def test_add_qualification(admin_client, trainee, admin_user, training_item):
|
||||
url = reverse('add_qualification', kwargs={'pk': trainee.pk})
|
||||
url = reverse("add_qualification", kwargs={"pk": trainee.pk})
|
||||
date = (timezone.now() + datetime.timedelta(days=3)).strftime("%Y-%m-%d")
|
||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
|
||||
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
|
||||
response = admin_client.post(url, {'date': date, 'trainee': admin_user.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||
response = admin_client.post(
|
||||
url, {"date": date, "trainee": trainee.pk, "supervisor": trainee.pk, "item": training_item.pk}
|
||||
)
|
||||
assertFormError(response.context["form"], "date", "Qualification date may not be in the future")
|
||||
assertFormError(response.context["form"], "supervisor", "One may not supervise oneself...")
|
||||
response = admin_client.post(
|
||||
url, {"date": date, "trainee": admin_user.pk, "supervisor": trainee.pk, "item": training_item.pk}
|
||||
)
|
||||
print(response.content)
|
||||
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
||||
assertFormError(response.context["form"], "supervisor", "Selected supervisor must actually *be* a supervisor...")
|
||||
|
||||
|
||||
def test_add_qualification_reversion(admin_client, trainee, training_item, supervisor):
|
||||
url = reverse('add_qualification', kwargs={'pk': trainee.pk})
|
||||
url = reverse("add_qualification", kwargs={"pk": trainee.pk})
|
||||
date = (timezone.now() + datetime.timedelta(days=-3)).strftime("%Y-%m-%d")
|
||||
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 0, 'notes': ""})
|
||||
response = admin_client.post(
|
||||
url,
|
||||
{
|
||||
"date": date,
|
||||
"supervisor": supervisor.pk,
|
||||
"trainee": trainee.pk,
|
||||
"item": training_item.pk,
|
||||
"depth": 0,
|
||||
"notes": "",
|
||||
},
|
||||
)
|
||||
print(response.content)
|
||||
assert response.status_code == 302
|
||||
qual = models.TrainingItemQualification.objects.last()
|
||||
@@ -32,13 +45,15 @@ def test_add_qualification_reversion(admin_client, trainee, training_item, super
|
||||
assert training_item.pk == qual.item_id
|
||||
# Ensure only one revision has been created
|
||||
assert Revision.objects.count() == 1
|
||||
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})
|
||||
response = admin_client.post(
|
||||
url, {"date": date, "supervisor": supervisor.pk, "trainee": trainee.pk, "item": training_item.pk, "depth": 1}
|
||||
)
|
||||
assert Revision.objects.count() == 2
|
||||
assert Version.objects.count() == 4 # Two item qualifications and the trainee twice
|
||||
|
||||
|
||||
def test_add_requirement(admin_client, level):
|
||||
url = reverse('add_requirement', kwargs={'pk': level.pk})
|
||||
url = reverse("add_requirement", kwargs={"pk": level.pk})
|
||||
response = admin_client.post(url)
|
||||
assertContains(response, level.pk)
|
||||
|
||||
@@ -51,32 +66,32 @@ def get_response(admin_client, url, kwargs={}):
|
||||
|
||||
|
||||
def test_trainee_detail(admin_client, trainee, admin_user):
|
||||
response = get_response(admin_client, 'trainee_detail', {'pk': admin_user.pk})
|
||||
response = get_response(admin_client, "trainee_detail", {"pk": admin_user.pk})
|
||||
assertContains(response, "Your Training Record")
|
||||
assertContains(response, "No qualifications in any levels")
|
||||
|
||||
response = get_response(admin_client, 'trainee_detail', {'pk': trainee.pk})
|
||||
response = get_response(admin_client, "trainee_detail", {"pk": trainee.pk})
|
||||
assertNotContains(response, "Your")
|
||||
assertContains(response, f"{trainee.get_full_name()}'s Training Record")
|
||||
|
||||
|
||||
def test_trainee_item_detail(admin_client, trainee):
|
||||
response = get_response(admin_client, 'trainee_item_detail', {'pk': trainee.pk})
|
||||
response = get_response(admin_client, "trainee_item_detail", {"pk": trainee.pk})
|
||||
assertContains(response, "Nothing found")
|
||||
|
||||
|
||||
def test_item_list(admin_client, training_item):
|
||||
response = get_response(admin_client, 'item_list')
|
||||
response = get_response(admin_client, "item_list")
|
||||
assertContains(response, str(training_item.category))
|
||||
|
||||
|
||||
def test_trainee_list_search(admin_client, admin_user, trainee, supervisor):
|
||||
response = get_response(admin_client, 'trainee_list')
|
||||
response = get_response(admin_client, "trainee_list")
|
||||
assertContains(response, admin_user.get_full_name())
|
||||
assertContains(response, trainee.get_full_name())
|
||||
assertContains(response, supervisor.get_full_name())
|
||||
|
||||
url = reverse('trainee_list')
|
||||
response = admin_client.get(url, {'q': trainee.get_full_name()})
|
||||
url = reverse("trainee_list")
|
||||
response = admin_client.get(url, {"q": trainee.get_full_name()})
|
||||
assertContains(response, trainee.get_full_name())
|
||||
assertNotContains(response, supervisor.get_full_name())
|
||||
|
||||
Reference in New Issue
Block a user