mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-10-06 20:45:30 +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:
@@ -19,7 +19,9 @@ def status(db):
|
||||
|
||||
@pytest.fixture
|
||||
def cable_type(db):
|
||||
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
|
||||
connector = models.Connector.objects.create(
|
||||
description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3
|
||||
)
|
||||
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
|
||||
yield cable_type
|
||||
connector.delete()
|
||||
@@ -28,14 +30,33 @@ def cable_type(db):
|
||||
|
||||
@pytest.fixture
|
||||
def test_cable(db, category, status, cable_type):
|
||||
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5", replacement_cost=50)
|
||||
cable = models.Asset.objects.create(
|
||||
asset_id="9666",
|
||||
description="125A -> Jack",
|
||||
comments="The cable from Hell...",
|
||||
status=status,
|
||||
category=category,
|
||||
date_acquired=datetime.date(2006, 6, 6),
|
||||
is_cable=True,
|
||||
cable_type=cable_type,
|
||||
length=10,
|
||||
csa="1.5",
|
||||
replacement_cost=50,
|
||||
)
|
||||
yield cable
|
||||
cable.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_asset(db, category, status):
|
||||
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
|
||||
asset, created = models.Asset.objects.get_or_create(
|
||||
asset_id="91991",
|
||||
description="Spaceflower",
|
||||
status=status,
|
||||
category=category,
|
||||
date_acquired=datetime.date(1991, 12, 26),
|
||||
replacement_cost=100,
|
||||
)
|
||||
yield asset
|
||||
asset.delete()
|
||||
|
||||
@@ -49,6 +70,20 @@ def test_status_2(db):
|
||||
|
||||
@pytest.fixture
|
||||
def test_asset_2(db, category, test_status_2):
|
||||
asset, created = models.Asset.objects.get_or_create(asset_id="10", description="Working Mic", status=test_status_2, category=category, date_acquired=datetime.date(2001, 10, 20), replacement_cost=1000)
|
||||
asset, created = models.Asset.objects.get_or_create(
|
||||
asset_id="10",
|
||||
description="Working Mic",
|
||||
status=test_status_2,
|
||||
category=category,
|
||||
date_acquired=datetime.date(2001, 10, 20),
|
||||
replacement_cost=1000,
|
||||
)
|
||||
yield asset
|
||||
asset.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_supplier(db):
|
||||
supplier, created = models.Supplier.objects.get_or_create(name="Fullmetal Heavy Industry")
|
||||
yield supplier
|
||||
supplier.delete()
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
# Collection of page object models for use within tests.
|
||||
from django.urls import reverse
|
||||
from pypom import Region
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
|
||||
from PyRIGS.tests import regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage, animation_is_finished
|
||||
|
||||
|
||||
class AssetList(BasePage):
|
||||
URL_TEMPLATE = '/assets/asset/list'
|
||||
|
||||
_asset_item_locator = (By.CLASS_NAME, 'assetRow')
|
||||
_search_text_locator = (By.ID, 'id_q')
|
||||
_status_select_locator = (By.CSS_SELECTOR, 'div#status-group>div.bootstrap-select')
|
||||
_category_select_locator = (By.CSS_SELECTOR, 'div#category-group>div.bootstrap-select')
|
||||
_go_button_locator = (By.ID, 'id_search')
|
||||
_filter_button_locator = (By.ID, 'filter-submit')
|
||||
|
||||
class AssetListRow(Region):
|
||||
_asset_id_locator = (By.CLASS_NAME, "assetID")
|
||||
_asset_description_locator = (By.CLASS_NAME, "assetDesc")
|
||||
_asset_category_locator = (By.CLASS_NAME, "assetCategory")
|
||||
_asset_status_locator = (By.CLASS_NAME, "assetStatus")
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.find_element(*self._asset_id_locator).text
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.find_element(*self._asset_description_locator).text
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return self.find_element(*self._asset_category_locator).text
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.find_element(*self._asset_status_locator).text
|
||||
|
||||
@property
|
||||
def assets(self):
|
||||
return [self.AssetListRow(self, i) for i in self.find_elements(*self._asset_item_locator)]
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self.find_element(*self._search_text_locator).text
|
||||
|
||||
def set_query(self, queryString):
|
||||
element = self.find_element(*self._search_text_locator)
|
||||
element.clear()
|
||||
element.send_keys(queryString)
|
||||
|
||||
def search(self):
|
||||
self.find_element(*self._go_button_locator).click()
|
||||
|
||||
def filter(self):
|
||||
self.find_element(*self._filter_button_locator).click()
|
||||
|
||||
@property
|
||||
def status_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._status_select_locator))
|
||||
|
||||
@property
|
||||
def category_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._category_select_locator))
|
||||
|
||||
|
||||
class AssetForm(FormPage):
|
||||
_purchased_from_select_locator = (By.XPATH, '//div[@id="purchased-from-group"]/div/div/div')
|
||||
_parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select')
|
||||
form_items = {
|
||||
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'comments': (regions.TextBox, (By.ID, 'id_comments')),
|
||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
||||
|
||||
'cable_type': (regions.SingleSelectPicker, (By.ID, 'id_cable_type')),
|
||||
'length': (regions.TextBox, (By.ID, 'id_length')),
|
||||
'csa': (regions.TextBox, (By.ID, 'id_csa')),
|
||||
}
|
||||
|
||||
@property
|
||||
def purchased_from_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._purchased_from_select_locator))
|
||||
|
||||
@property
|
||||
def parent_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._parent_select_locator))
|
||||
|
||||
|
||||
class AssetEdit(AssetForm):
|
||||
URL_TEMPLATE = '/assets/asset/id/{asset_id}/edit/'
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return '/edit' not in self.driver.current_url
|
||||
|
||||
|
||||
class AssetCreate(AssetForm):
|
||||
URL_TEMPLATE = '/assets/asset/create/'
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return '/create' not in self.driver.current_url
|
||||
|
||||
|
||||
class AssetDuplicate(AssetForm):
|
||||
URL_TEMPLATE = '/assets/asset/id/{asset_id}/duplicate'
|
||||
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Duplicate')]")
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return '/duplicate' not in self.driver.current_url
|
||||
|
||||
|
||||
class SupplierList(BasePage):
|
||||
URL_TEMPLATE = reverse('supplier_list')
|
||||
|
||||
_supplier_item_locator = (By.ID, 'row_item')
|
||||
_search_text_locator = (By.ID, 'id_search_text')
|
||||
_go_button_locator = (By.ID, 'id_search')
|
||||
|
||||
class SupplierListRow(Region):
|
||||
_name_locator = (By.ID, "cell_name")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.find_element(*self._name_locator).text
|
||||
|
||||
@property
|
||||
def suppliers(self):
|
||||
return [self.SupplierListRow(self, i) for i in self.find_elements(*self._supplier_item_locator)]
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self.find_element(*self._search_text_locator).text
|
||||
|
||||
def set_query(self, queryString):
|
||||
element = self.find_element(*self._search_text_locator)
|
||||
element.clear()
|
||||
element.send_keys(queryString)
|
||||
|
||||
def search(self):
|
||||
self.find_element(*self._go_button_locator).click()
|
||||
|
||||
|
||||
class SupplierForm(FormPage):
|
||||
form_items = {
|
||||
'name': (regions.TextBox, (By.ID, 'id_name')),
|
||||
}
|
||||
|
||||
|
||||
class SupplierCreate(SupplierForm):
|
||||
URL_TEMPLATE = reverse('supplier_create')
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return '/create' not in self.driver.current_url
|
||||
|
||||
|
||||
class SupplierEdit(SupplierForm):
|
||||
# TODO This should be using reverse
|
||||
URL_TEMPLATE = '/assets/supplier/{supplier_id}/edit'
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return '/edit' not in self.driver.current_url
|
||||
|
||||
|
||||
class AssetAuditList(AssetList):
|
||||
URL_TEMPLATE = reverse('asset_audit_list')
|
||||
|
||||
_search_text_locator = (By.ID, 'id_q')
|
||||
_go_button_locator = (By.ID, 'searchButton')
|
||||
_modal_locator = (By.ID, 'modal')
|
||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
||||
|
||||
@property
|
||||
def modal(self):
|
||||
return self.AssetAuditModal(self, self.find_element(*self._modal_locator))
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self.find_element(*self._search_text_locator).text
|
||||
|
||||
def set_query(self, query):
|
||||
element = self.find_element(*self._search_text_locator)
|
||||
element.clear()
|
||||
element.send_keys(query)
|
||||
|
||||
def search(self):
|
||||
self.find_element(*self._go_button_locator).click()
|
||||
self.wait.until(animation_is_finished())
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
try:
|
||||
return self.find_element(*self._errors_selector)
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
class AssetAuditModal(regions.Modal):
|
||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
||||
# Don't use the usual success selector - that tries and fails to hit the '10m long cable' helper button...
|
||||
_submit_locator = (By.ID, "id_mark_audited")
|
||||
_close_selector = (By.XPATH, "//button[@data-dismiss='modal']")
|
||||
|
||||
form_items = {
|
||||
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
||||
|
||||
'plug': (regions.SingleSelectPicker, (By.ID, 'id_plug')),
|
||||
'socket': (regions.SingleSelectPicker, (By.ID, 'id_socket')),
|
||||
'length': (regions.TextBox, (By.ID, 'id_length')),
|
||||
'csa': (regions.TextBox, (By.ID, 'id_csa')),
|
||||
'circuits': (regions.TextBox, (By.ID, 'id_circuits')),
|
||||
'cores': (regions.TextBox, (By.ID, 'id_cores'))
|
||||
}
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
try:
|
||||
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
|
||||
return error_page.errors
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
self.page.find_element(*self._close_selector).click()
|
||||
self.wait.until(expected_conditions.invisibility_of_element_located((By.ID, 'modal')))
|
||||
|
||||
def remove_all_required(self):
|
||||
self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
@@ -1,358 +0,0 @@
|
||||
import time
|
||||
import datetime
|
||||
import pytest
|
||||
|
||||
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 assets import models
|
||||
from . import pages
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
sound = models.AssetCategory.objects.create(name="Sound")
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
|
||||
|
||||
models.Asset.objects.create(asset_id="1", description="Broken XLR", status=broken, category=sound,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="10", description="Working Mic", status=working, category=sound,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="2", description="A light", status=working, category=lighting,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="C1", description="The pearl", status=broken, category=lighting,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetList(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_default_statuses_applied(self):
|
||||
# Only the working stuff should be shown initially
|
||||
asset_descriptions = list(map(lambda x: x.description, self.page.assets))
|
||||
self.assertEqual(2, len(asset_descriptions))
|
||||
self.assertIn("A light", asset_descriptions)
|
||||
self.assertIn("Working Mic", asset_descriptions)
|
||||
|
||||
def test_asset_order(self):
|
||||
# Only the working stuff should be shown initially
|
||||
self.page.status_selector.open()
|
||||
self.page.status_selector.set_option("Broken", True)
|
||||
self.page.status_selector.close()
|
||||
|
||||
self.page.search()
|
||||
|
||||
asset_ids = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", asset_ids[0])
|
||||
self.assertEqual("2", asset_ids[1])
|
||||
self.assertEqual("10", asset_ids[2])
|
||||
self.assertEqual("C1", asset_ids[3])
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Fails on CI for unknown reason", raises=AssertionError)
|
||||
def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type):
|
||||
page = pages.AssetList(logged_in_browser.driver, live_server.url).open()
|
||||
page.set_query(test_asset.asset_id)
|
||||
page.search()
|
||||
assert len(page.assets) == 1
|
||||
assert page.assets[0].description == test_asset.description
|
||||
assert page.assets[0].id == test_asset.asset_id
|
||||
|
||||
page.set_query(test_asset.description)
|
||||
page.search()
|
||||
assert len(page.assets) == 1
|
||||
assert page.assets[0].description == test_asset.description
|
||||
|
||||
page.set_query("Random string")
|
||||
page.search()
|
||||
assert len(page.assets) == 0
|
||||
|
||||
page.set_query("")
|
||||
page.search()
|
||||
# Only working stuff shown by default
|
||||
assert len(page.assets) == 1
|
||||
|
||||
page.status_selector.toggle()
|
||||
assert page.status_selector.is_open
|
||||
page.status_selector.select_all()
|
||||
page.status_selector.toggle()
|
||||
assert not page.status_selector.is_open
|
||||
page.filter()
|
||||
assert len(page.assets) == 2
|
||||
|
||||
page.category_selector.toggle()
|
||||
assert page.category_selector.is_open
|
||||
page.category_selector.set_option(category.name, True)
|
||||
page.category_selector.close()
|
||||
assert not page.category_selector.is_open
|
||||
page.filter()
|
||||
assert len(page.assets) == 2
|
||||
|
||||
|
||||
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):
|
||||
page = pages.AssetCreate(logged_in_browser.driver, live_server.url).open()
|
||||
wait = WebDriverWait(logged_in_browser.driver, 20)
|
||||
page.description = str(cable_type)
|
||||
page.category = category.name
|
||||
page.status = status.name
|
||||
page.serial_number = "MELON-MELON-MELON"
|
||||
page.comments = "You might need that"
|
||||
page.replacement_cost = "666"
|
||||
page.is_cable = True
|
||||
|
||||
assert logged_in_browser.driver.find_element(By.ID, 'cable-table').is_displayed()
|
||||
wait.until(animation_is_finished())
|
||||
page.cable_type = str(cable_type)
|
||||
page.length = 10
|
||||
page.csa = "1.5"
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
|
||||
|
||||
def test_asset_edit(logged_in_browser, admin_user, live_server, test_asset):
|
||||
page = pages.AssetEdit(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open()
|
||||
|
||||
assert logged_in_browser.driver.find_element(By.ID, 'id_asset_id').get_attribute('readonly') is not None
|
||||
|
||||
new_description = "Big Shelf"
|
||||
page.description = new_description
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
|
||||
assert models.Asset.objects.get(asset_id=test_asset.asset_id).description == new_description
|
||||
|
||||
|
||||
def test_asset_duplicate(logged_in_browser, admin_user, live_server, test_asset):
|
||||
page = pages.AssetDuplicate(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open()
|
||||
|
||||
assert test_asset.asset_id != page.asset_id
|
||||
assert test_asset.description == page.description
|
||||
assert test_asset.status.name == page.status
|
||||
assert test_asset.category.name == page.category
|
||||
assert test_asset.date_acquired == page.date_acquired.date()
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
assert models.Asset.objects.last().description == test_asset.description
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetForm(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Health & Safety")
|
||||
self.status = models.AssetStatus.objects.create(name="O.K.", should_show=True)
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
self.parent = models.Asset.objects.create(asset_id="9000", description="Shelf", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2000, 1, 1))
|
||||
self.connector = models.Connector.objects.create(description="IEC", current_rating=10, voltage_rating=240,
|
||||
num_pins=3)
|
||||
self.cable_type = models.CableType.objects.create(plug=self.connector, socket=self.connector, circuits=1,
|
||||
cores=3)
|
||||
self.page = pages.AssetCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_asset_create(self):
|
||||
# Test that ID is automatically assigned and properly incremented
|
||||
# self.assertIn(self.page.asset_id, "9001") FIXME
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.asset_id = "XX$X"
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("An Asset ID can only consist of letters and numbers, with a final number",
|
||||
self.page.errors["Asset id"])
|
||||
self.assertIn("This field is required.", self.page.errors["Description"])
|
||||
|
||||
self.page.open()
|
||||
|
||||
self.page.description = desc = "Bodge Lead"
|
||||
self.page.category = cat = "Health & Safety"
|
||||
self.page.status = status = "O.K."
|
||||
self.page.serial_number = sn = "0124567890-SAUSAGE"
|
||||
self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.replacement_cost = "99.12"
|
||||
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
|
||||
self.page.purchased_from_selector.toggle()
|
||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
||||
self.page.purchased_from_selector.search(self.supplier.name[:-8])
|
||||
self.page.purchased_from_selector.set_option(self.supplier.name, True)
|
||||
|
||||
self.page.parent_selector.toggle()
|
||||
self.assertTrue(self.page.parent_selector.is_open)
|
||||
option = self.parent.asset_id
|
||||
self.page.parent_selector.search(option)
|
||||
time.sleep(2) # Slow down for javascript
|
||||
# self.page.parent_selector.set_option(option, True)
|
||||
# self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||
self.page.parent_selector.toggle()
|
||||
|
||||
self.assertFalse(self.driver.find_element(By.ID, 'cable-table').is_displayed())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
# Check that data is right
|
||||
asset = models.Asset.objects.get(asset_id="9001")
|
||||
self.assertEqual(asset.description, desc)
|
||||
self.assertEqual(asset.category.name, cat)
|
||||
self.assertEqual(asset.status.name, status)
|
||||
self.assertEqual(asset.serial_number, sn)
|
||||
self.assertEqual(asset.comments, comments)
|
||||
# This one is important as it defaults to today's date
|
||||
self.assertEqual(asset.date_acquired, acquired)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
models.Supplier.objects.create(name="Acme.")
|
||||
models.Supplier.objects.create(name="TEC PA & Lighting")
|
||||
models.Supplier.objects.create(name="Caterpillar Inc.")
|
||||
models.Supplier.objects.create(name="N.E.R.D")
|
||||
models.Supplier.objects.create(name="Khumalo")
|
||||
models.Supplier.objects.create(name="1984 Incorporated")
|
||||
self.page = pages.SupplierList(self.driver, self.live_server_url).open()
|
||||
|
||||
# Should be sorted alphabetically
|
||||
def test_order(self):
|
||||
names = list(map(lambda x: x.name, self.page.suppliers))
|
||||
self.assertEqual("1984 Incorporated", names[0])
|
||||
self.assertEqual("Acme.", names[1])
|
||||
self.assertEqual("Caterpillar Inc.", names[2])
|
||||
self.assertEqual("Fullmetal Heavy Industry", names[3])
|
||||
self.assertEqual("Khumalo", names[4])
|
||||
self.assertEqual("N.E.R.D", names[5])
|
||||
self.assertEqual("TEC PA & Lighting", names[6])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("TEC")
|
||||
self.page.search()
|
||||
|
||||
self.assertTrue(len(self.page.suppliers) == 1)
|
||||
self.assertEqual("TEC PA & Lighting", self.page.suppliers[0].name)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
time.sleep(1)
|
||||
self.assertTrue(len(self.page.suppliers) == 7)
|
||||
|
||||
self.page.set_query("This is not a supplier")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 0)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierCreateAndEdit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
|
||||
def test_supplier_create(self):
|
||||
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("This field is required.", self.page.errors["Name"])
|
||||
|
||||
self.page.name = "Optican Health Supplies"
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_supplier_edit(self):
|
||||
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
|
||||
|
||||
self.assertEqual("Fullmetal Heavy Industry", self.page.name)
|
||||
new_name = "Cyberdyne Systems"
|
||||
self.page.name = new_name
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
|
||||
def test_audit_search(logged_in_browser, live_server, test_asset):
|
||||
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
|
||||
# Check that a failed search works
|
||||
page.set_query("NOTFOUND")
|
||||
page.search()
|
||||
assert not logged_in_browser.find_by_id('modal').visible
|
||||
logged_in_browser.driver.implicitly_wait(4)
|
||||
assert logged_in_browser.is_text_present("Asset with that ID does not exist!")
|
||||
|
||||
|
||||
def test_audit_success(logged_in_browser, admin_user, live_server, test_asset):
|
||||
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
|
||||
wait = WebDriverWait(logged_in_browser.driver, 20)
|
||||
page.set_query(test_asset.asset_id)
|
||||
page.search()
|
||||
wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
# Now do it properly
|
||||
page.modal.description = new_desc = "A BIG hammer"
|
||||
page.modal.submit()
|
||||
logged_in_browser.driver.implicitly_wait(4)
|
||||
wait.until(animation_is_finished())
|
||||
submit_time = timezone.now()
|
||||
# Check data is correct
|
||||
test_asset.refresh_from_db()
|
||||
assert test_asset.description in new_desc
|
||||
# Make sure audit 'log' was filled out
|
||||
assert admin_user.initials == test_asset.last_audited_by.initials
|
||||
assert_times_almost_equal(submit_time, test_asset.last_audited_at)
|
||||
# Check we've removed it from the 'needing audit' list
|
||||
assert test_asset.asset_id not in page.assets
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetAudit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Haulage")
|
||||
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
|
||||
self.supplier = models.Supplier.objects.create(name="The Bazaar")
|
||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||
voltage_rating=40, num_pins=13)
|
||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
|
||||
category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||
self.wait = WebDriverWait(self.driver, 20)
|
||||
|
||||
def test_audit_fail(self):
|
||||
self.page.set_query(self.asset.asset_id)
|
||||
self.page.search()
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
# Do it wrong on purpose to check error display
|
||||
self.page.modal.remove_all_required()
|
||||
self.page.modal.description = ""
|
||||
self.page.modal.submit()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.driver.implicitly_wait(4)
|
||||
self.assertIn("This field is required.", self.page.modal.errors["Description"])
|
||||
|
||||
def test_audit_list(self):
|
||||
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
|
||||
asset_row = self.page.assets[0]
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
||||
self.page.modal.close()
|
||||
self.assertFalse(self.driver.find_element(By.ID, 'modal').is_displayed())
|
||||
# Make sure audit log was NOT filled out
|
||||
audited = models.Asset.objects.get(asset_id=asset_row.id)
|
||||
assert audited.last_audited_by is None
|
||||
@@ -1,42 +1,39 @@
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
|
||||
from pytest_django.asserts import assertFormError, assertRedirects
|
||||
|
||||
from PyRIGS.tests.base import assert_oembed, login
|
||||
|
||||
from assets import models
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_supplier_create(admin_client):
|
||||
url = reverse('supplier_create')
|
||||
url = reverse("supplier_create")
|
||||
response = admin_client.post(url)
|
||||
assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
assertFormError(response.context["form"], "name", "This field is required.")
|
||||
|
||||
|
||||
def test_supplier_edit(admin_client):
|
||||
supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
|
||||
url = reverse('supplier_update', kwargs={'pk': supplier.pk})
|
||||
response = admin_client.post(url, {'name': ""})
|
||||
assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
url = reverse("supplier_update", kwargs={"pk": supplier.pk})
|
||||
response = admin_client.post(url, {"name": ""})
|
||||
assertFormError(response.context["form"], "name", "This field is required.")
|
||||
|
||||
|
||||
def test_404(admin_client):
|
||||
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
|
||||
urls = {"asset_detail", "asset_update", "asset_duplicate", "supplier_detail", "supplier_update"}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "0000"})
|
||||
request_url = reverse(url_name, kwargs={"pk": "0000"})
|
||||
response = admin_client.get(request_url, follow=True)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_embed_login_redirect(client, django_user_model, test_asset):
|
||||
request_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
request_url = reverse("asset_embed", kwargs={"pk": test_asset.asset_id})
|
||||
expected_url = "{0}?next={1}".format(reverse("login_embed"), request_url)
|
||||
|
||||
# Request the page and check it redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
@@ -51,14 +48,14 @@ def test_embed_login_redirect(client, django_user_model, test_asset):
|
||||
|
||||
|
||||
def test_login_cookie_warning(client, django_user_model):
|
||||
login_url = reverse('login_embed')
|
||||
login_url = reverse("login_embed")
|
||||
response = client.post(login_url, follow=True)
|
||||
assert "Cookies do not seem to be enabled" in str(response.content)
|
||||
|
||||
|
||||
def test_x_frame_headers(client, django_user_model, test_asset):
|
||||
asset_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
|
||||
login_url = reverse('login_embed')
|
||||
asset_url = reverse("asset_embed", kwargs={"pk": test_asset.asset_id})
|
||||
login_url = reverse("login_embed")
|
||||
|
||||
login(client, django_user_model)
|
||||
|
||||
@@ -73,58 +70,78 @@ def test_x_frame_headers(client, django_user_model, test_asset):
|
||||
|
||||
def test_oembed(client, test_asset):
|
||||
client.logout()
|
||||
asset_url = reverse('asset_detail', kwargs={'pk': test_asset.asset_id})
|
||||
asset_embed_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
|
||||
oembed_url = reverse('asset_oembed', kwargs={'pk': test_asset.asset_id})
|
||||
asset_url = reverse("asset_detail", kwargs={"pk": test_asset.asset_id})
|
||||
asset_embed_url = reverse("asset_embed", kwargs={"pk": test_asset.asset_id})
|
||||
oembed_url = reverse("asset_oembed", kwargs={"pk": test_asset.asset_id})
|
||||
|
||||
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
|
||||
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
|
||||
alt_oembed_url = reverse("asset_oembed", kwargs={"pk": 999})
|
||||
alt_asset_embed_url = reverse("asset_embed", kwargs={"pk": 999})
|
||||
|
||||
assert_oembed(alt_asset_embed_url, alt_oembed_url, client, asset_embed_url, asset_url, oembed_url)
|
||||
|
||||
|
||||
def test_asset_create(admin_client):
|
||||
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'replacement_cost': '-30'})
|
||||
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
response = admin_client.post(
|
||||
reverse("asset_create"),
|
||||
{"date_sold": "2000-01-01", "date_acquired": "2020-01-01", "purchase_price": "-30", "replacement_cost": "-30"},
|
||||
)
|
||||
assertFormError(
|
||||
response.context["form"],
|
||||
"asset_id",
|
||||
["This field is required.", "An Asset ID can only consist of letters and numbers, with a final number"],
|
||||
)
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
|
||||
def test_cable_create(admin_client):
|
||||
response = admin_client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True})
|
||||
assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
|
||||
assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
response = admin_client.post(reverse("asset_create"), {"asset_id": "X$%A", "is_cable": True})
|
||||
assertFormError(
|
||||
response.context["form"], "asset_id", "An Asset ID can only consist of letters and numbers, with a final number"
|
||||
)
|
||||
assertFormError(response.context["form"], "cable_type", "A cable must have a type")
|
||||
assertFormError(response.context["form"], "length", "The length of a cable must be more than 0")
|
||||
assertFormError(response.context["form"], "csa", "The CSA of a cable must be more than 0")
|
||||
|
||||
|
||||
def test_asset_edit(admin_client, test_asset):
|
||||
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
|
||||
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'replacement_cost': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
url = reverse("asset_update", kwargs={"pk": test_asset.asset_id})
|
||||
response = admin_client.post(
|
||||
url,
|
||||
{
|
||||
"date_sold": "2000-12-01",
|
||||
"date_acquired": "2020-12-01",
|
||||
"purchase_price": "-50",
|
||||
"replacement_cost": "-50",
|
||||
"description": "",
|
||||
"status": "",
|
||||
"category": "",
|
||||
},
|
||||
)
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
|
||||
def test_cable_edit(admin_client, test_cable):
|
||||
url = reverse('asset_update', kwargs={'pk': test_cable.asset_id})
|
||||
response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
|
||||
url = reverse("asset_update", kwargs={"pk": test_cable.asset_id})
|
||||
response = admin_client.post(url, {"is_cable": True, "length": -3, "csa": -3})
|
||||
|
||||
# TODO Can't figure out how to select the 'none' option...
|
||||
# assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
# assertFormError(response.context['form'], 'cable_type', 'A cable must have a type')
|
||||
assertFormError(response.context["form"], "length", "The length of a cable must be more than 0")
|
||||
assertFormError(response.context["form"], "csa", "The CSA of a cable must be more than 0")
|
||||
|
||||
|
||||
def test_asset_duplicate(admin_client, test_cable):
|
||||
url = reverse('asset_duplicate', kwargs={'pk': test_cable.asset_id})
|
||||
response = admin_client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
|
||||
url = reverse("asset_duplicate", kwargs={"pk": test_cable.asset_id})
|
||||
response = admin_client.post(url, {"is_cable": True, "length": 0, "csa": 0})
|
||||
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
assertFormError(response.context["form"], "length", "The length of a cable must be more than 0")
|
||||
assertFormError(response.context["form"], "csa", "The CSA of a cable must be more than 0")
|
||||
|
||||
|
||||
def assert_asset_form_errors(response):
|
||||
assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'replacement_cost', 'A price cannot be negative')
|
||||
assertFormError(response.context["form"], "description", "This field is required.")
|
||||
assertFormError(response.context["form"], "status", "This field is required.")
|
||||
assertFormError(response.context["form"], "category", "This field is required.")
|
||||
assertFormError(response.context["form"], "date_sold", "Cannot sell an item before it is acquired")
|
||||
assertFormError(response.context["form"], "purchase_price", "A price cannot be negative")
|
||||
assertFormError(response.context["form"], "replacement_cost", "A price cannot be negative")
|
||||
|
||||
Reference in New Issue
Block a user