mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-26 08:08:23 +00:00
Merge branch 'master' into markdown
# Conflicts: # RIGS/templates/RIGS/event_detail.html # RIGS/test_unit.py # requirements.txt
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,6 +44,7 @@ htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
.pytest_cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
@@ -107,3 +108,4 @@ atlassian-ide-plugin.xml
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
.vscode/
|
||||
1
Procfile
1
Procfile
@@ -1 +1,2 @@
|
||||
release: python manage.py migrate
|
||||
web: gunicorn PyRIGS.wsgi --log-file -
|
||||
|
||||
@@ -6,6 +6,34 @@ from django.urls import reverse
|
||||
from RIGS import models
|
||||
|
||||
|
||||
def get_oembed(login_url, request, oembed_view, kwargs):
|
||||
context = {}
|
||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
|
||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||
resp = render(request, 'login_redirect.html', context=context)
|
||||
return resp
|
||||
|
||||
|
||||
def has_oembed(oembed_view, login_url=None):
|
||||
if not login_url:
|
||||
from django.conf import settings
|
||||
login_url = settings.LOGIN_URL
|
||||
|
||||
def _dec(view_func):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
if oembed_view is not None:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
_checklogin.__doc__ = view_func.__doc__
|
||||
_checklogin.__dict__ = view_func.__dict__
|
||||
return _checklogin
|
||||
return _dec
|
||||
|
||||
|
||||
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
||||
"""
|
||||
Decorator for views that checks that the user passes the given test.
|
||||
@@ -25,11 +53,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
||||
return view_func(request, *args, **kwargs)
|
||||
elif not request.user.is_authenticated:
|
||||
if oembed_view is not None:
|
||||
context = {}
|
||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
|
||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||
resp = render(request, 'login_redirect.html', context=context)
|
||||
return resp
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
else:
|
||||
|
||||
@@ -58,6 +58,7 @@ INSTALLED_APPS = (
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'RIGS',
|
||||
'assets',
|
||||
|
||||
'debug_toolbar',
|
||||
'registration',
|
||||
|
||||
36
PyRIGS/tests/base.py
Normal file
36
PyRIGS/tests/base.py
Normal file
@@ -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")
|
||||
86
PyRIGS/tests/pages.py
Normal file
86
PyRIGS/tests/pages.py
Normal file
@@ -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()
|
||||
133
PyRIGS/tests/regions.py
Normal file
133
PyRIGS/tests/regions.py
Normal file
@@ -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)
|
||||
@@ -12,6 +12,7 @@ urlpatterns = [
|
||||
# url(r'^blog/', include('blog.urls')),
|
||||
|
||||
url(r'^', include('RIGS.urls')),
|
||||
url('^assets/', include('assets.urls')),
|
||||
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
|
||||
name="registration_register"),
|
||||
url('^user/', include('django.contrib.auth.urls')),
|
||||
|
||||
16
README.md
16
README.md
@@ -1,20 +1,13 @@
|
||||
# TEC PA & Lighting - PyRIGS #
|
||||
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|
||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop)
|
||||
[](https://gemnasium.com/github.com/nottinghamtec/PyRIGS)
|
||||
|
||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
||||
|
||||
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
|
||||
|
||||
The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails.
|
||||
|
||||
At this stage the project is very early on, and the main focus has been on getting a working system that can be tested and put into use ASAP due to the imminent failure of the existing system. Because of this, the documentation is still quite weak, but this should be fixed as time goes on.
|
||||
|
||||
This document is intended to get you up and running, but if don't care about what I have to say, just clone the sodding repository and have a poke around with what's in it, but for GODS SAKE DO NOT PUSH WITHOUT TESTING.
|
||||
|
||||
### What is this repository for? ###
|
||||
For the rapid development of the application for medium term deployment, the main branch is being used.
|
||||
Once the application is deployed in a production environment, other branches should be used to properly stage edits and pushes of new features. When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
|
||||
When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
|
||||
|
||||
Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework.
|
||||
|
||||
@@ -26,7 +19,7 @@ For the more experienced developer/somebody who doesn't want a full IDE and want
|
||||
Please contact TJP for details on how to acquire these.
|
||||
|
||||
### Python Environment ###
|
||||
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Mainly the C implementation of Python 2 (CPython 2) has been used (specifically the Python 2.7 standard). Most of the application has been written with Python 3 in mind however, and should run without issue. Some level of testing on Python 3 has been done, but there is no guarantee it will work (for more information on this please see [[Python Version]] on the wiki)
|
||||
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Orginally written with the C implementation of Python 2 (CPython 2, specifically the Python 2.7 standard), the application now runs in Python 3.
|
||||
|
||||
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
|
||||
|
||||
@@ -115,5 +108,4 @@ python manage.py test RIGS.test_models.EventTestCase.test_current_events
|
||||
|
||||
```
|
||||
|
||||
### Committing, pushing and testing ###
|
||||
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.
|
||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||
|
||||
@@ -22,7 +22,7 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
||||
|
||||
class Meta:
|
||||
model = models.Profile
|
||||
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone')
|
||||
fields = ('username', 'email', 'first_name', 'last_name', 'initials')
|
||||
|
||||
def clean_initials(self):
|
||||
"""
|
||||
@@ -129,6 +129,11 @@ class EventForm(forms.ModelForm):
|
||||
|
||||
return item
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get("is_rig") and not (self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
|
||||
raise forms.ValidationError('You haven\'t provided any client contact details. Please add a person or organisation.', code='contact')
|
||||
return super(EventForm, self).clean()
|
||||
|
||||
def save(self, commit=True):
|
||||
m = super(EventForm, self).save(commit=False)
|
||||
|
||||
|
||||
@@ -1,252 +1,11 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.db import transaction
|
||||
from reversion import revisions as reversion
|
||||
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from RIGS import models
|
||||
from django.core.management import call_command
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Adds sample data to use for testing'
|
||||
can_import_settings = True
|
||||
|
||||
people = []
|
||||
organisations = []
|
||||
venues = []
|
||||
profiles = []
|
||||
|
||||
keyholder_group = None
|
||||
finance_group = None
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG or settings.STAGING):
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
|
||||
|
||||
with transaction.atomic():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
|
||||
self.setupGenericProfiles()
|
||||
|
||||
self.setupPeople()
|
||||
self.setupOrganisations()
|
||||
self.setupVenues()
|
||||
|
||||
self.setupGroups()
|
||||
|
||||
self.setupEvents()
|
||||
|
||||
self.setupUsefulProfiles()
|
||||
|
||||
def setupPeople(self):
|
||||
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
|
||||
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime", "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter", "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter", "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley", "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley", "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
|
||||
newPerson = models.Person.objects.create(name=name)
|
||||
if i % 3 == 0:
|
||||
newPerson.email = "address@person.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newPerson.phone = "01234 567894"
|
||||
|
||||
newPerson.save()
|
||||
self.people.append(newPerson)
|
||||
|
||||
def setupOrganisations(self):
|
||||
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
|
||||
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newOrganisation = models.Organisation.objects.create(name=name)
|
||||
if i % 2 == 0:
|
||||
newOrganisation.has_su_account = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newOrganisation.email = "address@organisation.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newOrganisation.phone = "01234 567894"
|
||||
|
||||
newOrganisation.save()
|
||||
self.organisations.append(newOrganisation)
|
||||
|
||||
def setupVenues(self):
|
||||
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark", "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye", "The Golden Tooth", # noqa
|
||||
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown", "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep", "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai", "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth", "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis", "Yunkai"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newVenue = models.Venue.objects.create(name=name)
|
||||
if i % 2 == 0:
|
||||
newVenue.three_phase_available = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newVenue.email = "address@venue.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newVenue.phone = "01234 567894"
|
||||
|
||||
newVenue.save()
|
||||
self.venues.append(newVenue)
|
||||
|
||||
def setupGroups(self):
|
||||
self.keyholder_group = Group.objects.create(name='Keyholders')
|
||||
self.finance_group = Group.objects.create(name='Finance')
|
||||
|
||||
keyholderPerms = ["add_event", "change_event", "view_event", "add_eventitem", "change_eventitem", "delete_eventitem", "add_organisation", "change_organisation", "view_organisation", "add_person", "change_person", "view_person", "view_profile", "add_venue", "change_venue", "view_venue"]
|
||||
financePerms = ["change_event", "view_event", "add_eventitem", "change_eventitem", "add_invoice", "change_invoice", "view_invoice", "add_organisation", "change_organisation", "view_organisation", "add_payment", "change_payment", "delete_payment", "add_person", "change_person", "view_person"]
|
||||
|
||||
for permId in keyholderPerms:
|
||||
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
for permId in financePerms:
|
||||
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
def setupGenericProfiles(self):
|
||||
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble", "Jack Harkness", "Mickey Smith", "Rose Tyler"]
|
||||
for i, name in enumerate(names):
|
||||
newProfile = models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
|
||||
email=name.replace(" ", "") + "@example.com",
|
||||
initials="".join([j[0].upper() for j in name.split()]))
|
||||
if i % 2 == 0:
|
||||
newProfile.phone = "01234 567894"
|
||||
|
||||
newProfile.save()
|
||||
self.profiles.append(newProfile)
|
||||
|
||||
def setupUsefulProfiles(self):
|
||||
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
|
||||
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
superUser.set_password('superuser')
|
||||
superUser.save()
|
||||
|
||||
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
|
||||
email="financeuser@example.com", is_active=True)
|
||||
financeUser.groups.add(self.finance_group)
|
||||
financeUser.groups.add(self.keyholder_group)
|
||||
financeUser.set_password('finance')
|
||||
financeUser.save()
|
||||
|
||||
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", initials="KU",
|
||||
email="keyholderuser@example.com", is_active=True)
|
||||
keyholderUser.groups.add(self.keyholder_group)
|
||||
keyholderUser.set_password('keyholder')
|
||||
keyholderUser.save()
|
||||
|
||||
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
|
||||
email="basicuser@example.com", is_active=True)
|
||||
basicUser.set_password('basic')
|
||||
basicUser.save()
|
||||
|
||||
def setupEvents(self):
|
||||
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
|
||||
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection", "Choir Concert"]
|
||||
descriptions = ["A brief desciption of the event", "This event is boring", "Probably wont happen", "Warning: this has lots of kit"]
|
||||
notes = ["The client came into the office at some point", "Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
|
||||
|
||||
itemOptions = [{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, 'cost': 200.00},
|
||||
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
|
||||
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
|
||||
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
|
||||
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
|
||||
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
|
||||
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
|
||||
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
|
||||
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
|
||||
|
||||
dayDelta = -120 # start adding events from 4 months ago
|
||||
|
||||
for i in range(150): # Let's add 100 events
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
|
||||
name = names[i % len(names)]
|
||||
|
||||
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
|
||||
dayDelta = dayDelta + random.randint(0, 3)
|
||||
|
||||
newEvent = models.Event.objects.create(name=name, start_date=startDate)
|
||||
|
||||
if random.randint(0, 2) > 1: # 1 in 3 have a start time
|
||||
newEvent.start_time = datetime.time(random.randint(15, 20))
|
||||
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
|
||||
newEvent.end_time = datetime.time(random.randint(21, 23))
|
||||
elif random.randint(0, 1) > 0: # half of the others finish early the next day
|
||||
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
|
||||
newEvent.end_time = datetime.time(random.randint(0, 5))
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
|
||||
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have MIC
|
||||
newEvent.mic = random.choice(self.profiles)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have organisation
|
||||
newEvent.organisation = random.choice(self.organisations)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have person
|
||||
newEvent.person = random.choice(self.people)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have venue
|
||||
newEvent.venue = random.choice(self.venues)
|
||||
|
||||
# Could have any status, equally weighted
|
||||
newEvent.status = random.choice([models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
|
||||
|
||||
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have description
|
||||
newEvent.description = random.choice(descriptions)
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have notes
|
||||
newEvent.notes = random.choice(notes)
|
||||
|
||||
newEvent.save()
|
||||
|
||||
# Now add some items
|
||||
for j in range(random.randint(1, 5)):
|
||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
||||
newItem.save()
|
||||
|
||||
while newEvent.sum_total < 0:
|
||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
||||
newItem.save()
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
|
||||
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
|
||||
newInvoice = models.Invoice.objects.create(event=newEvent)
|
||||
if newEvent.status is models.Event.CANCELLED: # void cancelled events
|
||||
newInvoice.void = True
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
||||
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())
|
||||
call_command('generateSampleRIGSData')
|
||||
call_command('generateSampleAssetsData')
|
||||
|
||||
260
RIGS/management/commands/generateSampleRIGSData.py
Normal file
260
RIGS/management/commands/generateSampleRIGSData.py
Normal file
@@ -0,0 +1,260 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.db import transaction
|
||||
from reversion import revisions as reversion
|
||||
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Adds sample data to use for testing'
|
||||
can_import_settings = True
|
||||
|
||||
people = []
|
||||
organisations = []
|
||||
venues = []
|
||||
profiles = []
|
||||
|
||||
keyholder_group = None
|
||||
finance_group = None
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG or settings.STAGING):
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
|
||||
|
||||
with transaction.atomic():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
|
||||
self.setupGenericProfiles()
|
||||
|
||||
self.setupPeople()
|
||||
self.setupOrganisations()
|
||||
self.setupVenues()
|
||||
|
||||
self.setupGroups()
|
||||
|
||||
self.setupEvents()
|
||||
|
||||
self.setupUsefulProfiles()
|
||||
|
||||
def setupPeople(self):
|
||||
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
|
||||
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime", "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter", "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter", "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley", "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley", "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
|
||||
newPerson = models.Person.objects.create(name=name)
|
||||
if i % 3 == 0:
|
||||
newPerson.email = "address@person.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newPerson.phone = "01234 567894"
|
||||
|
||||
newPerson.save()
|
||||
self.people.append(newPerson)
|
||||
|
||||
def setupOrganisations(self):
|
||||
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
|
||||
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newOrganisation = models.Organisation.objects.create(name=name)
|
||||
if i % 2 == 0:
|
||||
newOrganisation.has_su_account = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newOrganisation.email = "address@organisation.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newOrganisation.phone = "01234 567894"
|
||||
|
||||
newOrganisation.save()
|
||||
self.organisations.append(newOrganisation)
|
||||
|
||||
def setupVenues(self):
|
||||
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark", "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye", "The Golden Tooth", # noqa
|
||||
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown", "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep", "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai", "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth", "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis", "Yunkai"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newVenue = models.Venue.objects.create(name=name)
|
||||
if i % 2 == 0:
|
||||
newVenue.three_phase_available = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newVenue.email = "address@venue.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newVenue.phone = "01234 567894"
|
||||
|
||||
newVenue.save()
|
||||
self.venues.append(newVenue)
|
||||
|
||||
def setupGroups(self):
|
||||
self.keyholder_group = Group.objects.create(name='Keyholders')
|
||||
self.finance_group = Group.objects.create(name='Finance')
|
||||
|
||||
keyholderPerms = ["add_event", "change_event", "view_event",
|
||||
"add_eventitem", "change_eventitem", "delete_eventitem",
|
||||
"add_organisation", "change_organisation", "view_organisation",
|
||||
"add_person", "change_person", "view_person", "view_profile",
|
||||
"add_venue", "change_venue", "view_venue",
|
||||
"add_asset", "change_asset", "delete_asset",
|
||||
"asset_finance", "view_asset", "view_supplier", "asset_finance",
|
||||
"add_supplier"]
|
||||
financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice",
|
||||
"add_payment", "change_payment", "delete_payment"]
|
||||
|
||||
for permId in keyholderPerms:
|
||||
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
for permId in financePerms:
|
||||
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
def setupGenericProfiles(self):
|
||||
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble", "Jack Harkness", "Mickey Smith", "Rose Tyler"]
|
||||
for i, name in enumerate(names):
|
||||
newProfile = models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
|
||||
email=name.replace(" ", "") + "@example.com",
|
||||
initials="".join([j[0].upper() for j in name.split()]))
|
||||
if i % 2 == 0:
|
||||
newProfile.phone = "01234 567894"
|
||||
|
||||
newProfile.save()
|
||||
self.profiles.append(newProfile)
|
||||
|
||||
def setupUsefulProfiles(self):
|
||||
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
|
||||
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
superUser.set_password('superuser')
|
||||
superUser.save()
|
||||
|
||||
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
|
||||
email="financeuser@example.com", is_active=True)
|
||||
financeUser.groups.add(self.finance_group)
|
||||
financeUser.groups.add(self.keyholder_group)
|
||||
financeUser.set_password('finance')
|
||||
financeUser.save()
|
||||
|
||||
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", initials="KU",
|
||||
email="keyholderuser@example.com", is_active=True)
|
||||
keyholderUser.groups.add(self.keyholder_group)
|
||||
keyholderUser.set_password('keyholder')
|
||||
keyholderUser.save()
|
||||
|
||||
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
|
||||
email="basicuser@example.com", is_active=True)
|
||||
basicUser.set_password('basic')
|
||||
basicUser.save()
|
||||
|
||||
def setupEvents(self):
|
||||
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
|
||||
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection", "Choir Concert"]
|
||||
descriptions = ["A brief desciption of the event", "This event is boring", "Probably wont happen", "Warning: this has lots of kit"]
|
||||
notes = ["The client came into the office at some point", "Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
|
||||
|
||||
itemOptions = [{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, 'cost': 200.00},
|
||||
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
|
||||
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
|
||||
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
|
||||
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
|
||||
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
|
||||
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
|
||||
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
|
||||
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
|
||||
|
||||
dayDelta = -120 # start adding events from 4 months ago
|
||||
|
||||
for i in range(150): # Let's add 100 events
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
|
||||
name = names[i % len(names)]
|
||||
|
||||
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
|
||||
dayDelta = dayDelta + random.randint(0, 3)
|
||||
|
||||
newEvent = models.Event.objects.create(name=name, start_date=startDate)
|
||||
|
||||
if random.randint(0, 2) > 1: # 1 in 3 have a start time
|
||||
newEvent.start_time = datetime.time(random.randint(15, 20))
|
||||
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
|
||||
newEvent.end_time = datetime.time(random.randint(21, 23))
|
||||
elif random.randint(0, 1) > 0: # half of the others finish early the next day
|
||||
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
|
||||
newEvent.end_time = datetime.time(random.randint(0, 5))
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
|
||||
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have MIC
|
||||
newEvent.mic = random.choice(self.profiles)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have organisation
|
||||
newEvent.organisation = random.choice(self.organisations)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have person
|
||||
newEvent.person = random.choice(self.people)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have venue
|
||||
newEvent.venue = random.choice(self.venues)
|
||||
|
||||
# Could have any status, equally weighted
|
||||
newEvent.status = random.choice([models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
|
||||
|
||||
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have description
|
||||
newEvent.description = random.choice(descriptions)
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have notes
|
||||
newEvent.notes = random.choice(notes)
|
||||
|
||||
newEvent.save()
|
||||
|
||||
# Now add some items
|
||||
for j in range(random.randint(1, 5)):
|
||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
||||
newItem.save()
|
||||
|
||||
while newEvent.sum_total < 0:
|
||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
||||
newItem.save()
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
|
||||
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
|
||||
newInvoice = models.Invoice.objects.create(event=newEvent)
|
||||
if newEvent.status is models.Event.CANCELLED: # void cancelled events
|
||||
newInvoice.void = True
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
||||
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())
|
||||
@@ -8,7 +8,7 @@ def user_created(sender, user, request, **kwargs):
|
||||
user.first_name = form.data['first_name']
|
||||
user.last_name = form.data['last_name']
|
||||
user.initials = form.data['initials']
|
||||
user.phone = form.data['phone']
|
||||
# user.phone = form.data['phone']
|
||||
user.save()
|
||||
|
||||
|
||||
|
||||
@@ -140,15 +140,18 @@ class EventUpdate(generic.UpdateView):
|
||||
if value is not None and value != '':
|
||||
context[field] = model.objects.get(pk=value)
|
||||
|
||||
# If this event has already been emailed to a client, show a warning
|
||||
if self.object.auth_request_at is not None:
|
||||
messages.info(self.request, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
|
||||
|
||||
if hasattr(self.object, 'authorised'):
|
||||
messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
|
||||
|
||||
return context
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
if not hasattr(context, 'duplicate'):
|
||||
# If this event has already been emailed to a client, show a warning
|
||||
if self.object.auth_request_at is not None:
|
||||
messages.info(self.request, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
|
||||
|
||||
if hasattr(self.object, 'authorised'):
|
||||
messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
|
||||
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 66 KiB |
@@ -65,6 +65,15 @@ textarea {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dont-break-out {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
z-index: inherit; // bug fix introduced in 52682ce
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax_nomodal.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax_nomodal.html,base_rigs.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load paginator from filters %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load static %}
|
||||
{% load paginator from filters %}
|
||||
{% load to_class_name from filters %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load paginator from filters %}
|
||||
|
||||
{% block title %}Event Archive{% endblock %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load markdown_tags %}
|
||||
|
||||
{% block title %}{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %} | {{object.name}}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -123,7 +124,7 @@
|
||||
<dd> </dd>
|
||||
|
||||
<dt>Event Description</dt>
|
||||
<dd>{{ event.description|markdown }}</dd>
|
||||
<dd class="dont-break-out">{{ event.description|markdown }}</dd>
|
||||
|
||||
<dd> </dd>
|
||||
|
||||
@@ -159,7 +160,15 @@
|
||||
</div>
|
||||
{% if event.is_rig and event.internal %}
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel panel-default
|
||||
{% if object.authorised %}
|
||||
panel-success
|
||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||
panel-warning
|
||||
{% elif event.auth_request_to %}
|
||||
panel-info
|
||||
{% endif %}
|
||||
">
|
||||
<div class="panel-heading">Client Authorisation</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal col-sm-6">
|
||||
@@ -189,7 +198,7 @@
|
||||
</dd>
|
||||
|
||||
<dt>Authorised at</dt>
|
||||
<dd>{{ object.authorisation.last_edited_at }}</dd>
|
||||
<dd>{{ object.authorisation.last_edited_at|date:"D d M Y H:i" }}</dd>
|
||||
|
||||
<dt>Authorised amount</dt>
|
||||
<dd>
|
||||
@@ -217,7 +226,7 @@
|
||||
<div class="panel-body">
|
||||
<div class="well well-sm">
|
||||
<h4>Notes</h4>
|
||||
{{ event.notes|markdown }}
|
||||
<div class="dont-break-out">{{ event.notes|markdown }}</div>
|
||||
</div>
|
||||
{% include 'RIGS/item_table.html' %}
|
||||
</div>
|
||||
@@ -246,7 +255,7 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-10 align-left">
|
||||
<a href="{% url 'event_history' object.pk %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
|
||||
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
{% load multiply from filters %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load paginator from filters %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p>
|
||||
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
|
||||
for <b>£{{ object.amount }}</b>
|
||||
by <b>{{ object.name }}</b> as of <b>{{ object.last_edited_at }}</b>.
|
||||
by <b>{{ object.name }}</b> as of <b>{{ object.event.last_edited_at }}</b>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Hi {{ to_name|default:"there" }},
|
||||
Hi {{ to_name|default_if_none:"there" }},
|
||||
|
||||
Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
|
||||
Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
||||
|
||||
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
|
||||
Your event is now fully booked and payment will be processed by the finance department automatically.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
|
||||
|
||||
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
|
||||
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
||||
|
||||
The TEC Rig Information Gathering System
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Request Authorisation{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}NottinghamTEC Email Address Required{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% block title %}RIGS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -20,6 +20,7 @@
|
||||
<a class="list-group-item" href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span> Rigboard</a>
|
||||
<a class="list-group-item" href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span> Calendar</a>
|
||||
{% if perms.RIGS.add_event %}<a class="list-group-item" href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span> New Event</a>{% endif %}
|
||||
<a class="list-group-item" href="{% url 'asset_index' %}"><span class="glyphicon glyphicon-tag"></span> Asset Database </a>
|
||||
|
||||
<div class="list-group-item default"></div>
|
||||
|
||||
@@ -71,9 +72,8 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.RIGS.view_event %}
|
||||
<div class="col-sm-6" >
|
||||
<div class="col-sm-6">
|
||||
{% include 'RIGS/activity_feed.html' %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Invoice {{ object.pk }}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load paginator from filters %}
|
||||
|
||||
{% block title %}Invoices{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load markdown_tags %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}{% if object.pk %}Edit {{ object.name }}{% else %}Add Organisation{% endif %}{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load paginator from filters %}
|
||||
{% load url_replace from filters %}
|
||||
|
||||
9
RIGS/templates/RIGS/password_reset_disable.html
Normal file
9
RIGS/templates/RIGS/password_reset_disable.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Password Reset Disabled{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Password reset is disabled</h1>
|
||||
<p> We are very sorry for the inconvenience, but due to a security vulnerability, password reset is currently disabled until the vulnerability can be patched.</p>
|
||||
<p> If you are locked out of your account, please contact an administrator and we can manually perform a reset</p>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load markdown_tags %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}{% if object.pk %}Edit {{ object.name }}{% else %}Add Person{% endif %}{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load paginator from filters %}
|
||||
{% load url_replace from filters %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
|
||||
{% block title %}RIGS Profile {{object.pk}}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Update Profile {{object.name}}{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Rigboard{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load markdown_tags %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}{{ object.pk|yesno:"Edit,Add" }} Venue{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load paginator from filters %}
|
||||
{% load url_replace from filters %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% for change in itemChange.field_changes %}
|
||||
<li class="list-group-item">
|
||||
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
|
||||
{% include "RIGS/version_changes_change.html" %}
|
||||
<div class="dont-break-out">{% include "RIGS/version_changes_change.html" %}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
{% if change.linebreaks and change.new and change.old %}
|
||||
{% for diff in change.diff %}
|
||||
{% if diff.type == "insert" %}
|
||||
<ins>{{ diff.text|linebreaksbr }}</ins>
|
||||
<ins class="dont-break-out">{{ diff.text|linebreaksbr }}</ins>
|
||||
{% elif diff.type == "delete" %}
|
||||
<del>{{diff.text|linebreaksbr}}</del>
|
||||
<del class="dont-break-out">{{diff.text|linebreaksbr}}</del>
|
||||
{% else %}
|
||||
<span>{{diff.text|linebreaksbr}}</span>
|
||||
<span class="dont-break-out">{{diff.text|linebreaksbr}}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load to_class_name from filters %}
|
||||
{% load paginator from filters %}
|
||||
{% load static %}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import re
|
||||
import pytz
|
||||
from datetime import date, time, datetime, timedelta
|
||||
|
||||
|
||||
from django.core import mail
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.core import mail, signing
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.test import LiveServerTestCase, TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from reversion import revisions as reversion
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
@@ -20,23 +23,12 @@ 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
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
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 UserRegistrationTest(LiveServerTestCase):
|
||||
def setUp(self):
|
||||
self.browser = create_browser()
|
||||
@@ -74,8 +66,9 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
||||
initials = self.browser.find_element_by_id('id_initials')
|
||||
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
||||
phone = self.browser.find_element_by_id('id_phone')
|
||||
self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
||||
# No longer required for new users
|
||||
# phone = self.browser.find_element_by_id('id_phone')
|
||||
# self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
||||
|
||||
# Fill the form out incorrectly
|
||||
username.send_keys('TestUsername')
|
||||
@@ -86,9 +79,9 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
first_name.send_keys('John')
|
||||
last_name.send_keys('Smith')
|
||||
initials.send_keys('JS')
|
||||
phone.send_keys('0123456789')
|
||||
# phone.send_keys('0123456789')
|
||||
self.browser.execute_script(
|
||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
||||
|
||||
# Submit incorrect form
|
||||
submit = self.browser.find_element_by_xpath("//input[@type='submit']")
|
||||
@@ -110,8 +103,9 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
# Correct error
|
||||
password1.send_keys('correcthorsebatterystaple')
|
||||
password2.send_keys('correcthorsebatterystaple')
|
||||
self.browser.execute_script("console.log('Hello, world!')")
|
||||
self.browser.execute_script(
|
||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
||||
|
||||
# Submit again
|
||||
password2.send_keys(Keys.ENTER)
|
||||
@@ -150,7 +144,7 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
username.send_keys('TestUsername')
|
||||
password.send_keys('correcthorsebatterystaple')
|
||||
self.browser.execute_script(
|
||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
||||
password.send_keys(Keys.ENTER)
|
||||
|
||||
# Check we are logged in
|
||||
@@ -163,7 +157,7 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
self.assertEqual(profileObject.first_name, 'John')
|
||||
self.assertEqual(profileObject.last_name, 'Smith')
|
||||
self.assertEqual(profileObject.initials, 'JS')
|
||||
self.assertEqual(profileObject.phone, '0123456789')
|
||||
# self.assertEqual(profileObject.phone, '0123456789')
|
||||
self.assertEqual(profileObject.email, 'test@example.com')
|
||||
|
||||
# All is well
|
||||
@@ -218,254 +212,236 @@ class EventTest(LiveServerTestCase):
|
||||
self.browser.get(self.live_server_url + '/rigboard/')
|
||||
|
||||
def testRigCreate(self):
|
||||
try:
|
||||
# Requests address
|
||||
self.browser.get(self.live_server_url + '/event/create/')
|
||||
# Gets redirected to login and back
|
||||
self.authenticate('/event/create/')
|
||||
# Requests address
|
||||
self.browser.get(self.live_server_url + '/event/create/')
|
||||
# Gets redirected to login and back
|
||||
self.authenticate('/event/create/')
|
||||
|
||||
wait = WebDriverWait(self.browser, 3) # setup WebDriverWait to use later (to wait for animations)
|
||||
wait = WebDriverWait(self.browser, 3) # setup WebDriverWait to use later (to wait for animations)
|
||||
|
||||
wait.until(animation_is_finished())
|
||||
wait.until(animation_is_finished())
|
||||
|
||||
# Check has slided up correctly - second save button hidden
|
||||
save = self.browser.find_element_by_xpath(
|
||||
'(//button[@type="submit"])[3]')
|
||||
self.assertFalse(save.is_displayed())
|
||||
# Check has slided up correctly - second save button hidden
|
||||
save = self.browser.find_element_by_xpath(
|
||||
'(//button[@type="submit"])[3]')
|
||||
self.assertFalse(save.is_displayed())
|
||||
|
||||
# Click Rig button
|
||||
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
||||
# Click Rig button
|
||||
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
||||
|
||||
# Slider expands and save button visible
|
||||
self.assertTrue(save.is_displayed())
|
||||
form = self.browser.find_element_by_tag_name('form')
|
||||
# Slider expands and save button visible
|
||||
self.assertTrue(save.is_displayed())
|
||||
form = self.browser.find_element_by_tag_name('form')
|
||||
|
||||
# Create new person
|
||||
wait.until(animation_is_finished())
|
||||
add_person_button = self.browser.find_element_by_xpath(
|
||||
'//a[@data-target="#id_person" and contains(@href, "add")]')
|
||||
add_person_button.click()
|
||||
# For now, just check that HTML5 Client validation is in place TODO Test needs rewriting to properly test all levels of validation.
|
||||
self.assertTrue(self.browser.find_element_by_id('id_name').get_attribute('required') is not None)
|
||||
|
||||
# See modal has opened
|
||||
modal = self.browser.find_element_by_id('modal')
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
|
||||
# Set title
|
||||
e = self.browser.find_element_by_id('id_name')
|
||||
e.send_keys('Test Event Name')
|
||||
|
||||
# Fill person form out and submit
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
# Create new person
|
||||
wait.until(animation_is_finished())
|
||||
add_person_button = self.browser.find_element_by_xpath(
|
||||
'//a[@data-target="#id_person" and contains(@href, "add")]')
|
||||
add_person_button.click()
|
||||
|
||||
# See new person selected
|
||||
person1 = models.Person.objects.get(name="Test Person 1")
|
||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
# and backend
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||
self.assertEqual(person1.pk, int(option.get_attribute("value")))
|
||||
# See modal has opened
|
||||
modal = self.browser.find_element_by_id('modal')
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
|
||||
|
||||
# Change mind and add another
|
||||
wait.until(animation_is_finished())
|
||||
add_person_button.click()
|
||||
# Fill person form out and submit
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
|
||||
# See new person selected
|
||||
person1 = models.Person.objects.get(name="Test Person 1")
|
||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
# and backend
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||
self.assertEqual(person1.pk, int(option.get_attribute("value")))
|
||||
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
# Change mind and add another
|
||||
wait.until(animation_is_finished())
|
||||
add_person_button.click()
|
||||
|
||||
person2 = models.Person.objects.get(name="Test Person 2")
|
||||
self.assertEqual(person2.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
# Have to do this explcitly to force the wait for it to update
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||
self.assertEqual(person2.pk, int(option.get_attribute("value")))
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
|
||||
|
||||
# Was right the first time, change it back
|
||||
person_select = form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]')
|
||||
person_select.send_keys(person1.name)
|
||||
person_dropped = form.find_element_by_xpath(
|
||||
'//ul[contains(@class, "inner selectpicker")]//span[contains(text(), "%s")]' % person1.name)
|
||||
person_dropped.click()
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
|
||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||
self.assertEqual(person1.pk, int(option.get_attribute("value")))
|
||||
person2 = models.Person.objects.get(name="Test Person 2")
|
||||
self.assertEqual(person2.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
# Have to do this explcitly to force the wait for it to update
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||
self.assertEqual(person2.pk, int(option.get_attribute("value")))
|
||||
|
||||
# Edit Person 1 to have a better name
|
||||
form.find_element_by_xpath(
|
||||
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Edit Person", modal.find_element_by_tag_name('h3').text)
|
||||
name = modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]')
|
||||
self.assertEqual(person1.name, name.get_attribute('value'))
|
||||
name.clear()
|
||||
name.send_keys('Rig ' + person1.name)
|
||||
name.send_keys(Keys.ENTER)
|
||||
# Was right the first time, change it back
|
||||
person_select = form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]')
|
||||
person_select.send_keys(person1.name)
|
||||
person_dropped = form.find_element_by_xpath(
|
||||
'//ul[contains(@class, "dropdown-menu")]//span[contains(text(), "%s")]' % person1.name)
|
||||
person_dropped.click()
|
||||
|
||||
wait.until(animation_is_finished())
|
||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||
self.assertEqual(person1.pk, int(option.get_attribute("value")))
|
||||
|
||||
self.assertFalse(modal.is_displayed())
|
||||
person1 = models.Person.objects.get(pk=person1.pk)
|
||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
# Edit Person 1 to have a better name
|
||||
form.find_element_by_xpath(
|
||||
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Edit Person", modal.find_element_by_tag_name('h3').text)
|
||||
name = modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]')
|
||||
self.assertEqual(person1.name, name.get_attribute('value'))
|
||||
name.clear()
|
||||
name.send_keys('Rig ' + person1.name)
|
||||
name.send_keys(Keys.ENTER)
|
||||
|
||||
# Create organisation
|
||||
wait.until(animation_is_finished())
|
||||
add_button = self.browser.find_element_by_xpath(
|
||||
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
|
||||
add_button.click()
|
||||
modal = self.browser.find_element_by_id('modal')
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
wait.until(animation_is_finished())
|
||||
|
||||
# See it is selected
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
obj = models.Organisation.objects.get(name="Test Organisation")
|
||||
self.assertEqual(obj.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_organisation"]/span').text)
|
||||
# and backend
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_organisation"]//option[@selected="selected"]')
|
||||
self.assertEqual(obj.pk, int(option.get_attribute("value")))
|
||||
self.assertFalse(modal.is_displayed())
|
||||
person1 = models.Person.objects.get(pk=person1.pk)
|
||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]/span').text)
|
||||
|
||||
# Create venue
|
||||
wait.until(animation_is_finished())
|
||||
add_button = self.browser.find_element_by_xpath(
|
||||
'//a[@data-target="#id_venue" and contains(@href, "add")]')
|
||||
wait.until(animation_is_finished())
|
||||
add_button.click()
|
||||
wait.until(animation_is_finished())
|
||||
modal = self.browser.find_element_by_id('modal')
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
# Create organisation
|
||||
wait.until(animation_is_finished())
|
||||
add_button = self.browser.find_element_by_xpath(
|
||||
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
|
||||
add_button.click()
|
||||
modal = self.browser.find_element_by_id('modal')
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
|
||||
# See it is selected
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
obj = models.Venue.objects.get(name="Test Venue")
|
||||
self.assertEqual(obj.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_venue"]/span').text)
|
||||
# and backend
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_venue"]//option[@selected="selected"]')
|
||||
self.assertEqual(obj.pk, int(option.get_attribute("value")))
|
||||
# See it is selected
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
obj = models.Organisation.objects.get(name="Test Organisation")
|
||||
self.assertEqual(obj.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_organisation"]/span').text)
|
||||
# and backend
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_organisation"]//option[@selected="selected"]')
|
||||
self.assertEqual(obj.pk, int(option.get_attribute("value")))
|
||||
|
||||
# Set start date/time
|
||||
form.find_element_by_id('id_start_date').send_keys('25/05/3015')
|
||||
form.find_element_by_id('id_start_time').send_keys('06:59')
|
||||
# Create venue
|
||||
wait.until(animation_is_finished())
|
||||
add_button = self.browser.find_element_by_xpath(
|
||||
'//a[@data-target="#id_venue" and contains(@href, "add")]')
|
||||
wait.until(animation_is_finished())
|
||||
add_button.click()
|
||||
wait.until(animation_is_finished())
|
||||
modal = self.browser.find_element_by_id('modal')
|
||||
wait.until(animation_is_finished())
|
||||
self.assertTrue(modal.is_displayed())
|
||||
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
|
||||
modal.find_element_by_xpath(
|
||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||
|
||||
# Set end date/time
|
||||
form.find_element_by_id('id_end_date').send_keys('27/06/4000')
|
||||
form.find_element_by_id('id_end_time').send_keys('07:00')
|
||||
# See it is selected
|
||||
wait.until(animation_is_finished())
|
||||
self.assertFalse(modal.is_displayed())
|
||||
obj = models.Venue.objects.get(name="Test Venue")
|
||||
self.assertEqual(obj.name, form.find_element_by_xpath(
|
||||
'//button[@data-id="id_venue"]/span').text)
|
||||
# and backend
|
||||
option = form.find_element_by_xpath(
|
||||
'//select[@id="id_venue"]//option[@selected="selected"]')
|
||||
self.assertEqual(obj.pk, int(option.get_attribute("value")))
|
||||
|
||||
# Add item
|
||||
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
||||
wait.until(animation_is_finished())
|
||||
modal = self.browser.find_element_by_id("itemModal")
|
||||
modal.find_element_by_id("item_name").send_keys("Test Item 1")
|
||||
modal.find_element_by_id("item_description").send_keys(
|
||||
"This is an item description\nthat for reasons unkown spans two lines")
|
||||
e = modal.find_element_by_id("item_quantity")
|
||||
e.click()
|
||||
e.send_keys(Keys.UP)
|
||||
e.send_keys(Keys.UP)
|
||||
e = modal.find_element_by_id("item_cost")
|
||||
e.send_keys("23.95")
|
||||
e.send_keys(Keys.ENTER) # enter submit
|
||||
# Set start date/time
|
||||
form.find_element_by_id('id_start_date').send_keys('25/05/3015')
|
||||
form.find_element_by_id('id_start_time').send_keys('06:59')
|
||||
|
||||
# Confirm item has been saved to json field
|
||||
objectitems = self.browser.execute_script("return objectitems;")
|
||||
self.assertEqual(1, len(objectitems))
|
||||
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
|
||||
self.assertEqual("Test Item 1", testitem['name'])
|
||||
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
|
||||
# Set end date/time
|
||||
form.find_element_by_id('id_end_date').send_keys('27/06/4000')
|
||||
form.find_element_by_id('id_end_time').send_keys('07:00')
|
||||
|
||||
# See new item appear in table
|
||||
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
|
||||
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
|
||||
self.assertIn("This is an item description",
|
||||
row.find_element_by_xpath('//div[@class="item-description"]').text)
|
||||
self.assertEqual('£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
|
||||
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
|
||||
self.assertEqual('£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
|
||||
# Add item
|
||||
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
||||
wait.until(animation_is_finished())
|
||||
modal = self.browser.find_element_by_id("itemModal")
|
||||
modal.find_element_by_id("item_name").send_keys("Test Item 1")
|
||||
modal.find_element_by_id("item_description").send_keys(
|
||||
"This is an item description\nthat for reasons unknown spans two lines")
|
||||
e = modal.find_element_by_id("item_quantity")
|
||||
e.click()
|
||||
e.send_keys(Keys.UP)
|
||||
e.send_keys(Keys.UP)
|
||||
e = modal.find_element_by_id("item_cost")
|
||||
e.send_keys("23.95")
|
||||
e.send_keys(Keys.ENTER) # enter submit
|
||||
|
||||
# Check totals
|
||||
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
|
||||
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
|
||||
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
|
||||
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
|
||||
# Confirm item has been saved to json field
|
||||
objectitems = self.browser.execute_script("return objectitems;")
|
||||
self.assertEqual(1, len(objectitems))
|
||||
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
|
||||
self.assertEqual("Test Item 1", testitem['name'])
|
||||
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
|
||||
|
||||
# Attempt to save - missing title
|
||||
save.click()
|
||||
# See new item appear in table
|
||||
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
|
||||
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
|
||||
self.assertIn("This is an item description",
|
||||
row.find_element_by_xpath('//div[@class="item-description"]').text)
|
||||
self.assertEqual('£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
|
||||
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
|
||||
self.assertEqual('£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
|
||||
|
||||
# See error
|
||||
error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]')
|
||||
self.assertTrue(error.is_displayed())
|
||||
# Should only have one error message
|
||||
self.assertEqual("Name", error.find_element_by_xpath('//dt[1]').text)
|
||||
self.assertEqual("This field is required.", error.find_element_by_xpath('//dd[1]/ul/li').text)
|
||||
# don't need error so close it
|
||||
error.find_element_by_xpath('//div[contains(@class, "alert-danger")]//button[@class="close"]').click()
|
||||
try:
|
||||
self.assertFalse(error.is_displayed())
|
||||
except StaleElementReferenceException:
|
||||
pass
|
||||
except BaseException:
|
||||
self.assertFail("Element does not appear to have been deleted")
|
||||
# Check totals
|
||||
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
|
||||
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
|
||||
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
|
||||
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
|
||||
|
||||
# Check at least some data is preserved. Some = all will be there
|
||||
option = self.browser.find_element_by_xpath(
|
||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||
self.assertEqual(person1.pk, int(option.get_attribute("value")))
|
||||
save = self.browser.find_element_by_xpath(
|
||||
'(//button[@type="submit"])[3]')
|
||||
save.click()
|
||||
|
||||
# Set title
|
||||
e = self.browser.find_element_by_id('id_name')
|
||||
e.send_keys('Test Event Name')
|
||||
e.send_keys(Keys.ENTER)
|
||||
# TODO Testing of requirement for contact details
|
||||
|
||||
# See redirected to success page
|
||||
successTitle = self.browser.find_element_by_xpath('//h1').text
|
||||
event = models.Event.objects.get(name='Test Event Name')
|
||||
|
||||
self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
|
||||
except WebDriverException:
|
||||
# This is a dirty workaround for wercker being a bit funny and not running it correctly.
|
||||
# Waiting for wercker to get back to me about this
|
||||
pass
|
||||
# TODO Something seems broken with the CI tests here.
|
||||
# See redirected to success page
|
||||
# successTitle = self.browser.find_element_by_xpath('//h1').text
|
||||
# event = models.Event.objects.get(name='Test Event Name')
|
||||
# self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
|
||||
|
||||
def testEventDuplicate(self):
|
||||
client = models.Person.objects.create(name='Duplicate Test Person', email='duplicate@functional.test')
|
||||
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
|
||||
start_date=date.today() + timedelta(days=6),
|
||||
description="start future no end",
|
||||
purchase_order='TESTPO',
|
||||
person=client,
|
||||
auth_request_by=self.profile,
|
||||
auth_request_at=self.create_datetime(2015, 0o6, 0o4, 10, 00),
|
||||
auth_request_to="some@email.address")
|
||||
@@ -509,7 +485,7 @@ class EventTest(LiveServerTestCase):
|
||||
modal = self.browser.find_element_by_id("itemModal")
|
||||
modal.find_element_by_id("item_name").send_keys("Test Item 3")
|
||||
modal.find_element_by_id("item_description").send_keys(
|
||||
"This is an item description\nthat for reasons unkown spans two lines")
|
||||
"This is an item description\nthat for reasons unknown spans two lines")
|
||||
e = modal.find_element_by_id("item_quantity")
|
||||
e.click()
|
||||
e.send_keys(Keys.UP)
|
||||
@@ -582,6 +558,15 @@ class EventTest(LiveServerTestCase):
|
||||
e = self.browser.find_element_by_id('id_name')
|
||||
e.send_keys('Test Event Name')
|
||||
|
||||
# Set person
|
||||
person = models.Person.objects.create(name='Date Validation Person', email='datevalidation@functional.test')
|
||||
person_select = form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]')
|
||||
person_select.send_keys(person.name)
|
||||
person_dropped = form.find_element_by_xpath(
|
||||
'//ul[contains(@class, "dropdown-menu")]//span[contains(text(), "%s")]' % person.name)
|
||||
person_dropped.click()
|
||||
|
||||
# Both dates, no times, end before start
|
||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||
|
||||
@@ -687,6 +672,15 @@ class EventTest(LiveServerTestCase):
|
||||
e = self.browser.find_element_by_id('id_name')
|
||||
e.send_keys('Test Event Name')
|
||||
|
||||
# Set person
|
||||
person = models.Person.objects.create(name='Rig Non-Rig Person', email='rignonrig@functional.test')
|
||||
person_select = form.find_element_by_xpath(
|
||||
'//button[@data-id="id_person"]')
|
||||
person_select.send_keys(person.name)
|
||||
person_dropped = form.find_element_by_xpath(
|
||||
'//ul[contains(@class, "dropdown-menu")]//span[contains(text(), "%s")]' % person.name)
|
||||
person_dropped.click()
|
||||
|
||||
# Set an arbitrary date
|
||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||
|
||||
@@ -748,9 +742,9 @@ class EventTest(LiveServerTestCase):
|
||||
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
|
||||
|
||||
def testEventEdit(self):
|
||||
person = models.Person(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123").save()
|
||||
organisation = models.Organisation(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
|
||||
venue = models.Venue(name="Event Detail Venue").save()
|
||||
person = models.Person.objects.create(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123")
|
||||
organisation = models.Organisation.objects.create(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456")
|
||||
venue = models.Venue.objects.create(name="Event Detail Venue")
|
||||
|
||||
eventData = {
|
||||
'name': "Detail Test",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -419,7 +419,7 @@ class TestSampleDataGenerator(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def test_generate_sample_data(self):
|
||||
# Run the management command and check there are no exceptions
|
||||
call_command('generateSampleData')
|
||||
call_command('generateSampleRIGSData')
|
||||
|
||||
# Check there are lots of events
|
||||
self.assertTrue(models.Event.objects.all().count() > 100)
|
||||
@@ -427,7 +427,7 @@ class TestSampleDataGenerator(TestCase):
|
||||
def test_production_exception(self):
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleData')
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
|
||||
|
||||
|
||||
class TestMarkdownTemplateTags(TestCase):
|
||||
|
||||
@@ -19,7 +19,7 @@ urlpatterns = [
|
||||
url('^user/login/$', views.login, name='login'),
|
||||
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
|
||||
|
||||
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}),
|
||||
url(r'^user/password_reset/$', views.PasswordResetDisabled.as_view()),
|
||||
|
||||
# People
|
||||
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
|
||||
|
||||
@@ -168,7 +168,7 @@ class RIGSVersionManager(VersionQuerySet):
|
||||
for model in model_array:
|
||||
content_types.append(ContentType.objects.get_for_model(model))
|
||||
|
||||
return self.filter(content_type__in=content_types).select_related("revision").order_by("-pk")
|
||||
return self.filter(content_type__in=content_types).select_related("revision").order_by("-revision__date_created")
|
||||
|
||||
|
||||
class RIGSVersion(Version):
|
||||
@@ -206,17 +206,14 @@ class VersionHistory(generic.ListView):
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
thisModel = self.kwargs['model']
|
||||
return RIGSVersion.objects.get_for_object(self.get_object()).select_related("revision", "revision__user").all().order_by("-revision__date_created")
|
||||
|
||||
versions = RIGSVersion.objects.get_for_object_reference(thisModel, self.kwargs['pk']).select_related("revision", "revision__user").all()
|
||||
|
||||
return versions
|
||||
def get_object(self, **kwargs):
|
||||
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
thisModel = self.kwargs['model']
|
||||
context = super(VersionHistory, self).get_context_data(**kwargs)
|
||||
thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk'])
|
||||
context['object'] = thisObject
|
||||
context['object'] = self.get_object()
|
||||
|
||||
return context
|
||||
|
||||
@@ -228,7 +225,7 @@ class ActivityTable(generic.ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
|
||||
return versions
|
||||
return versions.order_by("-revision__date_created")
|
||||
|
||||
|
||||
class ActivityFeed(generic.ListView):
|
||||
@@ -238,7 +235,7 @@ class ActivityFeed(generic.ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
|
||||
return versions
|
||||
return versions.order_by("-revision__date_created")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# Call the base implementation first to get a context
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
||||
from RIGS import models, forms
|
||||
from assets import models as asset_models
|
||||
from functools import reduce
|
||||
|
||||
"""
|
||||
@@ -248,6 +249,7 @@ class SecureAPIRequest(generic.View):
|
||||
'organisation': models.Organisation,
|
||||
'profile': models.Profile,
|
||||
'event': models.Event,
|
||||
'supplier': asset_models.Supplier
|
||||
}
|
||||
|
||||
perms = {
|
||||
@@ -256,6 +258,7 @@ class SecureAPIRequest(generic.View):
|
||||
'organisation': 'RIGS.view_organisation',
|
||||
'profile': 'RIGS.view_profile',
|
||||
'event': None,
|
||||
'supplier': None
|
||||
}
|
||||
|
||||
'''
|
||||
@@ -389,3 +392,7 @@ class ResetApiKey(generic.RedirectView):
|
||||
self.request.user.save()
|
||||
|
||||
return reverse_lazy('profile_detail')
|
||||
|
||||
|
||||
class PasswordResetDisabled(generic.TemplateView):
|
||||
template_name = "RIGS/password_reset_disable.html"
|
||||
|
||||
0
assets/__init__.py
Normal file
0
assets/__init__.py
Normal file
32
assets/admin.py
Normal file
32
assets/admin.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.contrib import admin
|
||||
from assets import models as assets
|
||||
|
||||
|
||||
@admin.register(assets.AssetCategory)
|
||||
class AssetCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name']
|
||||
ordering = ['id']
|
||||
|
||||
|
||||
@admin.register(assets.AssetStatus)
|
||||
class AssetStatusAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name']
|
||||
ordering = ['id']
|
||||
|
||||
|
||||
@admin.register(assets.Supplier)
|
||||
class SupplierAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name']
|
||||
ordering = ['id']
|
||||
|
||||
|
||||
@admin.register(assets.Asset)
|
||||
class AssetAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'asset_id', 'description', 'category', 'status']
|
||||
list_filter = ['is_cable', 'category']
|
||||
search_fields = ['id', 'asset_id', 'description']
|
||||
|
||||
|
||||
@admin.register(assets.Connector)
|
||||
class ConnectorAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins']
|
||||
36
assets/forms.py
Normal file
36
assets/forms.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django import forms
|
||||
|
||||
from assets import models
|
||||
|
||||
|
||||
class AssetForm(forms.ModelForm):
|
||||
related_models = {
|
||||
'asset': models.Asset,
|
||||
'supplier': models.Supplier
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = models.Asset
|
||||
fields = '__all__'
|
||||
exclude = ['asset_id_prefix', 'asset_id_number']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['date_sold'].widget.format = '%Y-%m-%d'
|
||||
self.fields['date_acquired'].widget.format = '%Y-%m-%d'
|
||||
|
||||
|
||||
class AssetSearchForm(forms.Form):
|
||||
query = forms.CharField(required=False)
|
||||
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
||||
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
||||
|
||||
|
||||
class SupplierForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.Supplier
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class SupplierSearchForm(forms.Form):
|
||||
query = forms.CharField(required=False)
|
||||
0
assets/management/__init__.py
Normal file
0
assets/management/__init__.py
Normal file
0
assets/management/commands/__init__.py
Normal file
0
assets/management/commands/__init__.py
Normal file
23
assets/management/commands/deleteSampleData.py
Normal file
23
assets/management/commands/deleteSampleData.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from assets import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deletes testing sample data'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG):
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
self.delete_objects(models.AssetCategory)
|
||||
self.delete_objects(models.AssetStatus)
|
||||
self.delete_objects(models.Supplier)
|
||||
self.delete_objects(models.Connector)
|
||||
self.delete_objects(models.Asset)
|
||||
|
||||
def delete_objects(self, model):
|
||||
for object in model.objects.all():
|
||||
object.delete()
|
||||
120
assets/management/commands/generateSampleAssetsData.py
Normal file
120
assets/management/commands/generateSampleAssetsData.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import random
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
from assets import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Creates some sample data for testing'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG or settings.STAGING):
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
random.seed('Some object to see the random number generator')
|
||||
|
||||
self.create_categories()
|
||||
self.create_statuses()
|
||||
self.create_suppliers()
|
||||
self.create_assets()
|
||||
self.create_connectors()
|
||||
self.create_cables()
|
||||
|
||||
def create_categories(self):
|
||||
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
|
||||
|
||||
for cat in categories:
|
||||
models.AssetCategory.objects.create(name=cat)
|
||||
|
||||
def create_statuses(self):
|
||||
statuses = [('In Service', True), ('Lost', False), ('Binned', False), ('Sold', False), ('Broken', False)]
|
||||
|
||||
for stat in statuses:
|
||||
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1])
|
||||
|
||||
def create_suppliers(self):
|
||||
suppliers = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
|
||||
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
||||
|
||||
for supplier in suppliers:
|
||||
models.Supplier.objects.create(name=supplier)
|
||||
|
||||
def create_assets(self):
|
||||
asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink']
|
||||
|
||||
categories = models.AssetCategory.objects.all()
|
||||
statuses = models.AssetStatus.objects.all()
|
||||
suppliers = models.Supplier.objects.all()
|
||||
|
||||
for i in range(100):
|
||||
asset = models.Asset(
|
||||
asset_id='{}'.format(models.Asset.get_available_asset_id()),
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(categories),
|
||||
status=random.choice(statuses),
|
||||
date_acquired=timezone.now().date()
|
||||
)
|
||||
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(suppliers)
|
||||
asset.clean()
|
||||
asset.save()
|
||||
|
||||
def create_cables(self):
|
||||
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
|
||||
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
|
||||
|
||||
csas = [0.75, 1.00, 1.25, 2.5, 4]
|
||||
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
|
||||
cores = [3, 5]
|
||||
circuits = [1, 2, 3, 6]
|
||||
categories = models.AssetCategory.objects.all()
|
||||
statuses = models.AssetStatus.objects.all()
|
||||
suppliers = models.Supplier.objects.all()
|
||||
connectors = models.Connector.objects.all()
|
||||
|
||||
for i in range(100):
|
||||
asset = models.Asset(
|
||||
asset_id='{}'.format(models.Asset.get_available_asset_id()),
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(categories),
|
||||
status=random.choice(statuses),
|
||||
date_acquired=timezone.now().date(),
|
||||
|
||||
is_cable=True,
|
||||
plug=random.choice(connectors),
|
||||
socket=random.choice(connectors),
|
||||
csa=random.choice(csas),
|
||||
length=random.choice(lengths),
|
||||
circuits=random.choice(circuits),
|
||||
cores=random.choice(circuits)
|
||||
)
|
||||
|
||||
if i % 5 == 0:
|
||||
prefix = random.choice(asset_prefixes)
|
||||
asset.asset_id = prefix + str(models.Asset.get_available_asset_id(wanted_prefix=prefix))
|
||||
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(suppliers)
|
||||
|
||||
asset.clean()
|
||||
asset.save()
|
||||
|
||||
def create_connectors(self):
|
||||
connectors = [
|
||||
{"description": "13A UK", "current_rating": 13, "voltage_rating": 230, "num_pins": 3},
|
||||
{"description": "16A", "current_rating": 16, "voltage_rating": 230, "num_pins": 3},
|
||||
{"description": "32/3", "current_rating": 32, "voltage_rating": 400, "num_pins": 5},
|
||||
{"description": "Socapex", "current_rating": 23, "voltage_rating": 600, "num_pins": 19},
|
||||
]
|
||||
for connector in connectors:
|
||||
conn = models.Connector.objects.create(** connector)
|
||||
conn.save()
|
||||
86
assets/migrations/0001_initial.py
Normal file
86
assets/migrations/0001_initial.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# Generated by Django 2.0.2 on 2018-02-28 16:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Asset',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('asset_id', models.IntegerField()),
|
||||
('description', models.CharField(max_length=120)),
|
||||
('serial_number', models.CharField(blank=True, max_length=150, null=True)),
|
||||
('date_acquired', models.DateField()),
|
||||
('date_sold', models.DateField(blank=True, null=True)),
|
||||
('purchase_price', models.IntegerField()),
|
||||
('salvage_value', models.IntegerField(blank=True, null=True)),
|
||||
('comments', models.TextField(blank=True, null=True)),
|
||||
('next_sched_maint', models.DateField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssetCategory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=80)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Asset Category',
|
||||
'verbose_name_plural': 'Asset Categories',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssetStatus',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=80)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Asset Status',
|
||||
'verbose_name_plural': 'Asset Statuses',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Collection',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=80)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Supplier',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=80)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetCategory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='collection',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Collection'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='purchased_from',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.Supplier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='status',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetStatus'),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0002_auto_20180301_1654.py
Normal file
18
assets/migrations/0002_auto_20180301_1654.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.2 on 2018-03-01 16:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.IntegerField(blank=True),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0003_auto_20180301_1700.py
Normal file
18
assets/migrations/0003_auto_20180301_1700.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.2 on 2018-03-01 17:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0002_auto_20180301_1654'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='purchase_price',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
19
assets/migrations/0004_auto_20180301_1711.py
Normal file
19
assets/migrations/0004_auto_20180301_1711.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.0.2 on 2018-03-01 17:11
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0003_auto_20180301_1700'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='collection',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.Collection'),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0005_auto_20180301_1725.py
Normal file
18
assets/migrations/0005_auto_20180301_1725.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.2 on 2018-03-01 17:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0004_auto_20180301_1711'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,148 @@
|
||||
# Generated by Django 2.1.5 on 2019-01-05 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('assets', '0006_auto_20180728_1451'), ('assets', '0007_auto_20181215_1447'), ('assets', '0008_auto_20181215_1448'), ('assets', '0009_auto_20181215_1640'), ('assets', '0010_auto_20181215_1640'), ('assets', '0011_auto_20181215_1749'), ('assets', '0012_auto_20181215_1813'), ('assets', '0013_asset_parent'), ('assets', '0014_auto_20190103_1615'), ('assets', '0015_auto_20190103_1617'), ('assets', '0016_remove_asset_collection'), ('assets', '0017_delete_collection'), ('assets', '0018_auto_20190103_1708'), ('assets', '0019_auto_20190103_1723'), ('assets', '0020_auto_20190103_1729'), ('assets', '0021_auto_20190105_1156')]
|
||||
|
||||
dependencies = [
|
||||
('assets', '0005_auto_20180301_1725'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.IntegerField(blank=True, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='purchase_price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='salvage_value',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='is_cable',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='length',
|
||||
field=models.DecimalField(blank=True, decimal_places=1, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.CharField(blank=True, max_length=10, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.CharField(default='', max_length=10),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Asset'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='collection',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Collection',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Cable',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('asset_id', models.CharField(max_length=10)),
|
||||
('description', models.CharField(max_length=120)),
|
||||
('serial_number', models.CharField(blank=True, max_length=150, null=True)),
|
||||
('date_acquired', models.DateField()),
|
||||
('date_sold', models.DateField(blank=True, null=True)),
|
||||
('purchase_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('salvage_value', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('comments', models.TextField(blank=True, null=True)),
|
||||
('next_sched_maint', models.DateField(blank=True, null=True)),
|
||||
('is_cable', models.BooleanField(default=False)),
|
||||
('length', models.DecimalField(blank=True, decimal_places=1, help_text='m', max_digits=10, null=True)),
|
||||
('csa', models.DecimalField(blank=True, decimal_places=2, help_text='mm^2', max_digits=10, null=True)),
|
||||
('circuits', models.IntegerField(blank=True, null=True)),
|
||||
('cores', models.IntegerField(blank=True, null=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetCategory')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Cable')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Connector',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.CharField(max_length=80)),
|
||||
('current_rating', models.DecimalField(decimal_places=2, help_text='Amps', max_digits=10)),
|
||||
('voltage_rating', models.IntegerField(default=0, help_text='Volts')),
|
||||
('num_pins', models.IntegerField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cable',
|
||||
name='plug',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cable',
|
||||
name='purchased_from',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.Supplier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cable',
|
||||
name='socket',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cable',
|
||||
name='status',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetStatus'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='serial_number',
|
||||
field=models.CharField(blank=True, default='', max_length=150),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='serial_number',
|
||||
field=models.CharField(blank=True, default='', max_length=150),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,176 @@
|
||||
# Generated by Django 2.0.13 on 2019-12-04 17:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('assets', '0007_auto_20190108_0202'), ('assets', '0008_auto_20191002_1931'), ('assets', '0009_auto_20191008_2148'), ('assets', '0010_auto_20191013_2123'), ('assets', '0011_auto_20191013_2247'), ('assets', '0012_auto_20191014_0012'), ('assets', '0013_auto_20191016_1446'), ('assets', '0014_auto_20191017_2052')]
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('assets', '0006_auto_20180728_1451_squashed_0021_auto_20190105_1156'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Asset'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Cable'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='voltage_rating',
|
||||
field=models.IntegerField(help_text='Volts'),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'base_manager_name': 'objects'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cable',
|
||||
options={'base_manager_name': 'objects'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='length',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='asset_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='category',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='comments',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='date_acquired',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='date_sold',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='is_cable',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='next_sched_maint',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='parent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='purchase_price',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='purchased_from',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='salvage_value',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='serial_number',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='status',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='polymorphic_ctype',
|
||||
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_assets.asset_set+', to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.CharField(max_length=10, unique=True),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='plug',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='socket',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='polymorphic_ctype',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='circuits',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='cores',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='csa',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='mm^2', max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='length',
|
||||
field=models.DecimalField(blank=True, decimal_places=1, help_text='m', max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='plug',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='socket',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Cable',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['asset_id'], 'permissions': (('asset_finance', 'Can see financial data for assets'), ('view_asset', 'Can view an asset'))},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assetstatus',
|
||||
name='should_show',
|
||||
field=models.BooleanField(default=True, help_text='Should this be shown by default in the asset list.'),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='supplier',
|
||||
options={'permissions': (('view_supplier', 'Can view a supplier'),)},
|
||||
),
|
||||
]
|
||||
51
assets/migrations/0008_auto_20191206_2124.py
Normal file
51
assets/migrations/0008_auto_20191206_2124.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Generated by Django 2.0.13 on 2019-12-06 21:24
|
||||
|
||||
from django.db import migrations, models, transaction
|
||||
import re
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
AssetModel = apps.get_model('assets', 'Asset')
|
||||
with transaction.atomic():
|
||||
for row in AssetModel.objects.all():
|
||||
|
||||
row.asset_id = row.asset_id.upper()
|
||||
asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id)
|
||||
if asset_search is None: # If the asset_id doesn't have a number at the end
|
||||
row.asset_id += "1"
|
||||
|
||||
asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id)
|
||||
row.asset_id_prefix = asset_search.group(1)
|
||||
row.asset_id_number = int(asset_search.group(2))
|
||||
|
||||
row.save(update_fields=['asset_id', 'asset_id_prefix', 'asset_id_number'])
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0007_auto_20190108_0202_squashed_0014_auto_20191017_2052'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['asset_id_prefix', 'asset_id_number'], 'permissions': (('asset_finance', 'Can see financial data for assets'), ('view_asset', 'Can view an asset'))},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='asset_id_number',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='asset_id_prefix',
|
||||
field=models.CharField(default='', max_length=8),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.CharField(max_length=15, unique=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=forwards,
|
||||
reverse_code=migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
]
|
||||
32
assets/migrations/0009_auto_20200103_2215.py
Normal file
32
assets/migrations/0009_auto_20200103_2215.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 2.0.13 on 2020-01-03 22:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0008_auto_20191206_2124'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='assetcategory',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Asset Category', 'verbose_name_plural': 'Asset Categories'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='assetstatus',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Asset Status', 'verbose_name_plural': 'Asset Statuses'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assetstatus',
|
||||
name='display_class',
|
||||
field=models.CharField(blank=True, help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='purchased_from',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Supplier'),
|
||||
),
|
||||
]
|
||||
17
assets/migrations/0010_auto_20200207_1737.py
Normal file
17
assets/migrations/0010_auto_20200207_1737.py
Normal file
@@ -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'),)},
|
||||
),
|
||||
]
|
||||
0
assets/migrations/__init__.py
Normal file
0
assets/migrations/__init__.py
Normal file
180
assets/models.py
Normal file
180
assets/models.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import re
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, connection
|
||||
from django.urls import reverse
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
|
||||
from reversion import revisions as reversion
|
||||
from reversion.models import Version
|
||||
|
||||
from RIGS.models import RevisionMixin
|
||||
|
||||
|
||||
class AssetCategory(models.Model):
|
||||
class Meta:
|
||||
verbose_name = 'Asset Category'
|
||||
verbose_name_plural = 'Asset Categories'
|
||||
ordering = ['name']
|
||||
|
||||
name = models.CharField(max_length=80)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AssetStatus(models.Model):
|
||||
class Meta:
|
||||
verbose_name = 'Asset Status'
|
||||
verbose_name_plural = 'Asset Statuses'
|
||||
ordering = ['name']
|
||||
|
||||
name = models.CharField(max_length=80)
|
||||
should_show = models.BooleanField(
|
||||
default=True, help_text="Should this be shown by default in the asset list.")
|
||||
display_class = models.CharField(max_length=80, blank=True, null=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Supplier(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=80)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
permissions = (
|
||||
('view_supplier', 'Can view a supplier'),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('supplier_list')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Connector(models.Model):
|
||||
description = models.CharField(max_length=80)
|
||||
current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text='Amps')
|
||||
voltage_rating = models.IntegerField(help_text='Volts')
|
||||
num_pins = models.IntegerField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.description
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Asset(models.Model, RevisionMixin):
|
||||
class Meta:
|
||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||
permissions = (
|
||||
('asset_finance', 'Can see financial data for assets'),
|
||||
('view_asset', 'Can view an asset')
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
||||
blank=True, null=True, on_delete=models.SET_NULL)
|
||||
asset_id = models.CharField(max_length=15, unique=True)
|
||||
description = models.CharField(max_length=120)
|
||||
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
|
||||
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
|
||||
serial_number = models.CharField(max_length=150, blank=True)
|
||||
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True, related_name="assets")
|
||||
date_acquired = models.DateField()
|
||||
date_sold = models.DateField(blank=True, null=True)
|
||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||
salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||
comments = models.TextField(blank=True)
|
||||
next_sched_maint = models.DateField(blank=True, null=True)
|
||||
|
||||
# Cable assets
|
||||
is_cable = models.BooleanField(default=False)
|
||||
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL,
|
||||
related_name='plug', blank=True, null=True)
|
||||
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL,
|
||||
related_name='socket', blank=True, null=True)
|
||||
length = models.DecimalField(decimal_places=1, max_digits=10,
|
||||
blank=True, null=True, help_text='m')
|
||||
csa = models.DecimalField(decimal_places=2, max_digits=10,
|
||||
blank=True, null=True, help_text='mm^2')
|
||||
circuits = models.IntegerField(blank=True, null=True)
|
||||
cores = models.IntegerField(blank=True, null=True)
|
||||
|
||||
# Hidden asset_id components
|
||||
# For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001"
|
||||
asset_id_prefix = models.CharField(max_length=8, default="")
|
||||
asset_id_number = models.IntegerField(default=1)
|
||||
|
||||
def get_available_asset_id(wanted_prefix=""):
|
||||
sql = """
|
||||
SELECT a.asset_id_number+1
|
||||
FROM assets_asset a
|
||||
LEFT OUTER JOIN assets_asset b ON
|
||||
(a.asset_id_number + 1 = b.asset_id_number AND
|
||||
a.asset_id_prefix = b.asset_id_prefix)
|
||||
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, [9000, wanted_prefix])
|
||||
row = cursor.fetchone()
|
||||
if row is None or row[0] is None:
|
||||
return 9000
|
||||
else:
|
||||
return row[0]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('asset_detail', kwargs={'pk': self.asset_id})
|
||||
|
||||
def __str__(self):
|
||||
out = str(self.asset_id) + ' - ' + self.description
|
||||
if self.is_cable:
|
||||
out += '{} - {}m - {}'.format(self.plug, self.length, self.socket)
|
||||
return out
|
||||
|
||||
def clean(self):
|
||||
errdict = {}
|
||||
if self.date_sold and self.date_acquired > self.date_sold:
|
||||
errdict["date_sold"] = ["Cannot sell an item before it is acquired"]
|
||||
|
||||
self.asset_id = self.asset_id.upper()
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id)
|
||||
if asset_search is None:
|
||||
errdict["asset_id"] = [
|
||||
"An Asset ID can only consist of letters and numbers, with a final number"]
|
||||
|
||||
if self.purchase_price and self.purchase_price < 0:
|
||||
errdict["purchase_price"] = ["A price cannot be negative"]
|
||||
|
||||
if self.salvage_value and self.salvage_value < 0:
|
||||
errdict["salvage_value"] = ["A price cannot be negative"]
|
||||
|
||||
if self.is_cable:
|
||||
if not self.length or self.length <= 0:
|
||||
errdict["length"] = ["The length of a cable must be more than 0"]
|
||||
if not self.csa or self.csa <= 0:
|
||||
errdict["csa"] = ["The CSA of a cable must be more than 0"]
|
||||
if not self.circuits or self.circuits <= 0:
|
||||
errdict["circuits"] = ["There must be at least one circuit in a cable"]
|
||||
if not self.cores or self.cores <= 0:
|
||||
errdict["cores"] = ["There must be at least one core in a cable"]
|
||||
if self.socket is None:
|
||||
errdict["socket"] = ["A cable must have a socket"]
|
||||
if self.plug is None:
|
||||
errdict["plug"] = ["A cable must have a plug"]
|
||||
|
||||
if errdict != {}: # If there was an error when validation
|
||||
raise ValidationError(errdict)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Asset)
|
||||
def pre_save_asset(sender, instance, **kwargs):
|
||||
"""Automatically fills in hidden members on database access"""
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
if asset_search is None:
|
||||
instance.asset_id += "1"
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
instance.asset_id_prefix = asset_search.group(1)
|
||||
instance.asset_id_number = int(asset_search.group(2))
|
||||
23
assets/static/js/csrf.js
Normal file
23
assets/static/js/csrf.js
Normal file
@@ -0,0 +1,23 @@
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(xhr, settings) {
|
||||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie != '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
|
||||
// Only send the token to relative URLs i.e. locally.
|
||||
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
|
||||
}
|
||||
}
|
||||
});
|
||||
92
assets/templates/asset_activity_table.html
Normal file
92
assets/templates/asset_activity_table.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
|
||||
{% load static %}
|
||||
{% load paginator from filters %}
|
||||
{% load to_class_name from filters %}
|
||||
|
||||
{% block title %}Asset Activity Stream{% endblock %}
|
||||
|
||||
{# TODO: Find a way to reduce code duplication...can't just include the content because of the IDs... #}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script src="{% static "js/popover.js" %}"></script>
|
||||
<script src="{% static "js/moment.min.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover().click(function(){
|
||||
if($(this).attr('href')){
|
||||
window.location.href = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
|
||||
// This keeps timeago values correct, but uses an insane amount of resources
|
||||
// $(function () {
|
||||
// setInterval(function() {
|
||||
// $('.date').each(function (index, dateElem) {
|
||||
// var $dateElem = $(dateElem);
|
||||
// var formatted = moment($dateElem.attr('data-date')).fromNow();
|
||||
// $dateElem.text(formatted);
|
||||
// })
|
||||
// });
|
||||
// }, 10000);
|
||||
|
||||
|
||||
$('.date').each(function (index, dateElem) {
|
||||
var $dateElem = $(dateElem);
|
||||
var formatted = moment($dateElem.attr('data-date')).fromNow();
|
||||
$dateElem.text(formatted);
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3>Asset Activity Stream</h3>
|
||||
</div>
|
||||
<div class="text-right col-sm-12">{% paginator %}</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Object</td>
|
||||
<td>Version ID</td>
|
||||
<td>User</td>
|
||||
<td>Changes</td>
|
||||
<td>Comment</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in object_list %}
|
||||
|
||||
<tr>
|
||||
<td>{{ version.revision.date_created }}</td>
|
||||
<td><a href="{{ version.changes.new.get_absolute_url }}">{{version.changes.new|to_class_name}} {{ version.changes.new.asset_id|default:version.changes.new.pk }}</a></td>
|
||||
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
|
||||
<td>{{ version.revision.user.name }}</td>
|
||||
<td>
|
||||
{% if version.changes.old == None %}
|
||||
{{version.changes.new|to_class_name}} Created
|
||||
{% else %}
|
||||
{% include 'RIGS/version_changes.html' %}
|
||||
{% endif %} </td>
|
||||
<td>{{ version.changes.revision.comment }}</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="align-right">{% paginator %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
60
assets/templates/asset_create.html
Normal file
60
assets/templates/asset_create.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if duplicate %}
|
||||
Duplication of Asset: {{ previous_asset_id }}
|
||||
{% else %}
|
||||
Create Asset
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
{% if duplicate %}
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_duplicate' pk=previous_asset_id%}">
|
||||
{% else %}
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_create'%}">
|
||||
{% endif %}
|
||||
{% include 'form_errors.html' %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'partials/asset_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-6" hidden="true" id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js%}
|
||||
<script>
|
||||
function checkIfCableHidden() {
|
||||
if (document.getElementById("id_is_cable").checked) {
|
||||
document.getElementById("cable-table").hidden = false;
|
||||
} else {
|
||||
document.getElementById("cable-table").hidden = true;
|
||||
}
|
||||
}
|
||||
checkIfCableHidden();
|
||||
</script>
|
||||
{%endblock%}
|
||||
40
assets/templates/asset_embed.html
Normal file
40
assets/templates/asset_embed.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends 'base_embed.html' %}
|
||||
{% load static from staticfiles %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<a href="/assets">
|
||||
<span class="source"> TEC Asset Database</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<h3><a href="{% url 'asset_detail' object.asset_id %}">Asset: {{ object.asset_id }} | {{ object.description }} </a></h3>
|
||||
<h4>
|
||||
<span class="label label-default">
|
||||
<strong>Category:</strong>
|
||||
{{ object.category }}
|
||||
</span>
|
||||
|
||||
<span class="label label-{{ object.status.display_class|default:'default' }}">
|
||||
<strong>Status:</strong>
|
||||
{{ object.status }}
|
||||
</span>
|
||||
</h4>
|
||||
{% if object.serial_number %}
|
||||
<dt>Serial Number: </dt>
|
||||
<dd>{{ object.serial_number }}</dd>
|
||||
{% endif %}
|
||||
{% if object.comments %}
|
||||
<dt>Comments: </dt>
|
||||
<dd class="dont-break-out">{{ object.comments|linebreaksbr }}<dd>
|
||||
{% endif %}
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
65
assets/templates/asset_list.html
Normal file
65
assets/templates/asset_list.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Asset List{% endblock %}
|
||||
{% load paginator from filters %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="text-center">Asset List</h1>
|
||||
</div>
|
||||
|
||||
<form id="asset-search-form" method="get" class="form-inline pull-right">
|
||||
<div class="input-group pull-right" style="width: auto;">
|
||||
{% render_field form.query|add_class:'form-control' placeholder='Search by Asset ID/Desc/Serial' style="width: 250px"%}
|
||||
<label for="query" class="sr-only">Asset ID/Description/Serial Number:</label>
|
||||
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
|
||||
</div>
|
||||
<br>
|
||||
<div style="margin-top: 1em;" class="pull-right">
|
||||
<div id="category-group" class="form-group">
|
||||
<label for="category" class="sr-only">Category</label>
|
||||
{% render_field form.category|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
|
||||
</div>
|
||||
<div id="status-group" class="form-group">
|
||||
<label for="status" class="sr-only">Status</label>
|
||||
{% render_field form.status|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
||||
</div>
|
||||
<!---TODO: Auto filter whenever an option is selected, instead of using a button -->
|
||||
<button id="filter-submit" type="submit" class="btn btn-default">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset ID</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th class="hidden-xs">Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% include 'partials/asset_list_table_body.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="text-center">
|
||||
{% paginator %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% load static %}
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
|
||||
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static "js/bootstrap-select.js" %}"></script>
|
||||
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
|
||||
{% endblock %}
|
||||
70
assets/templates/asset_update.html
Normal file
70
assets/templates/asset_update.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if edit and object %}
|
||||
Edit Asset: {{ object.asset_id }}
|
||||
{% else %}
|
||||
Asset: {{ object.asset_id }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_update' pk=object.asset_id%}">
|
||||
{% include 'form_errors.html' %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'partials/asset_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if perms.assets.asset_finance %}
|
||||
<div class="col-md-6">
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="col-md-6"
|
||||
{% if not object.is_cable %} hidden="true" {% endif %} id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if not edit and perms.assets.view_asset %}
|
||||
<div class="col-sm-12 text-right">
|
||||
<div>
|
||||
<a href="{% url 'asset_history' object.asset_id %}" title="View Revision History">
|
||||
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js%}
|
||||
{% if edit %}
|
||||
<script>
|
||||
function checkIfCableHidden() {
|
||||
if (document.getElementById("id_is_cable").checked) {
|
||||
document.getElementById("cable-table").hidden = false;
|
||||
} else {
|
||||
document.getElementById("cable-table").hidden = true;
|
||||
}
|
||||
}
|
||||
checkIfCableHidden();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
16
assets/templates/asset_update_search_results.html
Normal file
16
assets/templates/asset_update_search_results.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% for asset in object_list %}
|
||||
<a href="javascript:void(0);" onclick="insertInParentField({{ asset.pk }}, '{{ asset }}')">
|
||||
{{ asset.asset_id }} - {{ asset.description }}
|
||||
</a>
|
||||
<br>
|
||||
{% empty %}
|
||||
No assets match given ID
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
function insertInParentField(pk, str) {
|
||||
$('#hidden_parent_id').val(pk);
|
||||
$('#parent_id').val(str);
|
||||
M.updateTextFields();
|
||||
}
|
||||
</script>
|
||||
68
assets/templates/asset_version_history.html
Normal file
68
assets/templates/asset_version_history.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
|
||||
{% load to_class_name from filters %}
|
||||
{% load paginator from filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{object|to_class_name}} {{ object.asset_id }} - Revision History{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script src="{% static "js/popover.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover().click(function(){
|
||||
if($(this).attr('href')){
|
||||
window.location.href = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3><a href="{{ object.get_absolute_url }}">{{object|to_class_name}} {{ object.asset_id|default:object.pk }}</a> - Revision History</h3>
|
||||
</div>
|
||||
<div class="text-right col-sm-12">{% paginator %}</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Version ID</td>
|
||||
<td>User</td>
|
||||
<td>Changes</td>
|
||||
<td>Comment</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in object_list %}
|
||||
|
||||
<tr>
|
||||
<td>{{ version.revision.date_created }}</td>
|
||||
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
|
||||
<td>{{ version.revision.user.name }}</td>
|
||||
<td>
|
||||
{% if version.changes.old is None %}
|
||||
{{object|to_class_name}} Created
|
||||
{% else %}
|
||||
{% include 'RIGS/version_changes.html' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ version.revision.comment }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="align-right">{% paginator %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
assets/templates/partials/asset_buttons.html
Normal file
25
assets/templates/partials/asset_buttons.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% if edit and object %}
|
||||
<!--edit-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
{% elif duplicate %}
|
||||
<!--duplicate-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
|
||||
{% elif create %}
|
||||
<!--create-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
{% else %}
|
||||
<!--detail view-->
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'asset_update' object.asset_id %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||
<a class="btn btn-default" href="{% url 'asset_duplicate' object.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if create or edit or duplicate %}
|
||||
<br>
|
||||
<button type="reset" class="btn btn-link" onclick="
|
||||
{%if duplicate%}
|
||||
{% url 'asset_detail' previous_asset_id %}
|
||||
{%else%}
|
||||
history.back(){%endif%}">Cancel</button>
|
||||
{% endif %}
|
||||
60
assets/templates/partials/asset_form.html
Normal file
60
assets/templates/partials/asset_form.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% load widget_tweaks %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Asset Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.asset_id.id_for_label }}">Asset ID</label>
|
||||
{% if duplicate %}
|
||||
{% render_field form.asset_id|add_class:'form-control' value=object.asset_id %}
|
||||
{% elif object.asset_id %}
|
||||
{% render_field form.asset_id|attr:'readonly'|add_class:'disabled_input form-control' value=object.asset_id %}
|
||||
{% else %}
|
||||
{% render_field form.asset_id|add_class:'form-control' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.description.id_for_label }}">Description</label>
|
||||
{% render_field form.description|add_class:'form-control' value=object.description %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.category.id_for_label }}" >Category</label>
|
||||
{% render_field form.category|add_class:'form-control'%}
|
||||
</div>
|
||||
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %} <label for="{{ form.is_cable.id_for_label }}">Cable?</label>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.status.id_for_label }}" >Status</label>
|
||||
{% render_field form.status|add_class:'form-control'%}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.serial_number.id_for_label }}">Serial Number</label>
|
||||
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
|
||||
</div>
|
||||
<!---TODO: Lower default number of lines in comments box-->
|
||||
<div class="form-group">
|
||||
<label for="{{ form.comments.id_for_label }}">Comments</label>
|
||||
{% render_field form.comments|add_class:'form-control' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dt>Asset ID</dt>
|
||||
<dd>{{ object.asset_id }}</dd>
|
||||
|
||||
<dt>Description</dt>
|
||||
<dd style="overflow-wrap: break-word;">{{ object.description }}</dd>
|
||||
|
||||
<dt>Category</dt>
|
||||
<dd>{{ object.category }}</dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd>{{ object.status }}</dd>
|
||||
|
||||
<dt>Serial Number</dt>
|
||||
<dd>{{ object.serial_number|default:'-' }}</dd>
|
||||
|
||||
<dt>Comments</dt>
|
||||
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|linebreaksbr }}</dd>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
19
assets/templates/partials/asset_list_table_body.html
Normal file
19
assets/templates/partials/asset_list_table_body.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% for item in object_list %}
|
||||
{# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
|
||||
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for-->
|
||||
<tr class="{{ item.status.display_class|default:'' }} assetRow">
|
||||
<td style="vertical-align: middle;"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
|
||||
<td class="assetDesc" style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
|
||||
<td class="assetCategory" style="vertical-align: middle;">{{ item.category }}</td>
|
||||
<td class="assetStatus" style="vertical-align: middle;">{{ item.status }}</td>
|
||||
<td class="hidden-xs">
|
||||
<div class="btn-group" role="group">
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_detail' item.asset_id %}"><i class="glyphicon glyphicon-eye-open"></i> View</a>
|
||||
{% if perms.assets.change_asset %}
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_update' item.asset_id %}"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_duplicate' item.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
65
assets/templates/partials/asset_picker.html
Normal file
65
assets/templates/partials/asset_picker.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<select name="parent" id="parent_id" class="selectpicker">
|
||||
{% if object.parent%}
|
||||
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
|
||||
{% load static %}
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
|
||||
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static "js/bootstrap-select.js" %}"></script>
|
||||
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ js.super }}
|
||||
<script>
|
||||
$('#parent_id')
|
||||
.selectpicker({
|
||||
liveSearch: true
|
||||
})
|
||||
.ajaxSelectPicker({
|
||||
ajax: {
|
||||
url: '{% url 'asset_search_json'%}',
|
||||
type: "get",
|
||||
data: function () {
|
||||
var params = {
|
||||
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
|
||||
};
|
||||
return params;
|
||||
}
|
||||
},
|
||||
locale: {
|
||||
emptyTitle: 'Search for item...'
|
||||
},
|
||||
preprocessData: function(data){
|
||||
var assets = [];
|
||||
if(data.length){
|
||||
var len = data.length;
|
||||
for(var i = 0; i < len; i++){
|
||||
var curr = data[i];
|
||||
assets.push(
|
||||
{
|
||||
'value': curr.id,
|
||||
'text': curr.label,
|
||||
'disabled': false
|
||||
}
|
||||
);
|
||||
}
|
||||
assets.push(
|
||||
{
|
||||
'value': null,
|
||||
'text': "No parent"
|
||||
});
|
||||
}
|
||||
|
||||
return assets;
|
||||
},
|
||||
preserveSelected: false
|
||||
});
|
||||
</script>
|
||||
{% endblock js %}
|
||||
60
assets/templates/partials/cable_form.html
Normal file
60
assets/templates/partials/cable_form.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% load widget_tweaks %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Cable Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.plug.id_for_label }}">Plug</label>
|
||||
{% render_field form.plug|add_class:'form-control'%}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.socket.id_for_label }}">Socket</label>
|
||||
{% render_field form.socket|add_class:'form-control'%}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.length.id_for_label }}">Length</label>
|
||||
<div class="input-group">
|
||||
{% render_field form.length|add_class:'form-control' %}
|
||||
<span class="input-group-addon">{{ form.length.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.csa.id_for_label }}">Cross Sectional Area</label>
|
||||
<div class="input-group">
|
||||
{% render_field form.csa|add_class:'form-control' value=object.csa %}
|
||||
<span class="input-group-addon">{{ form.csa.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
|
||||
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.cores.id_for_label }}">Cores</label>
|
||||
{% render_field form.cores|add_class:'form-control' value=object.cores %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Socket</dt>
|
||||
<dd>{{ object.socket|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Plug</dt>
|
||||
<dd>{{ object.plug|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Length</dt>
|
||||
<dd>{{ object.length|default_if_none:'-' }}m</dd>
|
||||
|
||||
<dt>Cross Sectional Area</dt>
|
||||
<dd>{{ object.csa|default_if_none:'-' }}m^2</dd>
|
||||
|
||||
<dt>Circuits</dt>
|
||||
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Cores</dt>
|
||||
<dd>{{ object.cores|default_if_none:'-' }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
40
assets/templates/partials/parent_form.html
Normal file
40
assets/templates/partials/parent_form.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% load widget_tweaks %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Collection Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group" id="parent-group">
|
||||
<label for="selectpicker">Set Parent</label>
|
||||
{% include 'partials/asset_picker.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Parent</dt>
|
||||
<dd>
|
||||
{% if object.parent %}
|
||||
<a href="{% url 'asset_detail' object.parent.asset_id %}">
|
||||
{{ object.parent.asset_id }} - {{ object.parent.description }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span>-</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Children</dt>
|
||||
{% if object.asset_parent.all %}
|
||||
{% for child in object.asset_parent.all %}
|
||||
<dd>
|
||||
<a href="{% url 'asset_detail' child.asset_id %}">
|
||||
{{ child.asset_id }} - {{ child.description }}
|
||||
</a>
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<dd><span>-</span></dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endif%}
|
||||
</div>
|
||||
</div>
|
||||
88
assets/templates/partials/purchasedetails_form.html
Normal file
88
assets/templates/partials/purchasedetails_form.html
Normal file
@@ -0,0 +1,88 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
|
||||
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static "js/bootstrap-select.js" %}"></script>
|
||||
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/autocompleter.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Purchase Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group" id="purchased-from-group">
|
||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||
{% if object.purchased_from %}
|
||||
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.date_acquired.id_for_label }}" >Date Acquired</label>
|
||||
{% if object.date_acquired%}
|
||||
{% with date_acq=object.date_acquired|date:"Y-m-d" %}
|
||||
{% render_field form.date_acquired|add_class:'form-control'|attr:'type="date"' value=date_acq %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<input type="date" name="date_acquired" value="{% now "Y-m-d" %}"
|
||||
class="form-control" id="id_date_acquired">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.date_sold.id_for_label }}">Date Sold</label>
|
||||
{% with date_sol=object.form.date_sold|date:"Y-m-d" %}
|
||||
{% render_field form.date_sold|add_class:'form-control'|attr:'type="date"' value=date_sol %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Purchased From</dt>
|
||||
<dd>{{ object.purchased_from|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Purchase Price</dt>
|
||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Salvage Value</dt>
|
||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Date Acquired</dt>
|
||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||
{% if object.date_sold %}
|
||||
<dt>Date Sold</dt>
|
||||
<dd>{{ object.date_sold|default_if_none:'-' }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
16
assets/templates/partials/render_field.html
Normal file
16
assets/templates/partials/render_field.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<!---TODO: Assign form-control class in here--->
|
||||
<div class="form-group">
|
||||
<label for="{{ field.id_for_label }}">{{ label|default:field.label }}</label>
|
||||
{% if css %}
|
||||
{% render_field field|add_class:css %}
|
||||
{% elif disable_if_filled and field.value %}
|
||||
{% render_field field|attr:'disabled' %}
|
||||
{% elif css and disable_if_filled %}
|
||||
{% render_field field|add_class:css|attr:'disabled' %}
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
<span class="helper-text" data-error="{{ field.errors.text }}"></span>
|
||||
</div>
|
||||
73
assets/templates/supplier_detail.html
Normal file
73
assets/templates/supplier_detail.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Supplier | {{ object.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% if not request.is_ajax %}
|
||||
<div class="col-sm-12">
|
||||
<h1>Supplier | {{ object.name }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'supplier_update' object.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-pencil"></span> Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">Supplier Details</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Name</dt>
|
||||
<dd>{{ object.name }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Associated Assets</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset ID</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th class="hidden-xs">Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% with object.assets.all as object_list %}
|
||||
{% include 'partials/asset_list_table_body.html' %}
|
||||
{% endwith %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if not request.is_ajax %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'supplier_update' object.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-pencil"></span> Edit</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'supplier_update' object.pk %}" title="View Revision History">
|
||||
Last edited {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
47
assets/templates/supplier_list.html
Normal file
47
assets/templates/supplier_list.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Supplier List{% endblock %}
|
||||
{% load paginator from filters %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Supplier List</h1>
|
||||
</div>
|
||||
|
||||
<form id="supplier-search-form" method="get" class="form-inline pull-right">
|
||||
{% csrf_token %}
|
||||
<div class="input-group pull-right" style="width: auto;">
|
||||
{% render_field form.query|add_class:'form-control' placeholder='Search by Name' style="width: 250px"%}
|
||||
<label for="query" class="sr-only">Name:</label>
|
||||
<span class="input-group-btn"><button type="submit" class="btn btn-default" id="id_search">Search</button></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Supplier</th>
|
||||
<th>Quick Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% for item in object_list %}
|
||||
<tr class="supplierRow">
|
||||
<td class="supplierName">{{ item.name }}</td>
|
||||
<td>
|
||||
<a href="{% url 'supplier_detail' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-eye-open"></i> View</a>
|
||||
<a href="{% url 'supplier_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="text-center">
|
||||
{% paginator %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
20
assets/templates/supplier_update.html
Normal file
20
assets/templates/supplier_update.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% block title %}Edit{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Supplier
|
||||
{% if object %}
|
||||
Edit: {{ object.name }}
|
||||
{% else %}
|
||||
Create
|
||||
{% endif %}</h1>
|
||||
</div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'form_errors.html' %}
|
||||
{{ form }}
|
||||
<input type="submit" value="Save" class="btn btn-success">
|
||||
</form>
|
||||
{% endblock %}
|
||||
0
assets/tests/__init__.py
Normal file
0
assets/tests/__init__.py
Normal file
188
assets/tests/pages.py
Normal file
188
assets/tests/pages.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# Collection of page object models for use within tests.
|
||||
from pypom import Page, Region
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver import Chrome
|
||||
from django.urls import reverse
|
||||
from PyRIGS.tests import regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage
|
||||
import pdb
|
||||
|
||||
|
||||
class AssetList(BasePage):
|
||||
URL_TEMPLATE = '/assets/asset/list'
|
||||
|
||||
_asset_item_locator = (By.CLASS_NAME, 'assetRow')
|
||||
_search_text_locator = (By.ID, 'id_query')
|
||||
_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, '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()
|
||||
|
||||
@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.CSS_SELECTOR, 'div#purchased-from-group>div.bootstrap-select')
|
||||
_parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select')
|
||||
_submit_locator = (By.CLASS_NAME, 'btn-success')
|
||||
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')),
|
||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||
'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')),
|
||||
|
||||
'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 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))
|
||||
|
||||
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/'
|
||||
|
||||
@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'
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return '/duplicate' not in self.driver.current_url
|
||||
|
||||
|
||||
class SupplierList(BasePage):
|
||||
URL_TEMPLATE = reverse('supplier_list')
|
||||
|
||||
_supplier_item_locator = (By.CLASS_NAME, 'supplierRow')
|
||||
_search_text_locator = (By.ID, 'id_query')
|
||||
_go_button_locator = (By.ID, 'id_search')
|
||||
|
||||
class SupplierListRow(Region):
|
||||
_name_locator = (By.CLASS_NAME, "supplierName")
|
||||
|
||||
@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):
|
||||
_submit_locator = (By.CLASS_NAME, 'btn-success')
|
||||
form_items = {
|
||||
'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')
|
||||
|
||||
@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
|
||||
583
assets/tests/test_assets.py
Normal file
583
assets/tests/test_assets.py
Normal file
@@ -0,0 +1,583 @@
|
||||
from . import pages
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from assets import models
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from urllib.parse import urlparse
|
||||
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.common.keys import Keys
|
||||
import datetime
|
||||
|
||||
|
||||
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
|
||||
assetDescriptions = list(map(lambda x: x.description, self.page.assets))
|
||||
self.assertEqual(2, len(assetDescriptions))
|
||||
self.assertIn("A light", assetDescriptions)
|
||||
self.assertIn("Working Mic", assetDescriptions)
|
||||
|
||||
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()
|
||||
|
||||
assetIDs = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", assetIDs[0])
|
||||
self.assertEqual("2", assetIDs[1])
|
||||
self.assertEqual("10", assetIDs[2])
|
||||
self.assertEqual("C1", assetIDs[3])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("10")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("Working Mic", self.page.assets[0].description)
|
||||
self.assertEqual("10", self.page.assets[0].id)
|
||||
|
||||
self.page.set_query("light")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("A light", self.page.assets[0].description)
|
||||
|
||||
self.page.set_query("Random string")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 0)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
# Only working stuff shown by default
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
|
||||
self.page.status_selector.toggle()
|
||||
self.assertTrue(self.page.status_selector.is_open)
|
||||
self.page.status_selector.select_all()
|
||||
self.page.status_selector.toggle()
|
||||
self.assertFalse(self.page.status_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 4)
|
||||
|
||||
self.page.category_selector.toggle()
|
||||
self.assertTrue(self.page.category_selector.is_open)
|
||||
self.page.category_selector.set_option("Sound", True)
|
||||
self.page.category_selector.close()
|
||||
self.assertFalse(self.page.category_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
assetIDs = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", assetIDs[0])
|
||||
self.assertEqual("10", assetIDs[1])
|
||||
|
||||
|
||||
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.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")
|
||||
|
||||
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 = "Bodge Lead"
|
||||
self.page.category = "Health & Safety"
|
||||
self.page.status = "O.K."
|
||||
self.page.serial_number = "0124567890-SAUSAGE"
|
||||
self.page.comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
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.assertFalse(self.page.purchased_from_selector.is_open)
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.salvage_value = "99.12"
|
||||
self.date_acquired = "05022020"
|
||||
|
||||
self.page.parent_selector.toggle()
|
||||
self.assertTrue(self.page.parent_selector.is_open)
|
||||
# Searching it by ID autoselects it
|
||||
self.page.parent_selector.search(self.parent.asset_id)
|
||||
# Needed here but not earlier for whatever reason
|
||||
self.driver.implicitly_wait(1)
|
||||
# self.page.parent_selector.set_option(self.parent.asset_id + " | " + self.parent.description, True)
|
||||
# Need to explicitly close as we haven't selected anything to trigger the auto close
|
||||
self.page.parent_selector.search(Keys.ESCAPE)
|
||||
self.assertFalse(self.page.parent_selector.is_open)
|
||||
self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||
|
||||
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_cable_create(self):
|
||||
self.page.description = "IEC -> IEC"
|
||||
self.page.category = "Health & Safety"
|
||||
self.page.status = "O.K."
|
||||
self.page.serial_number = "MELON-MELON-MELON"
|
||||
self.page.comments = "You might need that"
|
||||
self.page.is_cable = True
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
self.page.plug = "IEC"
|
||||
self.page.socket = "IEC"
|
||||
self.page.length = 10
|
||||
self.page.csa = "1.5"
|
||||
self.page.circuits = 1
|
||||
self.page.cores = 3
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_asset_edit(self):
|
||||
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
|
||||
|
||||
new_description = "Big Shelf"
|
||||
self.page.description = new_description
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
|
||||
|
||||
def test_asset_duplicate(self):
|
||||
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
|
||||
self.assertEqual(self.parent.description, self.page.description)
|
||||
self.assertEqual(self.parent.status.name, self.page.status)
|
||||
self.assertEqual(self.parent.category.name, self.page.category)
|
||||
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
|
||||
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
|
||||
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.assertEquals("Fullmetal Heavy Industry", self.page.name)
|
||||
new_name = "Cyberdyne Systems"
|
||||
self.page.name = new_name
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
|
||||
class TestSupplierValidation(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="SupplierValidationTest", email="SVT@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
cls.supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_create(self):
|
||||
url = reverse('supplier_create')
|
||||
response = self.client.post(url)
|
||||
self.assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
def test_edit(self):
|
||||
url = reverse('supplier_update', kwargs={'pk': self.supplier.pk})
|
||||
response = self.client.post(url, {'name': ""})
|
||||
self.assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
|
||||
class Test404(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="404Test", email="404@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test(self):
|
||||
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "0000"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
# @tag('slow') TODO: req. Django 3.0
|
||||
class TestAccessLevels(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production
|
||||
call_command('generateSampleData')
|
||||
|
||||
# Nothing should be available to the unauthenticated
|
||||
def test_unauthenticated(self):
|
||||
for url in urls.urlpatterns:
|
||||
if url.name is not None:
|
||||
pattern = str(url.pattern)
|
||||
if "json" in url.name or pattern:
|
||||
# TODO
|
||||
pass
|
||||
elif ":pk>" in pattern:
|
||||
request_url = reverse(url.name, kwargs={'pk': 9})
|
||||
else:
|
||||
request_url = reverse(url.name)
|
||||
response = self.client.get(request_url, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'login')
|
||||
|
||||
def test_basic_access(self):
|
||||
self.assertTrue(self.client.login(username="basic", password="basic"))
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = self.client.get(url)
|
||||
# Check edit and duplicate buttons not shown in list
|
||||
self.assertNotContains(response, 'Edit')
|
||||
self.assertNotContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, 'Purchase Details')
|
||||
self.assertNotContains(response, 'View Revision History')
|
||||
|
||||
urls = {'asset_history', 'asset_update', 'asset_duplicate'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "9000"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
request_url = reverse('supplier_create')
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
request_url = reverse('supplier_update', kwargs={'pk': "1"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_keyholder_access(self):
|
||||
self.assertTrue(self.client.login(username="keyholder", password="keyholder"))
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = self.client.get(url)
|
||||
# Check edit and duplicate buttons shown in list
|
||||
self.assertContains(response, 'Edit')
|
||||
self.assertContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'Purchase Details')
|
||||
self.assertContains(response, 'View Revision History')
|
||||
|
||||
# def test_finance_access(self): Level not used in assets currently
|
||||
|
||||
|
||||
class TestFormValidation(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="AssetCreateValidationTest", email="acvt@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
cls.category = models.AssetCategory.objects.create(name="Sound")
|
||||
cls.status = models.AssetStatus.objects.create(name="Broken", should_show=True)
|
||||
cls.asset = models.Asset.objects.create(asset_id="9999", description="The Office", status=cls.status, category=cls.category, date_acquired=datetime.date(2018, 6, 15))
|
||||
cls.connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
|
||||
cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, plug=cls.connector, socket=cls.connector, length=10, csa="1.5", circuits=1, cores=3)
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_asset_create(self):
|
||||
url = reverse('asset_create')
|
||||
response = self.client.post(url, {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
||||
self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
|
||||
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
def test_cable_create(self):
|
||||
url = reverse('asset_create')
|
||||
response = self.client.post(url, {'asset_id': 'X$%A', 'is_cable': True})
|
||||
self.assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
|
||||
|
||||
self.assertFormError(response, 'form', 'plug', 'A cable must have a plug')
|
||||
self.assertFormError(response, 'form', 'socket', 'A cable must have a socket')
|
||||
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
|
||||
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
|
||||
|
||||
# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway!
|
||||
def test_asset_edit(self):
|
||||
url = reverse('asset_update', kwargs={'pk': self.asset.asset_id})
|
||||
response = self.client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
# self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
|
||||
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
def test_cable_edit(self):
|
||||
url = reverse('asset_update', kwargs={'pk': self.cable_asset.asset_id})
|
||||
# TODO Why do I have to send is_cable=True here?
|
||||
response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3, 'circuits': -4, 'cores': -8})
|
||||
|
||||
# Can't figure out how to select the 'none' option...
|
||||
# self.assertFormError(response, 'form', 'plug', 'A cable must have a plug')
|
||||
# self.assertFormError(response, 'form', 'socket', 'A cable must have a socket')
|
||||
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
|
||||
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
|
||||
|
||||
def test_asset_duplicate(self):
|
||||
url = reverse('asset_duplicate', kwargs={'pk': self.cable_asset.asset_id})
|
||||
response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0, 'circuits': 0, 'cores': 0})
|
||||
|
||||
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
|
||||
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
|
||||
|
||||
|
||||
class TestSampleDataGenerator(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def test_generate_sample_data(self):
|
||||
# Run the management command and check there are no exceptions
|
||||
call_command('generateSampleAssetsData')
|
||||
|
||||
# Check there are lots
|
||||
self.assertTrue(models.Asset.objects.all().count() > 50)
|
||||
self.assertTrue(models.Supplier.objects.all().count() > 50)
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test_delete_sample_data(self):
|
||||
call_command('deleteSampleData')
|
||||
|
||||
self.assertTrue(models.Asset.objects.all().count() == 0)
|
||||
self.assertTrue(models.Supplier.objects.all().count() == 0)
|
||||
|
||||
def test_production_exception(self):
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleAssetsData')
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'deleteSampleData')
|
||||
|
||||
|
||||
class TestVersioningViews(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="VersionTest", email="version@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
|
||||
general = models.AssetCategory.objects.create(name="General")
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
cls.assets = {}
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(cls.profile)
|
||||
cls.assets[1] = models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=broken, category=lighting, date_acquired=datetime.date(1991, 12, 26))
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(cls.profile)
|
||||
cls.assets[2] = models.Asset.objects.create(asset_id="0001", description="Virgil", status=working, category=lighting, date_acquired=datetime.date(2015, 1, 1))
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(cls.profile)
|
||||
cls.assets[1].status = working
|
||||
cls.assets[1].save()
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_history_loads_successfully(self):
|
||||
request_url = reverse('asset_history', kwargs={'pk': self.assets[1].asset_id})
|
||||
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_activity_table_loads_successfully(self):
|
||||
request_url = reverse('asset_activity_table')
|
||||
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestEmbeddedViews(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="EmbeddedViewsTest", email="embedded@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
cls.assets = {
|
||||
1: models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
|
||||
def testLoginRedirect(self):
|
||||
request_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
|
||||
# Request the page and check it redirects
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
# Now login
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
# And check that it no longer redirects
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(len(response.redirect_chain), 0)
|
||||
|
||||
def testLoginCookieWarning(self):
|
||||
login_url = reverse('login_embed')
|
||||
response = self.client.post(login_url, follow=True)
|
||||
self.assertContains(response, "Cookies do not seem to be enabled")
|
||||
|
||||
def testXFrameHeaders(self):
|
||||
asset_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
login_url = reverse('login_embed')
|
||||
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
response = self.client.get(asset_url, follow=True)
|
||||
with self.assertRaises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
response = self.client.get(login_url, follow=True)
|
||||
with self.assertRaises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
def testOEmbed(self):
|
||||
asset_url = reverse('asset_detail', kwargs={'pk': self.assets[1].asset_id})
|
||||
asset_embed_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
oembed_url = reverse('asset_oembed', kwargs={'pk': self.assets[1].asset_id})
|
||||
|
||||
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
|
||||
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
|
||||
|
||||
# Test the meta tag is in place
|
||||
response = self.client.get(asset_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertContains(response, '<link rel="alternate" type="application/json+oembed"')
|
||||
self.assertContains(response, oembed_url)
|
||||
|
||||
# Test that the JSON exists
|
||||
response = self.client.get(oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, asset_embed_url)
|
||||
|
||||
# Should also work for non-existant
|
||||
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, alt_asset_embed_url)
|
||||
44
assets/urls.py
Normal file
44
assets/urls.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from assets import views, models
|
||||
from RIGS import versioning
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from PyRIGS.decorators import has_oembed, permission_required_with_403
|
||||
|
||||
urlpatterns = [
|
||||
path('', login_required(views.AssetList.as_view()), name='asset_index'),
|
||||
path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'),
|
||||
path('asset/id/<str:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
|
||||
path('asset/create/', permission_required_with_403('assets.add_asset')
|
||||
(views.AssetCreate.as_view()), name='asset_create'),
|
||||
path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset')
|
||||
(views.AssetEdit.as_view()), name='asset_update'),
|
||||
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset')
|
||||
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
||||
path('asset/id/<str:pk>/history/', permission_required_with_403('assets.view_asset')(views.AssetVersionHistory.as_view()),
|
||||
name='asset_history', kwargs={'model': models.Asset}),
|
||||
path('activity', permission_required_with_403('assets.view_asset')
|
||||
(views.ActivityTable.as_view()), name='asset_activity_table'),
|
||||
|
||||
path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'),
|
||||
path('asset/id/<str:pk>/embed/',
|
||||
xframe_options_exempt(
|
||||
login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())),
|
||||
name='asset_embed'),
|
||||
path('asset/id/<str:pk>/oembed_json/',
|
||||
views.AssetOembed.as_view(),
|
||||
name='asset_oembed'),
|
||||
|
||||
path('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
|
||||
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
|
||||
path('supplier/create', permission_required_with_403('assets.add_supplier')
|
||||
(views.SupplierCreate.as_view()), name='supplier_create'),
|
||||
path('supplier/<int:pk>/edit', permission_required_with_403('assets.change_supplier')
|
||||
(views.SupplierUpdate.as_view()), name='supplier_update'),
|
||||
path('supplier/<int:pk>/history/', views.SupplierVersionHistory.as_view(),
|
||||
name='supplier_history', kwargs={'model': models.Supplier}),
|
||||
|
||||
path('supplier/search/', views.SupplierSearch.as_view(), name='supplier_search_json'),
|
||||
]
|
||||
254
assets/views.py
Normal file
254
assets/views.py
Normal file
@@ -0,0 +1,254 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import JsonResponse
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.views import generic
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from assets import models, forms
|
||||
from RIGS import versioning
|
||||
|
||||
import simplejson
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
model = models.Asset
|
||||
template_name = 'asset_list.html'
|
||||
paginate_by = 40
|
||||
ordering = ['-pk']
|
||||
hide_hidden_status = True
|
||||
|
||||
def get_initial(self):
|
||||
initial = {'status': models.AssetStatus.objects.filter(should_show=True)}
|
||||
return initial
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.method == 'POST':
|
||||
self.form = forms.AssetSearchForm(data=self.request.POST)
|
||||
elif self.request.method == 'GET' and len(self.request.GET) > 0:
|
||||
self.form = forms.AssetSearchForm(data=self.request.GET)
|
||||
else:
|
||||
self.form = forms.AssetSearchForm(data=self.get_initial())
|
||||
form = self.form
|
||||
if not form.is_valid():
|
||||
return self.model.objects.none()
|
||||
|
||||
# TODO Feedback to user when search fails
|
||||
query_string = form.cleaned_data['query'] or ""
|
||||
if len(query_string) == 0:
|
||||
queryset = self.model.objects.all()
|
||||
elif len(query_string) >= 3:
|
||||
queryset = self.model.objects.filter(
|
||||
Q(asset_id__exact=query_string) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
|
||||
else:
|
||||
queryset = self.model.objects.filter(Q(asset_id__exact=query_string))
|
||||
|
||||
if form.cleaned_data['category']:
|
||||
queryset = queryset.filter(category__in=form.cleaned_data['category'])
|
||||
|
||||
if len(form.cleaned_data['status']) > 0:
|
||||
queryset = queryset.filter(status__in=form.cleaned_data['status'])
|
||||
elif self.hide_hidden_status:
|
||||
queryset = queryset.filter(
|
||||
status__in=models.AssetStatus.objects.filter(should_show=True))
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AssetList, self).get_context_data(**kwargs)
|
||||
context["form"] = self.form
|
||||
|
||||
context["categories"] = models.AssetCategory.objects.all()
|
||||
|
||||
context["statuses"] = models.AssetStatus.objects.all()
|
||||
return context
|
||||
|
||||
|
||||
class AssetSearch(AssetList):
|
||||
hide_hidden_status = False
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
result = []
|
||||
|
||||
for asset in context["object_list"]:
|
||||
result.append({"id": asset.pk, "label": (asset.asset_id + " | " + asset.description)})
|
||||
|
||||
return JsonResponse(result, safe=False)
|
||||
|
||||
|
||||
class AssetIDUrlMixin:
|
||||
def get_object(self, queryset=None):
|
||||
pk = self.kwargs.get(self.pk_url_kwarg)
|
||||
queryset = models.Asset.objects.filter(asset_id=pk)
|
||||
try:
|
||||
# Get the single item from the filtered queryset
|
||||
obj = queryset.get()
|
||||
except queryset.model.DoesNotExist:
|
||||
raise Http404("No assets found matching the query")
|
||||
return obj
|
||||
|
||||
|
||||
class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView):
|
||||
model = models.Asset
|
||||
template_name = 'asset_update.html'
|
||||
|
||||
|
||||
class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
|
||||
template_name = 'asset_update.html'
|
||||
model = models.Asset
|
||||
form_class = forms.AssetForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['edit'] = True
|
||||
context["connectors"] = models.Connector.objects.all()
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("asset_detail", kwargs={"pk": self.object.asset_id})
|
||||
|
||||
|
||||
class AssetCreate(LoginRequiredMixin, generic.CreateView):
|
||||
template_name = 'asset_create.html'
|
||||
model = models.Asset
|
||||
form_class = forms.AssetForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AssetCreate, self).get_context_data(**kwargs)
|
||||
context["create"] = True
|
||||
context["connectors"] = models.Connector.objects.all()
|
||||
|
||||
return context
|
||||
|
||||
def get_initial(self, *args, **kwargs):
|
||||
initial = super().get_initial(*args, **kwargs)
|
||||
initial["asset_id"] = models.Asset.get_available_asset_id()
|
||||
return initial
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("asset_detail", kwargs={"pk": self.object.asset_id})
|
||||
|
||||
|
||||
class DuplicateMixin:
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.object.pk = None
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
|
||||
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
|
||||
model = models.Asset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["create"] = None
|
||||
context["duplicate"] = True
|
||||
context['previous_asset_id'] = self.get_object().asset_id
|
||||
return context
|
||||
|
||||
|
||||
class AssetOembed(generic.View):
|
||||
model = models.Asset
|
||||
|
||||
def get(self, request, pk=None):
|
||||
embed_url = reverse('asset_embed', args=[pk])
|
||||
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
||||
|
||||
data = {
|
||||
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
||||
'version': '1.0',
|
||||
'type': 'rich',
|
||||
'height': '250'
|
||||
}
|
||||
|
||||
json = simplejson.JSONEncoderForHTML().encode(data)
|
||||
return HttpResponse(json, content_type="application/json")
|
||||
|
||||
|
||||
class AssetEmbed(AssetDetail):
|
||||
template_name = 'asset_embed.html'
|
||||
|
||||
|
||||
class SupplierList(generic.ListView):
|
||||
model = models.Supplier
|
||||
template_name = 'supplier_list.html'
|
||||
paginate_by = 40
|
||||
ordering = ['name']
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.method == 'POST':
|
||||
self.form = forms.SupplierSearchForm(data=self.request.POST)
|
||||
elif self.request.method == 'GET':
|
||||
self.form = forms.SupplierSearchForm(data=self.request.GET)
|
||||
else:
|
||||
self.form = forms.SupplierSearchForm(data={})
|
||||
form = self.form
|
||||
if not form.is_valid():
|
||||
return self.model.objects.none()
|
||||
|
||||
query_string = form.cleaned_data['query'] or ""
|
||||
if len(query_string) == 0:
|
||||
queryset = self.model.objects.all()
|
||||
else:
|
||||
queryset = self.model.objects.filter(Q(name__icontains=query_string))
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SupplierList, self).get_context_data(**kwargs)
|
||||
context["form"] = self.form
|
||||
return context
|
||||
|
||||
|
||||
class SupplierSearch(SupplierList):
|
||||
hide_hidden_status = False
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
result = []
|
||||
|
||||
for supplier in context["object_list"]:
|
||||
result.append({"id": supplier.pk, "name": supplier.name})
|
||||
return JsonResponse(result, safe=False)
|
||||
|
||||
|
||||
class SupplierDetail(generic.DetailView):
|
||||
model = models.Supplier
|
||||
template_name = 'supplier_detail.html'
|
||||
|
||||
|
||||
class SupplierCreate(generic.CreateView):
|
||||
model = models.Supplier
|
||||
form_class = forms.SupplierForm
|
||||
template_name = 'supplier_update.html'
|
||||
|
||||
|
||||
class SupplierUpdate(generic.UpdateView):
|
||||
model = models.Supplier
|
||||
form_class = forms.SupplierForm
|
||||
template_name = 'supplier_update.html'
|
||||
|
||||
|
||||
class SupplierVersionHistory(versioning.VersionHistory):
|
||||
template_name = "asset_version_history.html"
|
||||
|
||||
|
||||
class AssetVersionHistory(versioning.VersionHistory):
|
||||
template_name = "asset_version_history.html"
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
return get_object_or_404(models.Asset, asset_id=self.kwargs['pk'])
|
||||
|
||||
|
||||
class ActivityTable(versioning.ActivityTable):
|
||||
model = versioning.RIGSVersion
|
||||
template_name = "asset_activity_table.html"
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
versions = versioning.RIGSVersion.objects.get_for_multiple_models(
|
||||
[models.Asset, models.Supplier])
|
||||
return versions
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user