diff --git a/PyRIGS/tests/base.py b/PyRIGS/tests/base.py
new file mode 100644
index 00000000..ecdacd21
--- /dev/null
+++ b/PyRIGS/tests/base.py
@@ -0,0 +1,36 @@
+from django.test import LiveServerTestCase
+from selenium import webdriver
+from RIGS import models as rigsmodels
+from . import pages
+import os
+
+
+def create_browser():
+ options = webdriver.ChromeOptions()
+ options.add_argument("--window-size=1920,1080")
+ if os.environ.get('CI', False):
+ options.add_argument("--headless")
+ options.add_argument("--no-sandbox")
+ driver = webdriver.Chrome(chrome_options=options)
+ return driver
+
+
+class BaseTest(LiveServerTestCase):
+ def setUp(self):
+ super().setUpClass()
+ self.driver = create_browser()
+
+ def tearDown(self):
+ super().tearDown()
+ self.driver.quit()
+
+
+class AutoLoginTest(BaseTest):
+ def setUp(self):
+ super().setUp()
+ self.profile = rigsmodels.Profile(
+ username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
+ self.profile.set_password("EventTestPassword")
+ self.profile.save()
+ loginPage = pages.LoginPage(self.driver, self.live_server_url).open()
+ loginPage.login("EventTest", "EventTestPassword")
diff --git a/PyRIGS/tests/pages.py b/PyRIGS/tests/pages.py
new file mode 100644
index 00000000..4bf34a6d
--- /dev/null
+++ b/PyRIGS/tests/pages.py
@@ -0,0 +1,86 @@
+from pypom import Page, Region
+from selenium.webdriver.common.by import By
+from selenium.webdriver import Chrome
+from selenium.common.exceptions import NoSuchElementException
+
+
+class BasePage(Page):
+ form_items = {}
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ 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
+
+
+class FormPage(BasePage):
+ _errors_selector = (By.CLASS_NAME, "alert-danger")
+
+ 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\")});")
+
+ @property
+ def errors(self):
+ try:
+ error_page = self.ErrorPage(self, self.find_element(*self._errors_selector))
+ return error_page.errors
+ except NoSuchElementException:
+ return None
+
+ class ErrorPage(Region):
+ _error_item_selector = (By.CSS_SELECTOR, "dl>span")
+
+ class ErrorItem(Region):
+ _field_selector = (By.CSS_SELECTOR, "dt")
+ _error_selector = (By.CSS_SELECTOR, "dd>ul>li")
+
+ @property
+ def field_name(self):
+ return self.find_element(*self._field_selector).text
+
+ @property
+ def errors(self):
+ return [x.text for x in self.find_elements(*self._error_selector)]
+
+ @property
+ def errors(self):
+ error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
+ errors = {}
+ for error in error_items:
+ errors[error.field_name] = error.errors
+ return errors
+
+
+class LoginPage(BasePage):
+ URL_TEMPLATE = '/user/login'
+
+ _username_locator = (By.ID, 'id_username')
+ _password_locator = (By.ID, 'id_password')
+ _submit_locator = (By.ID, 'id_submit')
+ _error_locator = (By.CSS_SELECTOR, '.errorlist>li')
+
+ def login(self, username, password):
+ username_element = self.find_element(*self._username_locator)
+ username_element.clear()
+ username_element.send_keys(username)
+
+ password_element = self.find_element(*self._password_locator)
+ password_element.clear()
+ password_element.send_keys(password)
+
+ self.find_element(*self._submit_locator).click()
diff --git a/PyRIGS/tests/regions.py b/PyRIGS/tests/regions.py
new file mode 100644
index 00000000..5dd364ff
--- /dev/null
+++ b/PyRIGS/tests/regions.py
@@ -0,0 +1,133 @@
+from pypom import Region
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions
+from selenium.webdriver.remote.webelement import WebElement
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support.select import Select
+import datetime
+
+
+def parse_bool_from_string(string):
+ # Used to convert from attribute strings to boolean values, written after I found this:
+ # >>> bool("false")
+ # True
+ if string == "true":
+ return True
+ else:
+ return False
+
+
+class BootstrapSelectElement(Region):
+ _main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
+ _option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
+ _option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a[role=option]')
+ _select_all_locator = (By.CLASS_NAME, 'bs-select-all')
+ _deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
+ _search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
+ _status_locator = (By.CLASS_NAME, 'status')
+
+ @property
+ def is_open(self):
+ return parse_bool_from_string(self.find_element(*self._main_button_locator).get_attribute("aria-expanded"))
+
+ def toggle(self):
+ original_state = self.is_open
+ return self.find_element(*self._main_button_locator).click()
+ option_box = self.find_element(*self._option_box_locator)
+ if original_state:
+ self.wait.until(expected_conditions.invisibility_of_element_located(option_box))
+ else:
+ self.wait.until(expected_conditions.visibility_of_element_located(option_box))
+
+ def open(self):
+ if not self.is_open:
+ self.toggle()
+
+ def close(self):
+ if self.is_open:
+ self.toggle()
+
+ def select_all(self):
+ self.find_element(*self._select_all_locator).click()
+
+ def deselect_all(self):
+ self.find_element(*self._deselect_all_locator).click()
+
+ def search(self, query):
+ search_box = self.find_element(*self._search_locator)
+ search_box.clear()
+ search_box.send_keys(query)
+ status_text = self.find_element(*self._status_locator)
+ self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
+
+ @property
+ def options(self):
+ options = list(self.find_elements(*self._option_locator))
+ return [self.BootstrapSelectOption(self, i) for i in options]
+
+ def set_option(self, name, selected):
+ options = list((x for x in self.options if x.name == name))
+ assert len(options) == 1
+ options[0].set_selected(selected)
+
+ class BootstrapSelectOption(Region):
+ _text_locator = (By.CLASS_NAME, 'text')
+
+ @property
+ def selected(self):
+ return parse_bool_from_string(self.root.get_attribute("aria-selected"))
+
+ def toggle(self):
+ self.root.click()
+
+ def set_selected(self, selected):
+ if self.selected != selected:
+ self.toggle()
+
+ @property
+ def name(self):
+ return self.find_element(*self._text_locator).text
+
+
+class TextBox(Region):
+ @property
+ def value(self):
+ return self.root.get_attribute("value")
+
+ def set_value(self, value):
+ self.root.clear()
+ self.root.send_keys(value)
+
+
+class CheckBox(Region):
+ def toggle(self):
+ self.root.click()
+
+ @property
+ def value(self):
+ return parse_bool_from_string(self.root.get_attribute("checked"))
+
+ def set_value(self, value):
+ if value != self.value:
+ self.toggle()
+
+
+class DatePicker(Region):
+ @property
+ def value(self):
+ return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d")
+
+ def set_value(self, value):
+ self.root.clear()
+ self.root.send_keys(value.strftime("%d%m%Y"))
+
+
+class SingleSelectPicker(Region):
+ @property
+ def value(self):
+ picker = Select(self.root)
+ return picker.first_selected_option.text
+
+ def set_value(self, value):
+ picker = Select(self.root)
+ picker.select_by_visible_text(value)
diff --git a/RIGS/templates/RIGS/organisation_detail.html b/RIGS/templates/RIGS/organisation_detail.html
index b12b6391..ca16aaa6 100644
--- a/RIGS/templates/RIGS/organisation_detail.html
+++ b/RIGS/templates/RIGS/organisation_detail.html
@@ -1,4 +1,4 @@
-{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
+ {% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% block title %}Organisation | {{ object.name }}{% endblock %}
diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py
index 8ea2481b..691f5294 100644
--- a/RIGS/test_functional.py
+++ b/RIGS/test_functional.py
@@ -20,15 +20,13 @@ from selenium.webdriver.support.ui import WebDriverWait
from RIGS import models
+from reversion import revisions as reversion
+from django.urls import reverse
+from django.core import mail, signing
+from PyRIGS.tests.base import create_browser
+from django.conf import settings
-def create_browser():
- options = webdriver.ChromeOptions()
- options.add_argument("--window-size=1920,1080")
- if os.environ.get('CI', False):
- options.add_argument("--headless")
- options.add_argument("--no-sandbox")
- driver = webdriver.Chrome(chrome_options=options)
- return driver
+import sys
class UserRegistrationTest(LiveServerTestCase):
diff --git a/RIGS/test_models.py b/RIGS/test_models.py
index 424e2f06..d35cbbe2 100644
--- a/RIGS/test_models.py
+++ b/RIGS/test_models.py
@@ -1,5 +1,3 @@
-
-
import pytz
from reversion import revisions as reversion
from django.conf import settings
@@ -8,6 +6,7 @@ from django.test import TestCase
from RIGS import models, versioning
from datetime import date, timedelta, datetime, time
from decimal import *
+from PyRIGS.tests.base import create_browser
class ProfileTestCase(TestCase):
diff --git a/assets/apps.py b/assets/apps.py
deleted file mode 100644
index 5569d303..00000000
--- a/assets/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class AssetsConfig(AppConfig):
- name = 'assets'
diff --git a/assets/filters.py b/assets/filters.py
deleted file mode 100644
index b7cc8ffa..00000000
--- a/assets/filters.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import django_filters
-
-from assets import models
-
-
-class AssetFilter(django_filters.FilterSet):
- class Meta:
- model = models.Asset
- fields = ['asset_id', 'description', 'serial_number', 'category', 'status']
diff --git a/assets/management/commands/import_old_db.py b/assets/management/commands/import_old_db.py
deleted file mode 100644
index 0fcff787..00000000
--- a/assets/management/commands/import_old_db.py
+++ /dev/null
@@ -1,229 +0,0 @@
-import os
-import datetime
-import xml.etree.ElementTree as ET
-from django.core.management.base import BaseCommand
-from django.conf import settings
-
-from assets import models
-
-
-class Command(BaseCommand):
- help = 'Imports old db from XML dump'
-
- epoch = datetime.date(1970, 1, 1)
-
- def handle(self, *args, **options):
- self.import_categories()
- self.import_statuses()
- self.import_suppliers()
- self.import_collections()
- self.import_assets()
- self.import_cables()
-
- @staticmethod
- def xml_path(file):
- return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
-
- @staticmethod
- def parse_xml(file):
- tree = ET.parse(file)
-
- return tree.getroot()
-
- def import_categories(self):
- # 0: updated, 1: created
- tally = [0, 0]
- root = self.parse_xml(self.xml_path('TEC_Asset_Categories.xml'))
-
- for child in root:
- obj, created = models.AssetCategory.objects.update_or_create(
- pk=int(child.find('AssetCategoryID').text),
- name=child.find('AssetCategory').text
- )
-
- if created:
- tally[1] += 1
- else:
- tally[0] += 1
-
- print('Categories - Updated: {}, Created: {}'.format(tally[0], tally[1]))
-
- def import_statuses(self):
- # 0: updated, 1: created
- tally = [0, 0]
- root = self.parse_xml(self.xml_path('TEC_Asset_Status_new.xml'))
-
- for child in root:
- obj, created = models.AssetStatus.objects.update_or_create(
- pk=int(child.find('StatusID').text),
- name=child.find('Status').text
- )
-
- if created:
- tally[1] += 1
- else:
- tally[0] += 1
-
- print('Statuses - Updated: {}, Created: {}'.format(tally[0], tally[1]))
-
- def import_suppliers(self):
- # 0: updated, 1: created
- tally = [0, 0]
- root = self.parse_xml(self.xml_path('TEC_Asset_Suppliers_new.xml'))
-
- for child in root:
- obj, created = models.Supplier.objects.update_or_create(
- pk=int(child.find('Supplier_x0020_Id').text),
- name=child.find('Supplier_x0020_Name').text
- )
-
- if created:
- tally[1] += 1
- else:
- tally[0] += 1
-
- print('Suppliers - Updated: {}, Created: {}'.format(tally[0], tally[1]))
-
- def import_assets(self):
- # 0: updated, 1: created
- tally = [0, 0]
- root = self.parse_xml(self.xml_path('TEC_Assets.xml'))
-
- for child in root:
- defaults = dict()
-
- # defaults['pk'] = int(child.find('ID').text)
- defaults['asset_id'] = child.find('AssetID').text
-
- try:
- defaults['description'] = child.find('AssetDescription').text
- except AttributeError:
- defaults['description'] = 'None'
-
- defaults['category'] = models.AssetCategory.objects.get(pk=int(child.find('AssetCategoryID').text))
- defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('StatusID').text))
-
- try:
- defaults['serial_number'] = child.find('SerialNumber').text
- except AttributeError:
- pass
-
- try:
- defaults['purchased_from'] = models.Supplier.objects.get(pk=int(child.find('Supplier_x0020_Id').text))
- except AttributeError:
- pass
-
- try:
- defaults['date_acquired'] = datetime.datetime.strptime(child.find('DateAcquired').text, '%d/%m/%Y').date()
- except AttributeError:
- defaults['date_acquired'] = self.epoch
-
- try:
- defaults['date_sold'] = datetime.datetime.strptime(child.find('DateSold').text, '%d/%m/%Y').date()
- except AttributeError:
- pass
-
- try:
- defaults['purchase_price'] = float(child.find('Replacement_x0020_Value').text)
- except AttributeError:
- pass
-
- try:
- defaults['salvage_value'] = float(child.find('SalvageValue').text)
- except AttributeError:
- pass
-
- try:
- defaults['comments'] = child.find('Comments').text
- except AttributeError:
- pass
-
- try:
- date = child.find('NextSchedMaint').text.split('T')[0]
- defaults['next_sched_maint'] = datetime.datetime.strptime(date, '%Y-%m-%d').date()
- except AttributeError:
- pass
-
- print(defaults)
-
- obj, created = models.Asset.objects.update_or_create(**defaults)
-
- if created:
- tally[1] += 1
- else:
- tally[0] += 1
-
- print('Assets - Updated: {}, Created: {}'.format(tally[0], tally[1]))
-
- def import_collections(self):
- tally = [0, 0]
- root = self.parse_xml(self.xml_path('TEC_Cable_Collections.xml'))
-
- for child in root:
- defaults = dict()
-
- defaults['pk'] = int(child.find('ID').text)
- defaults['name'] = child.find('Cable_x0020_Trunk').text
-
- obj, created = models.Collection.objects.update_or_create(**defaults)
-
- if created:
- tally[1] += 1
- else:
- tally[0] += 1
-
- print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))
-
- def import_cables(self):
- tally = [0, 0]
- root = self.parse_xml(self.xml_path('TEC_Cables.xml'))
-
- for child in root:
- defaults = dict()
-
- defaults['asset_id'] = child.find('Asset_x0020_Number').text
-
- try:
- defaults['description'] = child.find('Type_x0020_of_x0020_Cable').text
- except AttributeError:
- defaults['description'] = 'None'
-
- defaults['is_cable'] = True
- defaults['category'] = models.AssetCategory.objects.get(pk=9)
-
- try:
- defaults['length'] = child.find('Length_x0020__x0028_m_x0029_').text
- except AttributeError:
- pass
-
- defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('Status').text))
-
- try:
- defaults['comments'] = child.find('Comments').text
- except AttributeError:
- pass
-
- try:
- collection_id = int(child.find('Collection').text)
- if collection_id != 0:
- defaults['collection'] = models.Collection.objects.get(pk=collection_id)
- except AttributeError:
- pass
-
- try:
- defaults['purchase_price'] = float(child.find('Purchase_x0020_Price').text)
- except AttributeError:
- pass
-
- defaults['date_acquired'] = self.epoch
-
- print(defaults)
-
- obj, created = models.Asset.objects.update_or_create(**defaults)
-
- if created:
- tally[1] += 1
- else:
- tally[0] += 1
-
- print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))
diff --git a/assets/management/commands/update_old_db_file.py b/assets/management/commands/update_old_db_file.py
deleted file mode 100644
index bff0fe22..00000000
--- a/assets/management/commands/update_old_db_file.py
+++ /dev/null
@@ -1,110 +0,0 @@
-import os
-import datetime
-import xml.etree.ElementTree as ET
-from django.core.management.base import BaseCommand
-from django.conf import settings
-
-
-class Command(BaseCommand):
- help = 'Imports old db from XML dump'
-
- epoch = datetime.date(1970, 1, 1)
-
- def handle(self, *args, **options):
- # self.update_statuses()
- # self.update_suppliers()
- self.update_cable_statuses()
-
- @staticmethod
- def xml_path(file):
- return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
-
- @staticmethod
- def parse_xml(file):
- tree = ET.parse(file)
-
- return tree.getroot()
-
- def update_statuses(self):
- file = self.xml_path('TEC_Assets.xml')
- tree = ET.parse(file)
- root = tree.getroot()
-
- # map old status pk to new status pk
- status_map = {
- 2: 2,
- 3: 4,
- 4: 3,
- 5: 5,
- 6: 1
- }
-
- for child in root:
- status = int(child.find('StatusID').text)
- child.find('StatusID').text = str(status_map[status])
-
- tree.write(file)
-
- def update_suppliers(self):
- old_file = self.xml_path('TEC_Asset_Suppliers.xml')
- old_tree = ET.parse(old_file)
- old_root = old_tree.getroot()
-
- new_file = self.xml_path('TEC_Asset_Suppliers_new.xml')
- new_tree = ET.parse(new_file)
- new_root = new_tree.getroot()
-
- # map old supplier pk to new supplier pk
- supplier_map = dict()
-
- def find_in_old(name, root):
- for child in root:
- found_id = child.find('Supplier_x0020_Id').text
- found_name = child.find('Supplier_x0020_Name').text
-
- if found_name == name:
- return found_id
-
- for new_child in new_root:
- new_id = new_child.find('Supplier_x0020_Id').text
- new_name = new_child.find('Supplier_x0020_Name').text
-
- old_id = find_in_old(new_name, old_root)
-
- supplier_map[int(old_id)] = int(new_id)
-
- file = self.xml_path('TEC_Assets.xml')
- tree = ET.parse(file)
- root = tree.getroot()
-
- for child in root:
- try:
- supplier = int(child.find('Supplier_x0020_Id').text)
- child.find('Supplier_x0020_Id').text = str(supplier_map[supplier])
- except AttributeError:
- pass
-
- tree.write(file)
-
- def update_cable_statuses(self):
- file = self.xml_path('TEC_Cables.xml')
- tree = ET.parse(file)
- root = tree.getroot()
-
- # map old status pk to new status pk
- status_map = {
- 0: 7,
- 1: 3,
- 3: 2,
- 4: 5,
- 6: 6,
- 7: 1,
- 8: 4,
- 9: 2,
- }
-
- for child in root:
- status = int(child.find('Status').text)
- child.find('Status').text = str(status_map[status])
-
- tree.write(file)
diff --git a/assets/migrations/0010_auto_20200207_1737.py b/assets/migrations/0010_auto_20200207_1737.py
new file mode 100644
index 00000000..6ffeb824
--- /dev/null
+++ b/assets/migrations/0010_auto_20200207_1737.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.0.13 on 2020-02-07 17:37
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assets', '0009_auto_20200103_2215'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='supplier',
+ options={'ordering': ['name'], 'permissions': (('view_supplier', 'Can view a supplier'),)},
+ ),
+ ]
diff --git a/assets/models.py b/assets/models.py
index 122fdcb2..5bf830bc 100644
--- a/assets/models.py
+++ b/assets/models.py
@@ -44,6 +44,7 @@ class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80)
class Meta:
+ ordering = ['name']
permissions = (
('view_supplier', 'Can view a supplier'),
)
diff --git a/assets/templates/asset_create.html b/assets/templates/asset_create.html
index bc953d2d..14b4b66a 100644
--- a/assets/templates/asset_create.html
+++ b/assets/templates/asset_create.html
@@ -1,6 +1,5 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
-{% load asset_templatetags %}
{% block title %}Asset {{ object.asset_id }}{% endblock %}
{% block content %}
diff --git a/assets/templates/asset_list.html b/assets/templates/asset_list.html
index 90bb2346..cbd27b28 100644
--- a/assets/templates/asset_list.html
+++ b/assets/templates/asset_list.html
@@ -17,16 +17,16 @@