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:
2020-04-14 21:11:09 +01:00
committed by GitHub
parent 0fe7d55eab
commit 3be06a7b25
18 changed files with 595 additions and 94 deletions

View File

@@ -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

View File

@@ -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):