mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-12 23:18:23 +00:00
Create initial asset audit framework (#403)
* WIP: Basic work on audit * WIP: Audit modal works Need to get the ID search working. * WIP: Javascript shenanigans for asset audit search It's not clean but it works.. * Improve audit search bar Optimise for APM! * Filter asset audit list by never-audited * Added cable functionality to audit form Also improved styling * FIX: Revert partialising of asset search * Various UX Improvements Also rearranged asset detail/edit to be more space efficient * FIX: Remove assets from to-be-audited table when audited Previously required a page reload * Improve sample data generator Does reversion properly and sets colours for asset statuses * FIX: Gracefully handle 404s in audit search * FEAT: Add buttons for some common defaults on audit form TODO: Partialise those fragments and add them to the edit/create forms too. * FIX: Fix asset sample data command when run alone * FEAT: More handy buttons * FIX: Stop quickbuttons being tab-selected If someone's tabbing through, they won't be needing the buttons... * FIX: Hide asset detail buttons for basic users * FIX: Migrations * Start tests for audit * Some deduplication for testing code * Improve asset audit testing * Remember to test the tests Arona * Potentially make modal tests more consistent * FIX?: Up WebDriverWait timeout for modal tests * FIX?: What about this way... * Remake migrations * Fix README badges to point to right branch While I'm here eh :P * Use aware time in audit * Fix migrations again * Fix for my fix... * Modify audit exclusions to properly prevent data loss * pep eiiiiiight
This commit is contained in:
@@ -6,7 +6,7 @@ from selenium.webdriver import Chrome
|
||||
from django.urls import reverse
|
||||
from PyRIGS.tests import regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage
|
||||
import pdb
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
|
||||
class AssetList(BasePage):
|
||||
@@ -95,11 +95,6 @@ class AssetForm(FormPage):
|
||||
def parent_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._parent_select_locator))
|
||||
|
||||
def submit(self):
|
||||
previous_errors = self.errors
|
||||
self.find_element(*self._submit_locator).click()
|
||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
||||
|
||||
|
||||
class AssetEdit(AssetForm):
|
||||
URL_TEMPLATE = '/assets/asset/id/{asset_id}/edit/'
|
||||
@@ -162,11 +157,6 @@ class SupplierForm(FormPage):
|
||||
'name': (regions.TextBox, (By.ID, 'id_name')),
|
||||
}
|
||||
|
||||
def submit(self):
|
||||
previous_errors = self.errors
|
||||
self.find_element(*self._submit_locator).click()
|
||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
||||
|
||||
|
||||
class SupplierCreate(SupplierForm):
|
||||
URL_TEMPLATE = reverse('supplier_create')
|
||||
@@ -183,3 +173,90 @@ class SupplierEdit(SupplierForm):
|
||||
@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_query')
|
||||
_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, 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()
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
try:
|
||||
return self.find_element(*self._errors_selector)
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
class AssetAuditModal(Region):
|
||||
_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")
|
||||
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')),
|
||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||
'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 submit(self):
|
||||
previous_errors = self.errors
|
||||
self.root.find_element(*self._submit_locator).click()
|
||||
# self.wait.until(lambda x: not self.is_displayed) TODO
|
||||
|
||||
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\")});")
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
return form_element.value
|
||||
else:
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
form_element.set_value(value)
|
||||
else:
|
||||
self.__dict__[name] = value
|
||||
|
||||
@@ -9,8 +9,13 @@ from RIGS import models as rigsmodels
|
||||
from PyRIGS.tests.base import BaseTest, AutoLoginTest
|
||||
from assets import models, urls
|
||||
from reversion import revisions as reversion
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from RIGS.test_functional import animation_is_finished
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class TestAssetList(AutoLoginTest):
|
||||
@@ -255,6 +260,76 @@ class TestSupplierCreateAndEdit(AutoLoginTest):
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
|
||||
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))
|
||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||
self.wait = WebDriverWait(self.driver, 5)
|
||||
|
||||
def test_audit_process(self):
|
||||
asset_id = "1111"
|
||||
self.page.set_query(asset_id)
|
||||
self.page.search()
|
||||
mdl = self.page.modal
|
||||
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
|
||||
# Do it wrong on purpose to check error display
|
||||
mdl.remove_all_required()
|
||||
mdl.description = ""
|
||||
mdl.submit()
|
||||
# self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.wait.until(animation_is_finished())
|
||||
# self.assertTrue(self.driver.find_element_by_id('modal').is_displayed())
|
||||
self.assertIn("This field is required.", mdl.errors["Description"])
|
||||
# Now do it properly
|
||||
new_desc = "A BIG hammer"
|
||||
mdl.description = new_desc
|
||||
mdl.submit()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
|
||||
# Check data is correct
|
||||
audited = models.Asset.objects.get(asset_id="1111")
|
||||
self.assertEqual(audited.description, new_desc)
|
||||
# Make sure audit 'log' was filled out
|
||||
self.assertEqual(self.profile.initials, audited.last_audited_by.initials)
|
||||
self.assertEqual(timezone.now().date(), audited.last_audited_at.date())
|
||||
self.assertEqual(timezone.now().hour, audited.last_audited_at.hour)
|
||||
self.assertEqual(timezone.now().minute, audited.last_audited_at.minute)
|
||||
# Check we've removed it from the 'needing audit' list
|
||||
self.assertNotIn(asset_id, self.page.assets)
|
||||
|
||||
def test_audit_list(self):
|
||||
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
|
||||
|
||||
assetRow = self.page.assets[0]
|
||||
assetRow.find_element(By.CSS_SELECTOR, "td:nth-child(5) > div:nth-child(1) > a:nth-child(1)").click()
|
||||
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertEqual(self.page.modal.asset_id, assetRow.id)
|
||||
|
||||
# First close button is for the not found error
|
||||
self.page.find_element(By.XPATH, '(//button[@class="close"])[2]').click()
|
||||
self.wait.until(animation_is_finished())
|
||||
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=assetRow.id)
|
||||
self.assertEqual(None, audited.last_audited_by)
|
||||
|
||||
# Check that a failed search works
|
||||
self.page.set_query("NOTFOUND")
|
||||
self.page.search()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
self.assertIn("Asset with that ID does not exist!", self.page.error.text)
|
||||
|
||||
|
||||
class TestSupplierValidation(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
Reference in New Issue
Block a user