mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-10 22:18:25 +00:00
Compare commits
11 Commits
python_dep
...
asset_fixe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b3dca04a2 | ||
|
|
3731c5bba0 | ||
|
|
61a46fa1c2 | ||
| dea628e7a2 | |||
|
|
171d5d633e | ||
|
|
294c839bc3 | ||
|
|
73e8bc3326 | ||
|
|
5a081a97c4 | ||
|
|
1f0dc9f1ae | ||
|
|
8ad0bdf5f3 | ||
|
faa86dbe8d
|
@@ -12,14 +12,14 @@ install:
|
|||||||
- export PATH=$PATH:$(pwd)
|
- export PATH=$PATH:$(pwd)
|
||||||
- chmod +x chromedriver
|
- chmod +x chromedriver
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install coveralls codeclimate-test-reporter pycodestyle
|
- pip install coveralls codeclimate-test-reporter pep8
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- export PATH=$PATH:/usr/lib/chromium-browser/
|
- export PATH=$PATH:/usr/lib/chromium-browser/
|
||||||
- python manage.py collectstatic --noinput
|
- python manage.py collectstatic --noinput
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- pycodestyle . --exclude=migrations,importer*
|
- pep8 . --exclude=migrations,importer*
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- python manage.py makemigrations --check --dry-run
|
- python manage.py makemigrations --check --dry-run
|
||||||
- coverage run manage.py test --verbosity=2
|
- coverage run manage.py test --verbosity=2
|
||||||
|
|||||||
@@ -6,34 +6,6 @@ from django.urls import reverse
|
|||||||
from RIGS import models
|
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):
|
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.
|
Decorator for views that checks that the user passes the given test.
|
||||||
@@ -53,7 +25,11 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
|||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
elif not request.user.is_authenticated:
|
elif not request.user.is_authenticated:
|
||||||
if oembed_view is not None:
|
if oembed_view is not None:
|
||||||
return 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
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
|||||||
import os
|
import os
|
||||||
import raven
|
import raven
|
||||||
import secrets
|
import secrets
|
||||||
import datetime
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
@@ -45,11 +44,12 @@ if not DEBUG:
|
|||||||
|
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
||||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'), ('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
ADMINS = (
|
||||||
if DEBUG:
|
('Tom Price', 'tomtom5152@gmail.com')
|
||||||
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
)
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
@@ -168,8 +168,6 @@ RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', "6LeIxAcTAAAAAJcZV
|
|||||||
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
|
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
|
||||||
NOCAPTCHA = True
|
NOCAPTCHA = True
|
||||||
|
|
||||||
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
|
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAILER_TEST = False
|
EMAILER_TEST = False
|
||||||
if not DEBUG or EMAILER_TEST:
|
if not DEBUG or EMAILER_TEST:
|
||||||
@@ -184,8 +182,6 @@ if not DEBUG or EMAILER_TEST:
|
|||||||
else:
|
else:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
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(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")
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
from django.urls import path
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
@@ -16,8 +15,8 @@ urlpatterns = [
|
|||||||
url('^assets/', include('assets.urls')),
|
url('^assets/', include('assets.urls')),
|
||||||
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
|
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
|
||||||
name="registration_register"),
|
name="registration_register"),
|
||||||
path('user/', include('django.contrib.auth.urls')),
|
url('^user/', include('django.contrib.auth.urls')),
|
||||||
path('user/', include('registration.backends.default.urls')),
|
url('^user/', include('registration.backends.default.urls')),
|
||||||
|
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -1,13 +1,20 @@
|
|||||||
# TEC PA & Lighting - PyRIGS #
|
# TEC PA & Lighting - PyRIGS #
|
||||||
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|
||||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
[](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop)
|
||||||
|
[](https://gemnasium.com/github.com/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.
|
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.
|
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? ###
|
### What is this repository for? ###
|
||||||
When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -19,7 +26,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.
|
Please contact TJP for details on how to acquire these.
|
||||||
|
|
||||||
### Python Environment ###
|
### 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. Orginally written with the C implementation of Python 2 (CPython 2, specifically the Python 2.7 standard), the application now runs in Python 3.
|
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)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -108,4 +115,5 @@ python manage.py test RIGS.test_models.EventTestCase.test_current_events
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
### 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.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
@@ -22,22 +22,13 @@ admin.site.register(models.Invoice)
|
|||||||
admin.site.register(models.Payment)
|
admin.site.register(models.Payment)
|
||||||
|
|
||||||
|
|
||||||
def approve_user(modeladmin, request, queryset):
|
|
||||||
queryset.update(is_approved=True)
|
|
||||||
|
|
||||||
|
|
||||||
approve_user.short_description = "Approve selected users"
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Profile)
|
@admin.register(models.Profile)
|
||||||
class ProfileAdmin(UserAdmin):
|
class ProfileAdmin(UserAdmin):
|
||||||
# Don't know how to add 'is_approved' whilst preserving the default list...
|
|
||||||
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
(_('Personal info'), {
|
(_('Personal info'), {
|
||||||
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
||||||
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
|
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||||
'groups', 'user_permissions')}),
|
'groups', 'user_permissions')}),
|
||||||
(_('Important dates'), {
|
(_('Important dates'), {
|
||||||
'fields': ('last_login', 'date_joined')}),
|
'fields': ('last_login', 'date_joined')}),
|
||||||
@@ -50,7 +41,6 @@ class ProfileAdmin(UserAdmin):
|
|||||||
)
|
)
|
||||||
form = forms.ProfileChangeForm
|
form = forms.ProfileChangeForm
|
||||||
add_form = forms.ProfileCreationForm
|
add_form = forms.ProfileCreationForm
|
||||||
actions = [approve_user]
|
|
||||||
|
|
||||||
|
|
||||||
class AssociateAdmin(VersionAdmin):
|
class AssociateAdmin(VersionAdmin):
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from django.template.loader import get_template
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ class InvoicePrint(generic.View):
|
|||||||
|
|
||||||
pdfData = buffer.read()
|
pdfData = buffer.read()
|
||||||
|
|
||||||
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
|
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
response = HttpResponse(content_type='application/pdf')
|
||||||
response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName)
|
response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName)
|
||||||
@@ -123,34 +122,6 @@ class InvoiceArchive(generic.ListView):
|
|||||||
template_name = 'RIGS/invoice_list_archive.html'
|
template_name = 'RIGS/invoice_list_archive.html'
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
q = self.request.GET.get('q', "")
|
|
||||||
|
|
||||||
filter = Q(event__name__icontains=q)
|
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
filter = filter | Q(event__pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if q[0] == "N":
|
|
||||||
val = int(q[1:])
|
|
||||||
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
|
||||||
elif q[0] == "#":
|
|
||||||
val = int(q[1:])
|
|
||||||
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
|
|
||||||
|
|
||||||
return object_list
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceWaiting(generic.ListView):
|
class InvoiceWaiting(generic.ListView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ from django import forms
|
|||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
|
||||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||||
from registration.forms import RegistrationFormUniqueEmail
|
from registration.forms import RegistrationFormUniqueEmail
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
|
||||||
from captcha.fields import ReCaptchaField
|
from captcha.fields import ReCaptchaField
|
||||||
import simplejson
|
import simplejson
|
||||||
|
|
||||||
@@ -24,7 +22,7 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Profile
|
model = models.Profile
|
||||||
fields = ('username', 'email', 'first_name', 'last_name', 'initials')
|
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone')
|
||||||
|
|
||||||
def clean_initials(self):
|
def clean_initials(self):
|
||||||
"""
|
"""
|
||||||
@@ -35,16 +33,8 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
|||||||
return self.cleaned_data['initials']
|
return self.cleaned_data['initials']
|
||||||
|
|
||||||
|
|
||||||
class CheckApprovedForm(AuthenticationForm):
|
|
||||||
def confirm_login_allowed(self, user):
|
|
||||||
if user.is_approved or user.is_superuser:
|
|
||||||
return AuthenticationForm.confirm_login_allowed(self, user)
|
|
||||||
else:
|
|
||||||
raise forms.ValidationError("Your account hasn't been approved by an administrator yet. Please check back in a few minutes!")
|
|
||||||
|
|
||||||
|
|
||||||
# Embedded Login form - remove the autofocus
|
# Embedded Login form - remove the autofocus
|
||||||
class EmbeddedAuthenticationForm(CheckApprovedForm):
|
class EmbeddedAuthenticationForm(AuthenticationForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['username'].widget.attrs.pop('autofocus', None)
|
self.fields['username'].widget.attrs.pop('autofocus', None)
|
||||||
@@ -139,11 +129,6 @@ class EventForm(forms.ModelForm):
|
|||||||
|
|
||||||
return item
|
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):
|
def save(self, commit=True):
|
||||||
m = super(EventForm, self).save(commit=False)
|
m = super(EventForm, self).save(commit=False)
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-01-10 14:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0035_auto_20191124_1319'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='is_approved',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='last_emailed',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-01-11 18:29
|
|
||||||
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
def approve_legacy(apps, schema_editor):
|
|
||||||
Profile = apps.get_model('RIGS', 'Profile')
|
|
||||||
for person in Profile.objects.all():
|
|
||||||
person.is_approved = True
|
|
||||||
person.save()
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0036_profile_is_approved'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(approve_legacy)
|
|
||||||
]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-03-06 20:00
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0037_approve_legacy'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='event',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='invoice',
|
|
||||||
options={'ordering': ['-invoice_date']},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='organisation',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='person',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='profile',
|
|
||||||
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='venue',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -8,6 +8,7 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
from reversion.models import Version
|
from reversion.models import Version
|
||||||
import string
|
import string
|
||||||
@@ -21,12 +22,11 @@ from django.urls import reverse_lazy
|
|||||||
|
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Profile(AbstractUser):
|
class Profile(AbstractUser):
|
||||||
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
||||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
||||||
is_approved = models.BooleanField(default=False)
|
|
||||||
last_emailed = models.DateTimeField(blank=True, null=True) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_api_key(cls):
|
def make_api_key(cls):
|
||||||
@@ -53,17 +53,14 @@ class Profile(AbstractUser):
|
|||||||
def latest_events(self):
|
def latest_events(self):
|
||||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def admins(cls):
|
|
||||||
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def users_awaiting_approval_count(cls):
|
|
||||||
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_profile', 'Can view Profile'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RevisionMixin(object):
|
class RevisionMixin(object):
|
||||||
@property
|
@property
|
||||||
@@ -94,6 +91,7 @@ class RevisionMixin(object):
|
|||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Person(models.Model, RevisionMixin):
|
class Person(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||||
@@ -129,8 +127,14 @@ class Person(models.Model, RevisionMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_person', 'Can view Persons'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Organisation(models.Model, RevisionMixin):
|
class Organisation(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||||
@@ -167,6 +171,11 @@ class Organisation(models.Model, RevisionMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_organisation', 'Can view Organisations'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VatManager(models.Manager):
|
class VatManager(models.Manager):
|
||||||
def current_rate(self):
|
def current_rate(self):
|
||||||
@@ -183,6 +192,7 @@ class VatManager(models.Manager):
|
|||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class VatRate(models.Model, RevisionMixin):
|
class VatRate(models.Model, RevisionMixin):
|
||||||
start_at = models.DateField()
|
start_at = models.DateField()
|
||||||
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
||||||
@@ -203,6 +213,7 @@ class VatRate(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Venue(models.Model, RevisionMixin):
|
class Venue(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||||
@@ -225,6 +236,11 @@ class Venue(models.Model, RevisionMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_venue', 'Can view Venues'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventManager(models.Manager):
|
class EventManager(models.Manager):
|
||||||
def current_events(self):
|
def current_events(self):
|
||||||
@@ -271,6 +287,7 @@ class EventManager(models.Manager):
|
|||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
@reversion.register(follow=['items'])
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Event(models.Model, RevisionMixin):
|
class Event(models.Model, RevisionMixin):
|
||||||
# Done to make it much nicer on the database
|
# Done to make it much nicer on the database
|
||||||
PROVISIONAL = 0
|
PROVISIONAL = 0
|
||||||
@@ -464,6 +481,11 @@ class Event(models.Model, RevisionMixin):
|
|||||||
self.full_clean()
|
self.full_clean()
|
||||||
super(Event, self).save(*args, **kwargs)
|
super(Event, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_event', 'Can view Events'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventItem(models.Model):
|
class EventItem(models.Model):
|
||||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||||
@@ -501,7 +523,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
|
|||||||
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
|
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
|
||||||
account_code = models.CharField(max_length=50, blank=True, null=True)
|
account_code = models.CharField(max_length=50, blank=True, null=True)
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||||
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
sent_by = models.ForeignKey('RIGS.Profile', on_delete=models.CASCADE)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
|
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
|
||||||
@@ -511,6 +533,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
|
|||||||
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
|
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Invoice(models.Model):
|
class Invoice(models.Model):
|
||||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||||
invoice_date = models.DateField(auto_now_add=True)
|
invoice_date = models.DateField(auto_now_add=True)
|
||||||
@@ -543,9 +566,13 @@ class Invoice(models.Model):
|
|||||||
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_invoice', 'Can view Invoices'),
|
||||||
|
)
|
||||||
ordering = ['-invoice_date']
|
ordering = ['-invoice_date']
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Payment(models.Model):
|
class Payment(models.Model):
|
||||||
CASH = 'C'
|
CASH = 'C'
|
||||||
INTERNAL = 'I'
|
INTERNAL = 'I'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ def user_created(sender, user, request, **kwargs):
|
|||||||
user.first_name = form.data['first_name']
|
user.first_name = form.data['first_name']
|
||||||
user.last_name = form.data['last_name']
|
user.last_name = form.data['last_name']
|
||||||
user.initials = form.data['initials']
|
user.initials = form.data['initials']
|
||||||
# user.phone = form.data['phone']
|
user.phone = form.data['phone']
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class EventCreate(generic.CreateView):
|
|||||||
context['currentVAT'] = models.VatRate.objects.current_rate()
|
context['currentVAT'] = models.VatRate.objects.current_rate()
|
||||||
|
|
||||||
form = context['form']
|
form = context['form']
|
||||||
if re.search(r'"-\d+"', form['items_json'].value()):
|
if re.search('"-\d+"', form['items_json'].value()):
|
||||||
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
||||||
|
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||||
@@ -140,18 +140,15 @@ class EventUpdate(generic.UpdateView):
|
|||||||
if value is not None and value != '':
|
if value is not None and value != '':
|
||||||
context[field] = model.objects.get(pk=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
|
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):
|
def get_success_url(self):
|
||||||
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
@@ -206,6 +203,7 @@ class EventPrint(generic.View):
|
|||||||
}
|
}
|
||||||
|
|
||||||
rml = template.render(context)
|
rml = template.render(context)
|
||||||
|
|
||||||
buffer = rml2pdf.parseString(rml)
|
buffer = rml2pdf.parseString(rml)
|
||||||
merger.append(PdfFileReader(buffer))
|
merger.append(PdfFileReader(buffer))
|
||||||
buffer.close()
|
buffer.close()
|
||||||
@@ -218,25 +216,17 @@ class EventPrint(generic.View):
|
|||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
response = HttpResponse(content_type='application/pdf')
|
||||||
|
|
||||||
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
|
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||||
|
|
||||||
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
|
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
|
||||||
response.write(merged.getvalue())
|
response.write(merged.getvalue())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class EventArchive(generic.ListView):
|
class EventArchive(generic.ArchiveIndexView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
date_field = "start_date"
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
template_name = "RIGS/event_archive.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
# get super context
|
|
||||||
context = super(EventArchive, self).get_context_data(**kwargs)
|
|
||||||
|
|
||||||
context['start'] = self.request.GET.get('start', None)
|
|
||||||
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
start = self.request.GET.get('start', None)
|
start = self.request.GET.get('start', None)
|
||||||
@@ -248,34 +238,19 @@ class EventArchive(generic.ListView):
|
|||||||
"Muppet! Check the dates, it has been fixed for you.")
|
"Muppet! Check the dates, it has been fixed for you.")
|
||||||
start, end = end, start # Stop the impending fail
|
start, end = end, start # Stop the impending fail
|
||||||
|
|
||||||
filter = Q()
|
filter = False
|
||||||
if end != "":
|
if end != "":
|
||||||
filter &= Q(start_date__lte=end)
|
filter = Q(start_date__lte=end)
|
||||||
if start:
|
if start:
|
||||||
filter &= Q(start_date__gte=start)
|
if filter:
|
||||||
|
filter = filter & Q(start_date__gte=start)
|
||||||
|
else:
|
||||||
|
filter = Q(start_date__gte=start)
|
||||||
|
|
||||||
q = self.request.GET.get('q', "")
|
if filter:
|
||||||
|
qs = self.model.objects.filter(filter).order_by('-start_date')
|
||||||
if q is not "":
|
else:
|
||||||
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
|
qs = self.model.objects.all().order_by('-start_date')
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
qfilter = qfilter | Q(pk=val)
|
|
||||||
except: # noqa not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if q[0] == "N":
|
|
||||||
val = int(q[1:])
|
|
||||||
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
filter &= qfilter
|
|
||||||
|
|
||||||
qs = self.model.objects.filter(filter).order_by('-start_date')
|
|
||||||
|
|
||||||
# Preselect related for efficiency
|
# Preselect related for efficiency
|
||||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import datetime
|
|
||||||
import re
|
import re
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
@@ -11,9 +10,6 @@ from django.conf import settings
|
|||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from registration.signals import user_activated
|
|
||||||
from premailer import Premailer
|
from premailer import Premailer
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
|
|
||||||
@@ -73,7 +69,7 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
client_email.attach_alternative(html, 'text/html')
|
client_email.attach_alternative(html, 'text/html')
|
||||||
|
|
||||||
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
||||||
|
|
||||||
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
|
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
|
||||||
merged.getvalue(),
|
merged.getvalue(),
|
||||||
@@ -106,35 +102,3 @@ def on_revision_commit(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
|
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
|
||||||
|
|
||||||
|
|
||||||
def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|
||||||
# Bit more controlled than just emailing all superusers
|
|
||||||
for admin in models.Profile.admins():
|
|
||||||
# Check we've ever emailed them before and if so, if cooldown has passed.
|
|
||||||
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
|
|
||||||
context = {
|
|
||||||
'request': request,
|
|
||||||
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
|
|
||||||
'number_of_users': models.Profile.users_awaiting_approval_count(),
|
|
||||||
'to_name': admin.first_name
|
|
||||||
}
|
|
||||||
|
|
||||||
email = EmailMultiAlternatives(
|
|
||||||
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
|
|
||||||
get_template("RIGS/admin_awaiting_approval.txt").render(context),
|
|
||||||
to=[admin.email],
|
|
||||||
reply_to=[user.email],
|
|
||||||
)
|
|
||||||
css = staticfiles_storage.path('css/email.css')
|
|
||||||
html = Premailer(get_template("RIGS/admin_awaiting_approval.html").render(context),
|
|
||||||
external_styles=css).transform()
|
|
||||||
email.attach_alternative(html, 'text/html')
|
|
||||||
email.send()
|
|
||||||
|
|
||||||
# Update last sent
|
|
||||||
admin.last_emailed = timezone.now()
|
|
||||||
admin.save()
|
|
||||||
|
|
||||||
|
|
||||||
user_activated.connect(send_admin_awaiting_approval_email)
|
|
||||||
|
|||||||
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: 104 KiB After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 48 KiB |
@@ -65,15 +65,6 @@ textarea {
|
|||||||
overflow: hidden;
|
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 {
|
.modal-dialog {
|
||||||
z-index: inherit; // bug fix introduced in 52682ce
|
z-index: inherit; // bug fix introduced in 52682ce
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{% extends 'base_client_email.html' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<p>Hi {{ to_name|default_if_none:"Administrator" }},</p>
|
|
||||||
|
|
||||||
<p>{{ number_of_users|default_if_none:"Some" }} new users are awaiting administrator approval on RIGS. Click <a href="{{ request.scheme }}://{{ request.get_host }}{{ link_suffix }}">here</a> to approve them.</p>
|
|
||||||
|
|
||||||
<p>TEC PA & Lighting</p>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Hi {{ to_name|default_if_none:"Administrator" }},
|
|
||||||
|
|
||||||
{{ number_of_users|default_if_none:"Some" }} new users are awaiting administrator approval on RIGS. Use this link to approve them: {{ request.scheme }}://{{ request.get_host }}/{{ link_suffix }}
|
|
||||||
|
|
||||||
TEC PA & Lighting
|
|
||||||
@@ -5,49 +5,34 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<h2>Event Archive</h2>
|
||||||
<h2>Event Archive</h2>
|
|
||||||
</div>
|
<div class="col-sm-12 col-md-6 pagination">
|
||||||
<div class="col-sm-12">
|
|
||||||
<form class="form-inline">
|
<form class="form-inline">
|
||||||
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label for="start">Start</label>
|
||||||
<div class="input-group-addon">Start</div>
|
<input type="date" name="start" id="start" value="{{ request.GET.start }}" placeholder="Start" class="form-control" />
|
||||||
<input type="date" name="start" id="start" value="{{ start|default_if_none:"" }}" placeholder="Start" class="form-control" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label for="end">End</label>
|
||||||
<div class="input-group-addon">End</div>
|
<input type="date" name="end" id="end" value="{% if request.GET.end %}{{ request.GET.end }}{% else %}{% now "Y-m-d" %}{% endif %}" placeholder="End" class="form-control" />
|
||||||
<input type="date" name="end" id="end" value="{{ end|default_if_none:"" }}" placeholder="End" class="form-control" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<input type="submit" class="btn btn-primary" />
|
||||||
<div class="input-group-addon">Keyword</div>
|
|
||||||
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}"
|
|
||||||
class="form-control"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="submit" class="btn btn-primary" value="Search"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12">
|
{% if is_paginated %}
|
||||||
{% if is_paginated %}
|
<div class="col-md-6 text-right">
|
||||||
<div class="pull-right">
|
{% paginator %}
|
||||||
{% paginator %}
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
{% with latest as events %}
|
||||||
{% with object_list as events %}
|
{% include 'RIGS/event_table.html' %}
|
||||||
{% include 'RIGS/event_table.html' %}
|
{% endwith %}
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
|
|||||||
@@ -10,14 +10,12 @@
|
|||||||
| {{ object.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %}
|
| {{ object.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'RIGS/event_detail_buttons.html' %}
|
{% include 'RIGS/event_detail_buttons.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.is_rig and perms.RIGS.view_event %}
|
{% if object.is_rig %}
|
||||||
{# only need contact details for a rig #}
|
{# only need contact details for a rig #}
|
||||||
<div class="col-sm-12 col-md-6 col-lg-5">
|
<div class="col-sm-12 col-md-6 col-lg-5">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -74,7 +72,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-sm-12 {% if event.is_rig and perms.RIGS.view_event %}col-md-6 col-lg-7{% endif %}">
|
<div class="col-sm-12 {% if event.is_rig %}col-md-6 col-lg-7{% endif %}">
|
||||||
<div class="panel panel-info">
|
<div class="panel panel-info">
|
||||||
<div class="panel-heading">Event Info</div>
|
<div class="panel-heading">Event Info</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -124,7 +122,7 @@
|
|||||||
<dd> </dd>
|
<dd> </dd>
|
||||||
|
|
||||||
<dt>Event Description</dt>
|
<dt>Event Description</dt>
|
||||||
<dd class="dont-break-out">{{ event.description|linebreaksbr }}</dd>
|
<dd>{{ event.description|linebreaksbr }}</dd>
|
||||||
|
|
||||||
<dd> </dd>
|
<dd> </dd>
|
||||||
|
|
||||||
@@ -149,7 +147,7 @@
|
|||||||
<dd>{{ object.collector }}</dd>
|
<dd>{{ object.collector }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if event.is_rig and not event.internal and perms.RIGS.view_event %}
|
{% if event.is_rig and not event.internal %}
|
||||||
<dd> </dd>
|
<dd> </dd>
|
||||||
<dt>PO</dt>
|
<dt>PO</dt>
|
||||||
<dd>{{ object.purchase_order }}</dd>
|
<dd>{{ object.purchase_order }}</dd>
|
||||||
@@ -158,17 +156,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
{% if event.is_rig and event.internal %}
|
||||||
<div class="col-sm-12">
|
<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-heading">Client Authorisation</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<dl class="dl-horizontal col-sm-6">
|
<dl class="dl-horizontal col-sm-6">
|
||||||
@@ -198,7 +188,7 @@
|
|||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>Authorised at</dt>
|
<dt>Authorised at</dt>
|
||||||
<dd>{{ object.authorisation.last_edited_at|date:"D d M Y H:i" }}</dd>
|
<dd>{{ object.authorisation.last_edited_at }}</dd>
|
||||||
|
|
||||||
<dt>Authorised amount</dt>
|
<dt>Authorised amount</dt>
|
||||||
<dd>
|
<dd>
|
||||||
@@ -214,7 +204,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'RIGS/event_detail_buttons.html' %}
|
{% include 'RIGS/event_detail_buttons.html' %}
|
||||||
</div>
|
</div>
|
||||||
@@ -224,23 +214,21 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">Event Details</div>
|
<div class="panel-heading">Event Details</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<div class="well well-sm">
|
<div class="well well-sm">
|
||||||
<h4>Notes</h4>
|
<h4>Notes</h4>
|
||||||
<div class="dont-break-out">{{ event.notes|linebreaksbr }}</div>
|
{{ event.notes|linebreaksbr }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% include 'RIGS/item_table.html' %}
|
{% include 'RIGS/item_table.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'RIGS/event_detail_buttons.html' %}
|
{% include 'RIGS/event_detail_buttons.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'event_history' object.pk %}" title="View Revision History">
|
<a href="{% url 'event_history' object.pk %}" title="View Revision History">
|
||||||
@@ -255,16 +243,12 @@
|
|||||||
{% if request.is_ajax %}
|
{% if request.is_ajax %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<div class="col-sm-10 align-left">
|
<div class="col-sm-10 align-left">
|
||||||
<a href="{% url 'event_history' object.pk %}" title="View Revision History">
|
<a href="{% url 'event_history' object.pk %}" title="View Revision History">
|
||||||
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
|
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-2">
|
||||||
{% else %}
|
|
||||||
<div class="col-sm-12">
|
|
||||||
{% endif %}
|
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url 'event_detail' object.pk %}" class="btn btn-primary">Open Event Page <span
|
<a href="{% url 'event_detail' object.pk %}" class="btn btn-primary">Open Event Page <span
|
||||||
class="glyphicon glyphicon-eye"></span></a>
|
class="glyphicon glyphicon-eye"></span></a>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{% extends 'base_embed.html' %}
|
{% extends 'base_embed.html' %}
|
||||||
{% load static %}
|
{% load static from staticfiles %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
|
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -19,9 +20,9 @@
|
|||||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
<a href="{% url 'event_detail' object.pk %}">
|
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' object.pk %}"{% endif %}>
|
||||||
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
|
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
|
||||||
| {{ object.name }} </a>
|
| {{ object.name }} </a>
|
||||||
{% if object.venue %}
|
{% if object.venue %}
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-6">
|
<div class="col-xs-6">
|
||||||
|
|
||||||
{% if object.meet_at %}
|
{% if object.meet_at %}
|
||||||
<p>
|
<p>
|
||||||
<strong>Crew meet:</strong>
|
<strong>Crew meet:</strong>
|
||||||
@@ -95,7 +97,10 @@
|
|||||||
{{ object.description|linebreaksbr }}
|
{{ object.description|linebreaksbr }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% load filters %}
|
|
||||||
<setNextFrame name="main"/>
|
<setNextFrame name="main"/>
|
||||||
<nextFrame/>
|
<nextFrame/>
|
||||||
|
|
||||||
|
|
||||||
<blockTable style="headLayout" colWidths="330,165">
|
<blockTable style="headLayout" colWidths="330,165">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
|
|
||||||
<keepInFrame>
|
<keepInFrame>
|
||||||
<para style="style.event_description">
|
<para style="style.event_description">
|
||||||
{{ object.description|default_if_none:""|linebreaksxml }}
|
{{ object.description|default_if_none:""|linebreaksbr }}
|
||||||
</para>
|
</para>
|
||||||
</keepInFrame>
|
</keepInFrame>
|
||||||
</td>
|
</td>
|
||||||
@@ -74,9 +75,9 @@
|
|||||||
{% if invoice %}
|
{% if invoice %}
|
||||||
<keepInFrame>
|
<keepInFrame>
|
||||||
{% if object.organisation.address %}
|
{% if object.organisation.address %}
|
||||||
<para style="specific_description">{{ object.organisation.address|default_if_none:""|linebreaksxml }}</para>
|
<para style="specific_description">{{ object.organisation.address|default_if_none:""|linebreaksbr }}</para>
|
||||||
{% elif object.person.address %}
|
{% elif object.person.address %}
|
||||||
<para style="specific_description">{{ object.person.address|default_if_none:""|linebreaksxml }}</para>
|
<para style="specific_description">{{ object.person.address|default_if_none:""|linebreaksbr }}</para>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</keepInFrame>
|
</keepInFrame>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -108,12 +109,12 @@
|
|||||||
<h3>{{ object.venue.name }}</h3>
|
<h3>{{ object.venue.name }}</h3>
|
||||||
{% if not invoice %}
|
{% if not invoice %}
|
||||||
<keepInFrame>
|
<keepInFrame>
|
||||||
<para style="specific_description">{{ object.venue.address|default_if_none:""|linebreaksxml }}</para>
|
<para style="specific_description">{{ object.venue.address|default_if_none:""|linebreaksbr }}</para>
|
||||||
</keepInFrame>
|
</keepInFrame>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td rightPadding="0">
|
<td rightPadding="0">
|
||||||
|
|
||||||
<h2>Timings</h2>
|
<h2>Timings</h2>
|
||||||
<blockTable style="eventDetails" colWidths="55,75">
|
<blockTable style="eventDetails" colWidths="55,75">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -184,7 +185,7 @@
|
|||||||
{% if item.description %}
|
{% if item.description %}
|
||||||
</para>
|
</para>
|
||||||
<para style="item_description">
|
<para style="item_description">
|
||||||
<em>{{ item.description|linebreaksxml }}</em>
|
<em>{{ item.description|linebreaksbr }}</em>
|
||||||
</para>
|
</para>
|
||||||
<para>
|
<para>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<h4>
|
<h4>
|
||||||
<a href="{% url 'event_detail' event.pk %}">
|
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' event.pk %}" {% endif %}>
|
||||||
{{ event.name }}
|
{{ event.name }}
|
||||||
</a>
|
</a>
|
||||||
{% if event.venue %}
|
{% if event.venue %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
|
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
|
||||||
for <b>£{{ object.amount }}</b>
|
for <b>£{{ object.amount }}</b>
|
||||||
by <b>{{ object.name }}</b> as of <b>{{ object.event.last_edited_at }}</b>.
|
by <b>{{ object.name }}</b> as of <b>{{ object.last_edited_at }}</b>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Hi {{ to_name|default_if_none:"there" }},
|
Hi {{ to_name|default:"there" }},
|
||||||
|
|
||||||
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}}.
|
Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
|
||||||
|
|
||||||
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
|
{% 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.
|
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"}},
|
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.event.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.last_edited_at}}.
|
||||||
|
|
||||||
The TEC Rig Information Gathering System
|
The TEC Rig Information Gathering System
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% block title %}RIGS{% endblock %}
|
{% block title %}RIGS{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
<script>
|
|
||||||
$(function () {
|
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
||||||
@@ -19,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-{% if perms.RIGS.view_event %}6{% else %}12{% endif %}">
|
<div class="col-sm-{% if perms.RIGS.view_event %}6{% else %}12{% endif %}">
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="list-group-item-heading">Quick Links</h4>
|
<h4 class="list-group-item-heading">Quick Links</h4>
|
||||||
@@ -34,51 +26,44 @@
|
|||||||
|
|
||||||
<a class="list-group-item" href="https://forum.nottinghamtec.co.uk" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
|
<a class="list-group-item" href="https://forum.nottinghamtec.co.uk" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
|
||||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
|
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<a class="list-group-item" href="http://members.nottinghamtec.co.uk/wiki/images/2/22/Event_Risk_Assesment.pdf" target="_blank"><span class="glyphicon glyphicon-link"></span> Pre-Event Risk Assessment</a>
|
<a class="list-group-item" href="http://members.nottinghamtec.co.uk/wiki/images/2/22/Event_Risk_Assesment.pdf" target="_blank"><span class="glyphicon glyphicon-link"></span> Pre-Event Risk Assessment</a>
|
||||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a>
|
<a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a>
|
||||||
<a class="list-group-item" href="https://goo.gl/forms/jdPWov8PCNPoXtbn2" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a>
|
<a class="list-group-item" href="https://goo.gl/forms/jdPWov8PCNPoXtbn2" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Search Rigboard
|
<h4 class="panel-title">Search Rigboard</h4>
|
||||||
<a href="{% url 'search_help' %}" class="pull-right modal-href"><span class="glyphicon glyphicon-question-sign"</span></a></h4>
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
$(document).ready(function(){
|
|
||||||
$('#search-options li a').click(function(){
|
|
||||||
$('#searchForm').attr('action', $(this).data('action')).submit();
|
|
||||||
});
|
|
||||||
$('#id_search_input').keypress(function (e) {
|
|
||||||
if (e.which == 13) {
|
|
||||||
$('#searchForm').attr('action', $('#search-options li a').first().data('action')).submit();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<form id="searchForm" class="form" role="form" method="GET">
|
<form class="form" role="form" action="{% url 'person_list' %}" method="GET">
|
||||||
<div class="input-group" data-toggle="tooltip" title="Use the dropdown button to select what to search. The default is Event Archive.">
|
<div class="input-group">
|
||||||
<input id="id_search_input" type="search" name="q" class="form-control" placeholder="Search..." />
|
<input type="search" name="q" class="form-control" placeholder="Search People" />
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<div class="btn-group" role="group">
|
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
|
||||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-search"></span> Search <span class="caret"></span></button>
|
</span>
|
||||||
<ul id="search-options" class="dropdown-menu">
|
</div>
|
||||||
<li><a data-action="{% url 'event_archive' %}" href="#">Events</a></li>
|
</form>
|
||||||
<li><a data-action="{% url 'person_list' %}" href="#">People</a></li>
|
</div>
|
||||||
<li><a data-action="{% url 'organisation_list' %}" href="#">Organisations</a></li>
|
<div class="list-group-item">
|
||||||
<li><a data-action="{% url 'venue_list' %}" href="#">Venues</a></li>
|
<form class="form" role="form" action="{% url 'organisation_list' %}" method="GET">
|
||||||
{% if perms.RIGS.view_invoice %}
|
<div class="input-group">
|
||||||
<li><a data-action="{% url 'invoice_archive' %}" href="#">Invoices</a></li>
|
<input type="search" name="q" class="form-control" placeholder="Search Organisations" />
|
||||||
{% endif %}
|
<span class="input-group-btn">
|
||||||
</ul>
|
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item">
|
||||||
|
<form class="form" role="form" action="{% url 'venue_list' %}" method="GET">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="search" name="q" class="form-control" placeholder="Search Venues" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -87,8 +72,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if perms.RIGS.view_event %}
|
{% if perms.RIGS.view_event %}
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6" >
|
||||||
{% include 'RIGS/activity_feed.html' %}
|
{% include 'RIGS/activity_feed.html' %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>Authorisation request sent by</dt>
|
<dt>Authorsation request sent by</dt>
|
||||||
<dd>{{ object.authorisation.sent_by }}</dd>
|
<dd>{{ object.authorisation.sent_by }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
{% paginator %}
|
{% paginator %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block search %}{% endblock %}
|
|
||||||
<div class="table-responsive col-sm-12">
|
<div class="table-responsive col-sm-12">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -10,15 +10,4 @@ All Invoices
|
|||||||
|
|
||||||
{% block description %}
|
{% block description %}
|
||||||
<p>This page displays all invoices: outstanding, paid, and void</p>
|
<p>This page displays all invoices: outstanding, paid, and void</p>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block search %}
|
|
||||||
<div class="col-sm-3 col-sm-offset-9">
|
|
||||||
<form class="form form-horizontal col-sm-12">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="search" name="q" placeholder="Search" value="{{ request.GET.q }}"
|
|
||||||
class="form-control"/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="modal fade" id="itemModal" role="dialog" aria-labelledby="itemModal" aria-hidden="true">
|
<div class="modal fade" id="itemModal" role="dialog" aria-labelledby="itemModal" aria-hidded="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="item_quantity" class="col-sm-4 control-label">Quantity</label>
|
<label for="item_quantity" class="col-sm-4 control-label">Quantity</label>
|
||||||
@@ -70,4 +71,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -6,21 +6,17 @@
|
|||||||
<em class="description">{{item.description|linebreaksbr}}</em>
|
<em class="description">{{item.description|linebreaksbr}}</em>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<td>£ <span class="cost">{{item.cost|floatformat:2}}</span></td>
|
<td>£ <span class="cost">{{item.cost|floatformat:2}}</span></td>
|
||||||
{% endif %}
|
|
||||||
<td class="quantity">{{item.quantity}}</td>
|
<td class="quantity">{{item.quantity}}</td>
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<td>£ <span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
|
<td>£ <span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
|
||||||
{% endif %}
|
|
||||||
{% if edit %}
|
{% if edit %}
|
||||||
<td class="vert-align text-right">
|
<td class="vert-align text-right">
|
||||||
<button type="button" class="item-edit btn btn-xs btn-default"
|
<button type="button" class="item-edit btn btn-xs btn-default"
|
||||||
data-pk="{{item.pk}}"
|
data-pk="{{item.pk}}"
|
||||||
data-toggle="modal" data-target="#itemModal">
|
data-toggle="modal" data-target="#itemModal">
|
||||||
<span class="glyphicon glyphicon-edit"></span>
|
<span class="glyphicon glyphicon-edit"></span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="item-delete btn btn-xs btn-danger"
|
<button type="button" class="item-delete btn btn-xs btn-danger"
|
||||||
data-pk="{{item.pk}}">
|
data-pk="{{item.pk}}">
|
||||||
<span class="glyphicon glyphicon-remove"></span>
|
<span class="glyphicon glyphicon-remove"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,17 +3,13 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Item</td>
|
<td>Item</td>
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<td>Price</td>
|
<td>Price</td>
|
||||||
{% endif %}
|
|
||||||
<td>Quantity</td>
|
<td>Quantity</td>
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<td>Sub-total</td>
|
<td>Sub-total</td>
|
||||||
{% endif %}
|
|
||||||
{% if edit %}
|
{% if edit %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<button type="button" class="btn btn-default btn-xs item-add"
|
<button type="button" class="btn btn-default btn-xs item-add"
|
||||||
data-toggle="modal"
|
data-url="{#% url eventitem_add object.pk %#}" data-toggle="modal"
|
||||||
data-target="#itemModal">
|
data-target="#itemModal">
|
||||||
<span class="glyphicon glyphicon-plus"></span>
|
<span class="glyphicon glyphicon-plus"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -26,7 +22,6 @@
|
|||||||
{% include 'RIGS/item_row.html' %}
|
{% include 'RIGS/item_row.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="3" colspan="2"></td>
|
<td rowspan="3" colspan="2"></td>
|
||||||
@@ -48,7 +43,6 @@
|
|||||||
<td colspan="2">£ <span id="total">{{object.total|default:0|floatformat:2}}</span></td>
|
<td colspan="2">£ <span id="total">{{object.total|default:0|floatformat:2}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
{% endif %}
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<table class="hidden invisible">
|
<table class="hidden invisible">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
{% block title %}Organisation | {{ object.name }}{% endblock %}
|
{% block title %}Organisation | {{ object.name }}{% endblock %}
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Search Help{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
{% if not request.is_ajax %}
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<h1>Search Help</h1>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3 class="panel-title">Searching Events</h3>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<p>
|
|
||||||
Searches for entire query in:
|
|
||||||
<button type="button" class="btn btn-default btn-xs">name</button>
|
|
||||||
<button type="button" class="btn btn-default btn-xs">description</button> and
|
|
||||||
<button type="button" class="btn btn-default btn-xs">notes</button>
|
|
||||||
</p>
|
|
||||||
<p>You can search for an event by <button type="button" class="btn btn-default btn-xs">event_id</button> by entering an integer, or using the format <code>N01234</code></p>
|
|
||||||
<p>On the search results page you can also specify the date range for the <button type="button" class="btn btn-default btn-xs">start_date</button> of the event</p>
|
|
||||||
<p>Events are sorted in reverse <button type="button" class="btn btn-default btn-xs">start_date</button> order (most recent events at the top)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3 class="panel-title">Searching People/Organisations/Venues</h3>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<p>
|
|
||||||
Searches for entire search phrase in:
|
|
||||||
<button type="button" class="btn btn-default btn-xs">name</button>
|
|
||||||
<button type="button" class="btn btn-default btn-xs">email</button>
|
|
||||||
<button type="button" class="btn btn-default btn-xs">address</button>
|
|
||||||
<button type="button" class="btn btn-default btn-xs">notes</button> and
|
|
||||||
<button type="button" class="btn btn-default btn-xs">phone</button>
|
|
||||||
</p>
|
|
||||||
<p>You can search for an entry by <button type="button" class="btn btn-default btn-xs">id</button> by entering an integer</p>
|
|
||||||
<p>Entries are sorted in alphabetical order by <button type="button" class="btn btn-default btn-xs">name</button></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if perms.RIGS.view_invoice %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3 class="panel-title">Searching Invoices</h3>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<p>
|
|
||||||
Searches for entire search phrase in:
|
|
||||||
<button type="button" class="btn btn-default btn-xs">event__name</button>
|
|
||||||
</p>
|
|
||||||
<p>You can search for an event's invoice by entering the <button type="button" class="btn btn-default btn-xs">event_id</button> using the format <code>N01234</code></p>
|
|
||||||
<p>You can search for an invoice by <button type="button" class="btn btn-default btn-xs">invoice_id</button> using the format <code>#01234</code></p>
|
|
||||||
<p>Entering a raw integer will search by both <button type="button" class="btn btn-default btn-xs">invoice_id</button> and <button type="button" class="btn btn-default btn-xs">event_id</button></p>
|
|
||||||
<p>Entries are sorted in reverse <button type="button" class="btn btn-default btn-xs">invoice_date</button> order</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
{% for change in itemChange.field_changes %}
|
{% for change in itemChange.field_changes %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
|
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
|
||||||
<div class="dont-break-out">{% include "RIGS/version_changes_change.html" %}</div>
|
{% include "RIGS/version_changes_change.html" %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -23,4 +23,4 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
nothing useful
|
nothing useful
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
{% if change.linebreaks and change.new and change.old %}
|
{% if change.linebreaks and change.new and change.old %}
|
||||||
{% for diff in change.diff %}
|
{% for diff in change.diff %}
|
||||||
{% if diff.type == "insert" %}
|
{% if diff.type == "insert" %}
|
||||||
<ins class="dont-break-out">{{ diff.text|linebreaksbr }}</ins>
|
<ins>{{ diff.text|linebreaksbr }}</ins>
|
||||||
{% elif diff.type == "delete" %}
|
{% elif diff.type == "delete" %}
|
||||||
<del class="dont-break-out">{{diff.text|linebreaksbr}}</del>
|
<del>{{diff.text|linebreaksbr}}</del>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="dont-break-out">{{diff.text|linebreaksbr}}</span>
|
<span>{{diff.text|linebreaksbr}}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -2,24 +2,10 @@ from django import template
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.forms import NON_FIELD_ERRORS
|
from django.forms.forms import NON_FIELD_ERRORS
|
||||||
from django.forms.utils import ErrorDict
|
from django.forms.utils import ErrorDict
|
||||||
from django.utils.text import normalize_newlines
|
|
||||||
from django.template.defaultfilters import stringfilter
|
|
||||||
from django.utils.safestring import SafeData, mark_safe
|
|
||||||
from django.utils.html import escape
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.filter(is_safe=True, needs_autoescape=True)
|
|
||||||
@stringfilter
|
|
||||||
def linebreaksxml(value, autoescape=True):
|
|
||||||
autoescape = autoescape and not isinstance(value, SafeData)
|
|
||||||
value = normalize_newlines(value)
|
|
||||||
if autoescape:
|
|
||||||
value = escape(value)
|
|
||||||
return mark_safe(value.replace('\n', '<br />'))
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def multiply(value, arg):
|
def multiply(value, arg):
|
||||||
return value * arg
|
return value * arg
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import pytz
|
||||||
from datetime import date, time, datetime, timedelta
|
from datetime import date, time, datetime, timedelta
|
||||||
|
|
||||||
import pytz
|
|
||||||
from django.conf import settings
|
from django.core import mail
|
||||||
from django.core import mail, signing
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from django.test import LiveServerTestCase, TestCase
|
from django.test import LiveServerTestCase, TestCase
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
from django.urls import reverse
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.common.exceptions import StaleElementReferenceException
|
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
|
||||||
from selenium.webdriver.support import expected_conditions
|
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
|
||||||
@@ -23,12 +20,23 @@ from RIGS import models
|
|||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core import mail, signing
|
from django.core import mail, signing
|
||||||
from PyRIGS.tests.base import create_browser
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
import sys
|
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):
|
class UserRegistrationTest(LiveServerTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.browser = create_browser()
|
self.browser = create_browser()
|
||||||
@@ -66,9 +74,8 @@ class UserRegistrationTest(LiveServerTestCase):
|
|||||||
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
||||||
initials = self.browser.find_element_by_id('id_initials')
|
initials = self.browser.find_element_by_id('id_initials')
|
||||||
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
||||||
# No longer required for new users
|
phone = self.browser.find_element_by_id('id_phone')
|
||||||
# phone = self.browser.find_element_by_id('id_phone')
|
self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
||||||
# self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
|
||||||
|
|
||||||
# Fill the form out incorrectly
|
# Fill the form out incorrectly
|
||||||
username.send_keys('TestUsername')
|
username.send_keys('TestUsername')
|
||||||
@@ -79,7 +86,7 @@ class UserRegistrationTest(LiveServerTestCase):
|
|||||||
first_name.send_keys('John')
|
first_name.send_keys('John')
|
||||||
last_name.send_keys('Smith')
|
last_name.send_keys('Smith')
|
||||||
initials.send_keys('JS')
|
initials.send_keys('JS')
|
||||||
# phone.send_keys('0123456789')
|
phone.send_keys('0123456789')
|
||||||
self.browser.execute_script(
|
self.browser.execute_script(
|
||||||
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
||||||
|
|
||||||
@@ -94,8 +101,7 @@ class UserRegistrationTest(LiveServerTestCase):
|
|||||||
# Read what the error is
|
# Read what the error is
|
||||||
alert = self.browser.find_element_by_css_selector(
|
alert = self.browser.find_element_by_css_selector(
|
||||||
'div.alert-danger').text
|
'div.alert-danger').text
|
||||||
# TODO Use regex matching to handle smart/unsmart quotes...
|
self.assertIn("password fields didn't match", alert)
|
||||||
self.assertIn("password fields didn", alert)
|
|
||||||
|
|
||||||
# Passwords should be empty
|
# Passwords should be empty
|
||||||
self.assertEqual(password1.get_attribute('value'), '')
|
self.assertEqual(password1.get_attribute('value'), '')
|
||||||
@@ -122,7 +128,7 @@ class UserRegistrationTest(LiveServerTestCase):
|
|||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertIn('John Smith "JS" activation required', email.subject)
|
self.assertIn('John Smith "JS" activation required', email.subject)
|
||||||
urls = re.findall(
|
urls = re.findall(
|
||||||
r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
|
'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
|
||||||
self.assertEqual(len(urls), 1)
|
self.assertEqual(len(urls), 1)
|
||||||
|
|
||||||
mail.outbox = [] # empty this for later
|
mail.outbox = [] # empty this for later
|
||||||
@@ -142,46 +148,23 @@ class UserRegistrationTest(LiveServerTestCase):
|
|||||||
self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
||||||
self.assertEqual(password.get_attribute('type'), 'password')
|
self.assertEqual(password.get_attribute('type'), 'password')
|
||||||
|
|
||||||
# Expected to fail as not approved
|
|
||||||
username.send_keys('TestUsername')
|
username.send_keys('TestUsername')
|
||||||
password.send_keys('correcthorsebatterystaple')
|
password.send_keys('correcthorsebatterystaple')
|
||||||
self.browser.execute_script(
|
self.browser.execute_script(
|
||||||
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
||||||
password.send_keys(Keys.ENTER)
|
password.send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Test approval
|
|
||||||
profileObject = models.Profile.objects.all()[0]
|
|
||||||
self.assertFalse(profileObject.is_approved)
|
|
||||||
|
|
||||||
# Read what the error is
|
|
||||||
alert = self.browser.find_element_by_css_selector(
|
|
||||||
'div.alert-danger').text
|
|
||||||
self.assertIn("approved", alert)
|
|
||||||
|
|
||||||
# Approve the user so we can proceed
|
|
||||||
profileObject.is_approved = True
|
|
||||||
profileObject.save()
|
|
||||||
|
|
||||||
# Retry login
|
|
||||||
self.browser.get(self.live_server_url + '/user/login')
|
|
||||||
username = self.browser.find_element_by_id('id_username')
|
|
||||||
username.send_keys('TestUsername')
|
|
||||||
password = self.browser.find_element_by_id('id_password')
|
|
||||||
password.send_keys('correcthorsebatterystaple')
|
|
||||||
self.browser.execute_script(
|
|
||||||
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
|
|
||||||
password.send_keys(Keys.ENTER)
|
|
||||||
|
|
||||||
# Check we are logged in
|
# Check we are logged in
|
||||||
udd = self.browser.find_element_by_class_name('navbar').text
|
udd = self.browser.find_element_by_class_name('navbar').text
|
||||||
self.assertIn('Hi John', udd)
|
self.assertIn('Hi John', udd)
|
||||||
|
|
||||||
# Check all the data actually got saved
|
# Check all the data actually got saved
|
||||||
|
profileObject = models.Profile.objects.all()[0]
|
||||||
self.assertEqual(profileObject.username, 'TestUsername')
|
self.assertEqual(profileObject.username, 'TestUsername')
|
||||||
self.assertEqual(profileObject.first_name, 'John')
|
self.assertEqual(profileObject.first_name, 'John')
|
||||||
self.assertEqual(profileObject.last_name, 'Smith')
|
self.assertEqual(profileObject.last_name, 'Smith')
|
||||||
self.assertEqual(profileObject.initials, 'JS')
|
self.assertEqual(profileObject.initials, 'JS')
|
||||||
# self.assertEqual(profileObject.phone, '0123456789')
|
self.assertEqual(profileObject.phone, '0123456789')
|
||||||
self.assertEqual(profileObject.email, 'test@example.com')
|
self.assertEqual(profileObject.email, 'test@example.com')
|
||||||
|
|
||||||
# All is well
|
# All is well
|
||||||
@@ -236,236 +219,254 @@ class EventTest(LiveServerTestCase):
|
|||||||
self.browser.get(self.live_server_url + '/rigboard/')
|
self.browser.get(self.live_server_url + '/rigboard/')
|
||||||
|
|
||||||
def testRigCreate(self):
|
def testRigCreate(self):
|
||||||
# Requests address
|
try:
|
||||||
self.browser.get(self.live_server_url + '/event/create/')
|
# Requests address
|
||||||
# Gets redirected to login and back
|
self.browser.get(self.live_server_url + '/event/create/')
|
||||||
self.authenticate('/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
|
# Check has slided up correctly - second save button hidden
|
||||||
save = self.browser.find_element_by_xpath(
|
save = self.browser.find_element_by_xpath(
|
||||||
'(//button[@type="submit"])[3]')
|
'(//button[@type="submit"])[3]')
|
||||||
self.assertFalse(save.is_displayed())
|
self.assertFalse(save.is_displayed())
|
||||||
|
|
||||||
# Click Rig button
|
# Click Rig button
|
||||||
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
||||||
|
|
||||||
# Slider expands and save button visible
|
# Slider expands and save button visible
|
||||||
self.assertTrue(save.is_displayed())
|
self.assertTrue(save.is_displayed())
|
||||||
form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
|
|
||||||
# For now, just check that HTML5 Client validation is in place TODO Test needs rewriting to properly test all levels of validation.
|
# Create new person
|
||||||
self.assertTrue(self.browser.find_element_by_id('id_name').get_attribute('required') is not None)
|
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()
|
||||||
|
|
||||||
# Set title
|
# See modal has opened
|
||||||
e = self.browser.find_element_by_id('id_name')
|
modal = self.browser.find_element_by_id('modal')
|
||||||
e.send_keys('Test Event Name')
|
wait.until(animation_is_finished())
|
||||||
|
self.assertTrue(modal.is_displayed())
|
||||||
|
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
|
||||||
|
|
||||||
# Create new person
|
# Fill person form out and submit
|
||||||
wait.until(animation_is_finished())
|
modal.find_element_by_xpath(
|
||||||
add_person_button = self.browser.find_element_by_xpath(
|
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
|
||||||
'//a[@data-target="#id_person" and contains(@href, "add")]')
|
modal.find_element_by_xpath(
|
||||||
add_person_button.click()
|
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||||
|
wait.until(animation_is_finished())
|
||||||
|
self.assertFalse(modal.is_displayed())
|
||||||
|
|
||||||
# See modal has opened
|
# See new person selected
|
||||||
modal = self.browser.find_element_by_id('modal')
|
person1 = models.Person.objects.get(name="Test Person 1")
|
||||||
wait.until(animation_is_finished())
|
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||||
self.assertTrue(modal.is_displayed())
|
'//button[@data-id="id_person"]/span').text)
|
||||||
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').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")))
|
||||||
|
|
||||||
# Fill person form out and submit
|
# Change mind and add another
|
||||||
modal.find_element_by_xpath(
|
wait.until(animation_is_finished())
|
||||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
|
add_person_button.click()
|
||||||
modal.find_element_by_xpath(
|
|
||||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
|
||||||
wait.until(animation_is_finished())
|
|
||||||
self.assertFalse(modal.is_displayed())
|
|
||||||
|
|
||||||
# See new person selected
|
wait.until(animation_is_finished())
|
||||||
person1 = models.Person.objects.get(name="Test Person 1")
|
self.assertTrue(modal.is_displayed())
|
||||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
|
||||||
'//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")))
|
|
||||||
|
|
||||||
# Change mind and add another
|
modal.find_element_by_xpath(
|
||||||
wait.until(animation_is_finished())
|
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
|
||||||
add_person_button.click()
|
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())
|
person2 = models.Person.objects.get(name="Test Person 2")
|
||||||
self.assertTrue(modal.is_displayed())
|
self.assertEqual(person2.name, form.find_element_by_xpath(
|
||||||
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
|
'//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")))
|
||||||
|
|
||||||
modal.find_element_by_xpath(
|
# Was right the first time, change it back
|
||||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
|
person_select = form.find_element_by_xpath(
|
||||||
modal.find_element_by_xpath(
|
'//button[@data-id="id_person"]')
|
||||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
person_select.send_keys(person1.name)
|
||||||
wait.until(animation_is_finished())
|
person_dropped = form.find_element_by_xpath(
|
||||||
self.assertFalse(modal.is_displayed())
|
'//ul[contains(@class, "inner selectpicker")]//span[contains(text(), "%s")]' % person1.name)
|
||||||
|
person_dropped.click()
|
||||||
|
|
||||||
person2 = models.Person.objects.get(name="Test Person 2")
|
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||||
self.assertEqual(person2.name, form.find_element_by_xpath(
|
'//button[@data-id="id_person"]/span').text)
|
||||||
'//button[@data-id="id_person"]/span').text)
|
option = form.find_element_by_xpath(
|
||||||
# Have to do this explcitly to force the wait for it to update
|
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||||
option = form.find_element_by_xpath(
|
self.assertEqual(person1.pk, int(option.get_attribute("value")))
|
||||||
'//select[@id="id_person"]//option[@selected="selected"]')
|
|
||||||
self.assertEqual(person2.pk, int(option.get_attribute("value")))
|
|
||||||
|
|
||||||
# Was right the first time, change it back
|
# Edit Person 1 to have a better name
|
||||||
person_select = form.find_element_by_xpath(
|
form.find_element_by_xpath(
|
||||||
'//button[@data-id="id_person"]')
|
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
|
||||||
person_select.send_keys(person1.name)
|
wait.until(animation_is_finished())
|
||||||
person_dropped = form.find_element_by_xpath(
|
self.assertTrue(modal.is_displayed())
|
||||||
'//ul[contains(@class, "dropdown-menu")]//span[contains(text(), "%s")]' % person1.name)
|
self.assertIn("Edit Person", modal.find_element_by_tag_name('h3').text)
|
||||||
person_dropped.click()
|
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)
|
||||||
|
|
||||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
wait.until(animation_is_finished())
|
||||||
'//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")))
|
|
||||||
|
|
||||||
# Edit Person 1 to have a better name
|
self.assertFalse(modal.is_displayed())
|
||||||
form.find_element_by_xpath(
|
person1 = models.Person.objects.get(pk=person1.pk)
|
||||||
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
|
self.assertEqual(person1.name, form.find_element_by_xpath(
|
||||||
wait.until(animation_is_finished())
|
'//button[@data-id="id_person"]/span').text)
|
||||||
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)
|
|
||||||
|
|
||||||
wait.until(animation_is_finished())
|
# 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()
|
||||||
|
|
||||||
self.assertFalse(modal.is_displayed())
|
# See it is selected
|
||||||
person1 = models.Person.objects.get(pk=person1.pk)
|
wait.until(animation_is_finished())
|
||||||
self.assertEqual(person1.name, form.find_element_by_xpath(
|
self.assertFalse(modal.is_displayed())
|
||||||
'//button[@data-id="id_person"]/span').text)
|
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")))
|
||||||
|
|
||||||
# Create organisation
|
# Create venue
|
||||||
wait.until(animation_is_finished())
|
wait.until(animation_is_finished())
|
||||||
add_button = self.browser.find_element_by_xpath(
|
add_button = self.browser.find_element_by_xpath(
|
||||||
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
|
'//a[@data-target="#id_venue" and contains(@href, "add")]')
|
||||||
add_button.click()
|
wait.until(animation_is_finished())
|
||||||
modal = self.browser.find_element_by_id('modal')
|
add_button.click()
|
||||||
wait.until(animation_is_finished())
|
wait.until(animation_is_finished())
|
||||||
self.assertTrue(modal.is_displayed())
|
modal = self.browser.find_element_by_id('modal')
|
||||||
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
|
wait.until(animation_is_finished())
|
||||||
modal.find_element_by_xpath(
|
self.assertTrue(modal.is_displayed())
|
||||||
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
|
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
|
||||||
modal.find_element_by_xpath(
|
modal.find_element_by_xpath(
|
||||||
'//div[@id="modal"]//input[@type="submit"]').click()
|
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
|
||||||
|
modal.find_element_by_xpath(
|
||||||
|
'//div[@id="modal"]//input[@type="submit"]').click()
|
||||||
|
|
||||||
# See it is selected
|
# See it is selected
|
||||||
wait.until(animation_is_finished())
|
wait.until(animation_is_finished())
|
||||||
self.assertFalse(modal.is_displayed())
|
self.assertFalse(modal.is_displayed())
|
||||||
obj = models.Organisation.objects.get(name="Test Organisation")
|
obj = models.Venue.objects.get(name="Test Venue")
|
||||||
self.assertEqual(obj.name, form.find_element_by_xpath(
|
self.assertEqual(obj.name, form.find_element_by_xpath(
|
||||||
'//button[@data-id="id_organisation"]/span').text)
|
'//button[@data-id="id_venue"]/span').text)
|
||||||
# and backend
|
# and backend
|
||||||
option = form.find_element_by_xpath(
|
option = form.find_element_by_xpath(
|
||||||
'//select[@id="id_organisation"]//option[@selected="selected"]')
|
'//select[@id="id_venue"]//option[@selected="selected"]')
|
||||||
self.assertEqual(obj.pk, int(option.get_attribute("value")))
|
self.assertEqual(obj.pk, int(option.get_attribute("value")))
|
||||||
|
|
||||||
# Create venue
|
# Set start date/time
|
||||||
wait.until(animation_is_finished())
|
form.find_element_by_id('id_start_date').send_keys('25/05/3015')
|
||||||
add_button = self.browser.find_element_by_xpath(
|
form.find_element_by_id('id_start_time').send_keys('06:59')
|
||||||
'//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()
|
|
||||||
|
|
||||||
# See it is selected
|
# Set end date/time
|
||||||
wait.until(animation_is_finished())
|
form.find_element_by_id('id_end_date').send_keys('27/06/4000')
|
||||||
self.assertFalse(modal.is_displayed())
|
form.find_element_by_id('id_end_time').send_keys('07:00')
|
||||||
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")))
|
|
||||||
|
|
||||||
# Set start date/time
|
# Add item
|
||||||
form.find_element_by_id('id_start_date').send_keys('25/05/3015')
|
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
||||||
form.find_element_by_id('id_start_time').send_keys('06:59')
|
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 end date/time
|
# Confirm item has been saved to json field
|
||||||
form.find_element_by_id('id_end_date').send_keys('27/06/4000')
|
objectitems = self.browser.execute_script("return objectitems;")
|
||||||
form.find_element_by_id('id_end_time').send_keys('07:00')
|
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
|
||||||
|
|
||||||
# Add item
|
# See new item appear in table
|
||||||
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
|
||||||
wait.until(animation_is_finished())
|
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
|
||||||
modal = self.browser.find_element_by_id("itemModal")
|
self.assertIn("This is an item description",
|
||||||
modal.find_element_by_id("item_name").send_keys("Test Item 1")
|
row.find_element_by_xpath('//div[@class="item-description"]').text)
|
||||||
modal.find_element_by_id("item_description").send_keys(
|
self.assertEqual('£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
|
||||||
"This is an item description\nthat for reasons unknown spans two lines")
|
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
|
||||||
e = modal.find_element_by_id("item_quantity")
|
self.assertEqual('£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
|
||||||
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
|
|
||||||
|
|
||||||
# Confirm item has been saved to json field
|
# Check totals
|
||||||
objectitems = self.browser.execute_script("return objectitems;")
|
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
|
||||||
self.assertEqual(1, len(objectitems))
|
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
|
||||||
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
|
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
|
||||||
self.assertEqual("Test Item 1", testitem['name'])
|
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
|
||||||
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
|
|
||||||
|
|
||||||
# See new item appear in table
|
# Attempt to save - missing title
|
||||||
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
|
save.click()
|
||||||
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)
|
|
||||||
|
|
||||||
# Check totals
|
# See error
|
||||||
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
|
error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]')
|
||||||
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
|
self.assertTrue(error.is_displayed())
|
||||||
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
|
# Should only have one error message
|
||||||
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
|
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")
|
||||||
|
|
||||||
save = self.browser.find_element_by_xpath(
|
# Check at least some data is preserved. Some = all will be there
|
||||||
'(//button[@type="submit"])[3]')
|
option = self.browser.find_element_by_xpath(
|
||||||
save.click()
|
'//select[@id="id_person"]//option[@selected="selected"]')
|
||||||
|
self.assertEqual(person1.pk, int(option.get_attribute("value")))
|
||||||
|
|
||||||
# TODO Testing of requirement for contact details
|
# Set title
|
||||||
|
e = self.browser.find_element_by_id('id_name')
|
||||||
|
e.send_keys('Test Event Name')
|
||||||
|
e.send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# TODO Something seems broken with the CI tests here.
|
# See redirected to success page
|
||||||
# See redirected to success page
|
successTitle = self.browser.find_element_by_xpath('//h1').text
|
||||||
# successTitle = self.browser.find_element_by_xpath('//h1').text
|
event = models.Event.objects.get(name='Test Event Name')
|
||||||
# event = models.Event.objects.get(name='Test Event Name')
|
|
||||||
# self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
|
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
|
||||||
|
|
||||||
def testEventDuplicate(self):
|
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,
|
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
|
||||||
start_date=date.today() + timedelta(days=6),
|
start_date=date.today() + timedelta(days=6),
|
||||||
description="start future no end",
|
description="start future no end",
|
||||||
purchase_order='TESTPO',
|
purchase_order='TESTPO',
|
||||||
person=client,
|
|
||||||
auth_request_by=self.profile,
|
auth_request_by=self.profile,
|
||||||
auth_request_at=self.create_datetime(2015, 0o6, 0o4, 10, 00),
|
auth_request_at=self.create_datetime(2015, 0o6, 0o4, 10, 00),
|
||||||
auth_request_to="some@email.address")
|
auth_request_to="some@email.address")
|
||||||
@@ -493,7 +494,7 @@ class EventTest(LiveServerTestCase):
|
|||||||
|
|
||||||
save = self.browser.find_element_by_xpath(
|
save = self.browser.find_element_by_xpath(
|
||||||
'(//button[@type="submit"])[3]')
|
'(//button[@type="submit"])[3]')
|
||||||
form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
|
|
||||||
# Check the items are visible
|
# Check the items are visible
|
||||||
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
||||||
@@ -505,13 +506,11 @@ class EventTest(LiveServerTestCase):
|
|||||||
|
|
||||||
# Add item
|
# Add item
|
||||||
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
||||||
modal = self.browser.find_element_by_id("itemModal")
|
|
||||||
wait.until(animation_is_finished())
|
wait.until(animation_is_finished())
|
||||||
# See modal has opened
|
modal = self.browser.find_element_by_id("itemModal")
|
||||||
self.assertTrue(modal.is_displayed())
|
|
||||||
modal.find_element_by_id("item_name").send_keys("Test Item 3")
|
modal.find_element_by_id("item_name").send_keys("Test Item 3")
|
||||||
modal.find_element_by_id("item_description").send_keys(
|
modal.find_element_by_id("item_description").send_keys(
|
||||||
"This is an item description\nthat for reasons unknown spans two lines")
|
"This is an item description\nthat for reasons unkown spans two lines")
|
||||||
e = modal.find_element_by_id("item_quantity")
|
e = modal.find_element_by_id("item_quantity")
|
||||||
e.click()
|
e.click()
|
||||||
e.send_keys(Keys.UP)
|
e.send_keys(Keys.UP)
|
||||||
@@ -577,22 +576,13 @@ class EventTest(LiveServerTestCase):
|
|||||||
# Click Rig button
|
# Click Rig button
|
||||||
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
||||||
|
|
||||||
form = self.browser.find_element_by_xpath('//*[@id="content"]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
||||||
|
|
||||||
# Set title
|
# Set title
|
||||||
e = self.browser.find_element_by_id('id_name')
|
e = self.browser.find_element_by_id('id_name')
|
||||||
e.send_keys('Test Event 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
|
# Both dates, no times, end before start
|
||||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||||
|
|
||||||
@@ -607,7 +597,7 @@ class EventTest(LiveServerTestCase):
|
|||||||
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
|
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
|
||||||
|
|
||||||
# Same date, end time before start time
|
# Same date, end time before start time
|
||||||
form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
||||||
|
|
||||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||||
@@ -626,7 +616,7 @@ class EventTest(LiveServerTestCase):
|
|||||||
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
|
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
|
||||||
|
|
||||||
# Same date, end time before start time
|
# Same date, end time before start time
|
||||||
form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
||||||
|
|
||||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||||
@@ -639,7 +629,7 @@ class EventTest(LiveServerTestCase):
|
|||||||
form.find_element_by_id('id_end_time').send_keys('06:00')
|
form.find_element_by_id('id_end_time').send_keys('06:00')
|
||||||
|
|
||||||
# No end date, end time before start time
|
# No end date, end time before start time
|
||||||
form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
||||||
|
|
||||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||||
@@ -658,7 +648,7 @@ class EventTest(LiveServerTestCase):
|
|||||||
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
|
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
|
||||||
|
|
||||||
# 2 dates, end after start
|
# 2 dates, end after start
|
||||||
form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
||||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||||
self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-26'")
|
self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-26'")
|
||||||
@@ -691,22 +681,13 @@ class EventTest(LiveServerTestCase):
|
|||||||
# Click Rig button
|
# Click Rig button
|
||||||
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
|
||||||
|
|
||||||
form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
|
form = self.browser.find_element_by_tag_name('form')
|
||||||
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
|
||||||
|
|
||||||
# Set title
|
# Set title
|
||||||
e = self.browser.find_element_by_id('id_name')
|
e = self.browser.find_element_by_id('id_name')
|
||||||
e.send_keys('Test Event 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
|
# Set an arbitrary date
|
||||||
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
|
||||||
|
|
||||||
@@ -768,9 +749,9 @@ class EventTest(LiveServerTestCase):
|
|||||||
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
|
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
|
||||||
|
|
||||||
def testEventEdit(self):
|
def testEventEdit(self):
|
||||||
person = models.Person.objects.create(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123")
|
person = models.Person(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123").save()
|
||||||
organisation = models.Organisation.objects.create(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456")
|
organisation = models.Organisation(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
|
||||||
venue = models.Venue.objects.create(name="Event Detail Venue")
|
venue = models.Venue(name="Event Detail Venue").save()
|
||||||
|
|
||||||
eventData = {
|
eventData = {
|
||||||
'name': "Detail Test",
|
'name': "Detail Test",
|
||||||
@@ -1225,47 +1206,3 @@ class TECEventAuthorisationTest(TestCase):
|
|||||||
self.assertEqual(self.event.auth_request_by, self.profile)
|
self.assertEqual(self.event.auth_request_by, self.profile)
|
||||||
self.assertEqual(self.event.auth_request_to, 'client@functional.test')
|
self.assertEqual(self.event.auth_request_to, 'client@functional.test')
|
||||||
self.assertIsNotNone(self.event.auth_request_at)
|
self.assertIsNotNone(self.event.auth_request_at)
|
||||||
|
|
||||||
|
|
||||||
class SearchTest(LiveServerTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.profile = models.Profile(
|
|
||||||
username="SearchTest", first_name="Search", last_name="Test", initials="STU", is_superuser=True)
|
|
||||||
self.profile.set_password("SearchTestPassword")
|
|
||||||
self.profile.save()
|
|
||||||
|
|
||||||
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
|
||||||
|
|
||||||
self.browser = create_browser()
|
|
||||||
self.browser.implicitly_wait(10) # Set implicit wait session wide
|
|
||||||
|
|
||||||
os.environ['RECAPTCHA_TESTING'] = 'True'
|
|
||||||
|
|
||||||
models.Event.objects.create(name="Right Event", status=models.Event.PROVISIONAL,
|
|
||||||
start_date=date.today(), description="This event is searched for endlessly over and over")
|
|
||||||
models.Event.objects.create(name="Wrong Event", status=models.Event.PROVISIONAL, start_date=date.today(),
|
|
||||||
description="This one should never be found.")
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.browser.quit()
|
|
||||||
os.environ['RECAPTCHA_TESTING'] = 'False'
|
|
||||||
|
|
||||||
def test_search(self):
|
|
||||||
self.browser.get(self.live_server_url)
|
|
||||||
username = self.browser.find_element_by_id('id_username')
|
|
||||||
password = self.browser.find_element_by_id('id_password')
|
|
||||||
submit = self.browser.find_element_by_css_selector(
|
|
||||||
'input[type=submit]')
|
|
||||||
|
|
||||||
username.send_keys("SearchTest")
|
|
||||||
password.send_keys("SearchTestPassword")
|
|
||||||
submit.click()
|
|
||||||
|
|
||||||
form = self.browser.find_element_by_id('searchForm')
|
|
||||||
search_box = form.find_element_by_id('id_search_input')
|
|
||||||
search_box.send_keys('Right')
|
|
||||||
search_box.send_keys(Keys.ENTER)
|
|
||||||
|
|
||||||
event_name = self.browser.find_element_by_xpath('//*[@id="content"]/div[1]/div[4]/div/div/table/tbody/tr[1]/td[3]/h4').text
|
|
||||||
self.assertIn('Right', event_name)
|
|
||||||
self.assertNotIn('Wrong', event_name)
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -6,7 +8,6 @@ from django.test import TestCase
|
|||||||
from RIGS import models, versioning
|
from RIGS import models, versioning
|
||||||
from datetime import date, timedelta, datetime, time
|
from datetime import date, timedelta, datetime, time
|
||||||
from decimal import *
|
from decimal import *
|
||||||
from PyRIGS.tests.base import create_browser
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileTestCase(TestCase):
|
class ProfileTestCase(TestCase):
|
||||||
@@ -424,7 +425,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
|
|
||||||
def test_find_parent_version(self):
|
def test_find_parent_version(self):
|
||||||
# Find the most recent version
|
# Find the most recent version
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
self.assertEqual(currentVersion._object_version.object.notes, "A new note on the event")
|
self.assertEqual(currentVersion._object_version.object.notes, "A new note on the event")
|
||||||
|
|
||||||
# Check the prev version is loaded correctly
|
# Check the prev version is loaded correctly
|
||||||
@@ -436,7 +437,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
|
|
||||||
def test_changes_since(self):
|
def test_changes_since(self):
|
||||||
# Find the most recent version
|
# Find the most recent version
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
|
|
||||||
changes = currentVersion.changes
|
changes = currentVersion.changes
|
||||||
self.assertEqual(len(changes.field_changes), 1)
|
self.assertEqual(len(changes.field_changes), 1)
|
||||||
@@ -453,7 +454,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
self.event.save()
|
self.event.save()
|
||||||
|
|
||||||
# Find the most recent version
|
# Find the most recent version
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
diff = currentVersion.changes
|
diff = currentVersion.changes
|
||||||
|
|
||||||
# There are two changes
|
# There are two changes
|
||||||
@@ -475,7 +476,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
self.person.save()
|
self.person.save()
|
||||||
|
|
||||||
# Find the most recent version
|
# Find the most recent version
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.person).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.person).latest(field_name='revision__date_created')
|
||||||
diff = currentVersion.changes
|
diff = currentVersion.changes
|
||||||
|
|
||||||
# Should be declared as long
|
# Should be declared as long
|
||||||
@@ -488,7 +489,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
self.event.save()
|
self.event.save()
|
||||||
|
|
||||||
# Find the most recent version
|
# Find the most recent version
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
|
|
||||||
# Check the diff is correct
|
# Check the diff is correct
|
||||||
self.assertEqual(currentVersion.changes.field_changes[0].diff,
|
self.assertEqual(currentVersion.changes.field_changes[0].diff,
|
||||||
@@ -504,12 +505,12 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
self.event.status = models.Event.CONFIRMED
|
self.event.status = models.Event.CONFIRMED
|
||||||
self.event.save()
|
self.event.save()
|
||||||
|
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
self.assertEqual(currentVersion.changes.field_changes[0].old, 'Provisional')
|
self.assertEqual(currentVersion.changes.field_changes[0].old, 'Provisional')
|
||||||
self.assertEqual(currentVersion.changes.field_changes[0].new, 'Confirmed')
|
self.assertEqual(currentVersion.changes.field_changes[0].new, 'Confirmed')
|
||||||
|
|
||||||
def test_creation_behaviour(self):
|
def test_creation_behaviour(self):
|
||||||
firstVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created').parent
|
firstVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created').parent
|
||||||
diff = firstVersion.changes
|
diff = firstVersion.changes
|
||||||
|
|
||||||
# Mainly to check for exceptions:
|
# Mainly to check for exceptions:
|
||||||
@@ -522,7 +523,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
self.event.save()
|
self.event.save()
|
||||||
|
|
||||||
# Find the most recent version
|
# Find the most recent version
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
|
|
||||||
diffs = currentVersion.changes.item_changes
|
diffs = currentVersion.changes.item_changes
|
||||||
|
|
||||||
@@ -541,7 +542,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
item1.save()
|
item1.save()
|
||||||
self.event.save()
|
self.event.save()
|
||||||
|
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
|
|
||||||
diffs = currentVersion.changes.item_changes
|
diffs = currentVersion.changes.item_changes
|
||||||
|
|
||||||
@@ -563,7 +564,7 @@ class RIGSVersionTestCase(TestCase):
|
|||||||
self.event.save()
|
self.event.save()
|
||||||
|
|
||||||
# Find the most recent version
|
# Find the most recent version
|
||||||
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
|
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
|
||||||
|
|
||||||
diffs = currentVersion.changes.item_changes
|
diffs = currentVersion.changes.item_changes
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class TestPrintPaperwork(TestCase):
|
|||||||
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||||
|
|
||||||
cls.events = {
|
cls.events = {
|
||||||
1: models.Event.objects.create(name="TE E1", start_date=date.today(), description="This is an event description\nthat for a very specific reason spans two lines."),
|
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.invoices = {
|
cls.invoices = {
|
||||||
@@ -423,107 +423,3 @@ class TestSampleDataGenerator(TestCase):
|
|||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
|
|
||||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
|
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
|
||||||
|
|
||||||
|
|
||||||
class TestSearchLogic(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
|
|
||||||
is_active=True, is_staff=True)
|
|
||||||
|
|
||||||
cls.persons = {
|
|
||||||
1: models.Person.objects.create(name="Right Person", phone="1234"),
|
|
||||||
2: models.Person.objects.create(name="Wrong Person", phone="5678"),
|
|
||||||
}
|
|
||||||
|
|
||||||
cls.organisations = {
|
|
||||||
1: models.Organisation.objects.create(name="Right Organisation", email="test@example.com"),
|
|
||||||
2: models.Organisation.objects.create(name="Wrong Organisation", email="check@fake.co.uk"),
|
|
||||||
}
|
|
||||||
|
|
||||||
cls.venues = {
|
|
||||||
1: models.Venue.objects.create(name="Right Venue", address="1 Test Street, EX1"),
|
|
||||||
2: models.Venue.objects.create(name="Wrong Venue", address="2 Check Way, TS2"),
|
|
||||||
}
|
|
||||||
|
|
||||||
cls.events = {
|
|
||||||
1: models.Event.objects.create(name="Right Event", start_date=date.today(), person=cls.persons[1],
|
|
||||||
organisation=cls.organisations[1], venue=cls.venues[1]),
|
|
||||||
2: models.Event.objects.create(name="Wrong Event", start_date=date.today(), person=cls.persons[2],
|
|
||||||
organisation=cls.organisations[2], venue=cls.venues[2]),
|
|
||||||
}
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.profile.set_password('testuser')
|
|
||||||
self.profile.save()
|
|
||||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
|
||||||
|
|
||||||
def test_event_search(self):
|
|
||||||
# Test search by name
|
|
||||||
request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].name)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.events[1].name)
|
|
||||||
self.assertNotContains(response, self.events[2].name)
|
|
||||||
|
|
||||||
# Test search by ID
|
|
||||||
request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].pk)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.events[1].name)
|
|
||||||
self.assertNotContains(response, self.events[2].name)
|
|
||||||
|
|
||||||
def test_people_search(self):
|
|
||||||
# Test search by name
|
|
||||||
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].name)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.persons[1].name)
|
|
||||||
self.assertNotContains(response, self.persons[2].name)
|
|
||||||
|
|
||||||
# Test search by ID
|
|
||||||
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].pk)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.persons[1].name)
|
|
||||||
self.assertNotContains(response, self.persons[2].name)
|
|
||||||
|
|
||||||
# Test search by phone
|
|
||||||
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].phone)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.persons[1].name)
|
|
||||||
self.assertNotContains(response, self.persons[2].name)
|
|
||||||
|
|
||||||
def test_organisation_search(self):
|
|
||||||
# Test search by name
|
|
||||||
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].name)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.organisations[1].name)
|
|
||||||
self.assertNotContains(response, self.organisations[2].name)
|
|
||||||
|
|
||||||
# Test search by ID
|
|
||||||
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].pk)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.organisations[1].name)
|
|
||||||
self.assertNotContains(response, self.organisations[2].name)
|
|
||||||
|
|
||||||
# Test search by email
|
|
||||||
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].email)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.organisations[1].email)
|
|
||||||
self.assertNotContains(response, self.organisations[2].email)
|
|
||||||
|
|
||||||
def test_venue_search(self):
|
|
||||||
# Test search by name
|
|
||||||
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].name)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.venues[1].name)
|
|
||||||
self.assertNotContains(response, self.venues[2].name)
|
|
||||||
|
|
||||||
# Test search by ID
|
|
||||||
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].pk)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.venues[1].name)
|
|
||||||
self.assertNotContains(response, self.venues[2].name)
|
|
||||||
|
|
||||||
# Test search by address
|
|
||||||
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].address)
|
|
||||||
response = self.client.get(request_url, follow=True)
|
|
||||||
self.assertContains(response, self.venues[1].address)
|
|
||||||
self.assertNotContains(response, self.venues[2].address)
|
|
||||||
|
|||||||
15
RIGS/urls.py
15
RIGS/urls.py
@@ -1,14 +1,12 @@
|
|||||||
from django.urls import path
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib.auth.views import PasswordResetView
|
from django.contrib.auth.views import password_reset
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.views import LoginView
|
|
||||||
from RIGS import models, views, rigboard, finance, ical, versioning, forms
|
from RIGS import models, views, rigboard, finance, ical, versioning, forms
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
|
|
||||||
from PyRIGS.decorators import permission_required_with_403, has_oembed
|
from PyRIGS.decorators import permission_required_with_403
|
||||||
from PyRIGS.decorators import api_key_required
|
from PyRIGS.decorators import api_key_required
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -18,10 +16,10 @@ urlpatterns = [
|
|||||||
url('^$', login_required(views.Index.as_view()), name='index'),
|
url('^$', login_required(views.Index.as_view()), name='index'),
|
||||||
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
||||||
|
|
||||||
path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'),
|
url('^user/login/$', views.login, name='login'),
|
||||||
path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'),
|
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
|
||||||
|
|
||||||
url(r'^search_help/$', views.SearchHelp.as_view(), name='search_help'),
|
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}),
|
||||||
|
|
||||||
# People
|
# People
|
||||||
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
|
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
|
||||||
@@ -89,7 +87,8 @@ urlpatterns = [
|
|||||||
permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()),
|
permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()),
|
||||||
name='activity_feed'),
|
name='activity_feed'),
|
||||||
|
|
||||||
url(r'^event/(?P<pk>\d+)/$', has_oembed(oembed_view="event_oembed")(
|
url(r'^event/(?P<pk>\d+)/$',
|
||||||
|
permission_required_with_403('RIGS.view_event', oembed_view="event_oembed")(
|
||||||
rigboard.EventDetail.as_view()),
|
rigboard.EventDetail.as_view()),
|
||||||
name='event_detail'),
|
name='event_detail'),
|
||||||
url(r'^event/(?P<pk>\d+)/embed/$',
|
url(r'^event/(?P<pk>\d+)/embed/$',
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class RIGSVersionManager(VersionQuerySet):
|
|||||||
for model in model_array:
|
for model in model_array:
|
||||||
content_types.append(ContentType.objects.get_for_model(model))
|
content_types.append(ContentType.objects.get_for_model(model))
|
||||||
|
|
||||||
return self.filter(content_type__in=content_types).select_related("revision").order_by("-revision__date_created")
|
return self.filter(content_type__in=content_types).select_related("revision").order_by("-pk")
|
||||||
|
|
||||||
|
|
||||||
class RIGSVersion(Version):
|
class RIGSVersion(Version):
|
||||||
@@ -184,7 +184,8 @@ class RIGSVersion(Version):
|
|||||||
versions = RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId).select_related("revision", "revision__user").all()
|
versions = RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId).select_related("revision", "revision__user").all()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
previousVersion = versions.filter(revision_id__lt=self.revision_id).latest('revision__date_created')
|
previousVersion = versions.filter(revision_id__lt=self.revision_id).latest(
|
||||||
|
field_name='revision__date_created')
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -205,14 +206,17 @@ class VersionHistory(generic.ListView):
|
|||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return RIGSVersion.objects.get_for_object(self.get_object()).select_related("revision", "revision__user").all().order_by("-revision__date_created")
|
thisModel = self.kwargs['model']
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
versions = RIGSVersion.objects.get_for_object_reference(thisModel, self.kwargs['pk']).select_related("revision", "revision__user").all()
|
||||||
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
|
|
||||||
|
return versions
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
thisModel = self.kwargs['model']
|
||||||
context = super(VersionHistory, self).get_context_data(**kwargs)
|
context = super(VersionHistory, self).get_context_data(**kwargs)
|
||||||
context['object'] = self.get_object()
|
thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk'])
|
||||||
|
context['object'] = thisObject
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -224,7 +228,7 @@ class ActivityTable(generic.ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
|
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
|
||||||
return versions.order_by("-revision__date_created")
|
return versions
|
||||||
|
|
||||||
|
|
||||||
class ActivityFeed(generic.ListView):
|
class ActivityFeed(generic.ListView):
|
||||||
@@ -234,7 +238,7 @@ class ActivityFeed(generic.ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
|
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
|
||||||
return versions.order_by("-revision__date_created")
|
return versions
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# Call the base implementation first to get a context
|
# Call the base implementation first to get a context
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.http.response import HttpResponseRedirect
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.contrib.auth.views import LoginView
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
@@ -18,7 +17,6 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
|
|
||||||
|
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
from assets import models as asset_models
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -35,19 +33,28 @@ class Index(generic.TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SearchHelp(generic.TemplateView):
|
def login(request, **kwargs):
|
||||||
template_name = 'RIGS/search_help.html'
|
if request.user.is_authenticated:
|
||||||
|
next = request.GET.get('next', '/')
|
||||||
|
return HttpResponseRedirect(next)
|
||||||
|
else:
|
||||||
|
from django.contrib.auth.views import login
|
||||||
|
|
||||||
|
return login(request)
|
||||||
|
|
||||||
|
|
||||||
# This view should be exempt from requiring CSRF token.
|
# This view should be exempt from requiring CSRF token.
|
||||||
# Then we can check for it and show a nice error
|
# Then we can check for it and show a nice error
|
||||||
# Don't worry, django.contrib.auth.views.login will
|
# Don't worry, django.contrib.auth.views.login will
|
||||||
# check for it before logging the user in
|
# check for it before logging the user in
|
||||||
class LoginEmbed(LoginView):
|
@csrf_exempt
|
||||||
template_name = 'registration/login_embed.html'
|
def login_embed(request, **kwargs):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
next = request.GET.get('next', '/')
|
||||||
|
return HttpResponseRedirect(next)
|
||||||
|
else:
|
||||||
|
from django.contrib.auth.views import login
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
csrf_cookie = request.COOKIES.get('csrftoken', None)
|
csrf_cookie = request.COOKIES.get('csrftoken', None)
|
||||||
|
|
||||||
@@ -55,7 +62,7 @@ class LoginEmbed(LoginView):
|
|||||||
messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.')
|
messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.')
|
||||||
request.method = 'GET' # Render the page without trying to login
|
request.method = 'GET' # Render the page without trying to login
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return login(request, template_name="registration/login_embed.html", authentication_form=forms.EmbeddedAuthenticationForm)
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -78,20 +85,11 @@ class PersonList(generic.ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = self.request.GET.get('q', "")
|
q = self.request.GET.get('q', "")
|
||||||
|
if len(q) >= 3:
|
||||||
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q)
|
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(email__icontains=q))
|
||||||
|
else:
|
||||||
# try and parse an int
|
object_list = self.model.objects.all()
|
||||||
try:
|
orderBy = self.request.GET.get('orderBy', None)
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter)
|
|
||||||
|
|
||||||
orderBy = self.request.GET.get('orderBy', 'name')
|
|
||||||
if orderBy is not None:
|
if orderBy is not None:
|
||||||
object_list = object_list.order_by(orderBy)
|
object_list = object_list.order_by(orderBy)
|
||||||
return object_list
|
return object_list
|
||||||
@@ -141,20 +139,11 @@ class OrganisationList(generic.ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = self.request.GET.get('q', "")
|
q = self.request.GET.get('q', "")
|
||||||
|
if len(q) >= 3:
|
||||||
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q)
|
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q))
|
||||||
|
else:
|
||||||
# try and parse an int
|
object_list = self.model.objects.all()
|
||||||
try:
|
orderBy = self.request.GET.get('orderBy', "")
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter)
|
|
||||||
|
|
||||||
orderBy = self.request.GET.get('orderBy', "name")
|
|
||||||
if orderBy is not "":
|
if orderBy is not "":
|
||||||
object_list = object_list.order_by(orderBy)
|
object_list = object_list.order_by(orderBy)
|
||||||
return object_list
|
return object_list
|
||||||
@@ -204,20 +193,11 @@ class VenueList(generic.ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = self.request.GET.get('q', "")
|
q = self.request.GET.get('q', "")
|
||||||
|
if len(q) >= 3:
|
||||||
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q)
|
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q))
|
||||||
|
else:
|
||||||
# try and parse an int
|
object_list = self.model.objects.all()
|
||||||
try:
|
orderBy = self.request.GET.get('orderBy', "")
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter)
|
|
||||||
|
|
||||||
orderBy = self.request.GET.get('orderBy', "name")
|
|
||||||
if orderBy is not "":
|
if orderBy is not "":
|
||||||
object_list = object_list.order_by(orderBy)
|
object_list = object_list.order_by(orderBy)
|
||||||
return object_list
|
return object_list
|
||||||
@@ -268,7 +248,6 @@ class SecureAPIRequest(generic.View):
|
|||||||
'organisation': models.Organisation,
|
'organisation': models.Organisation,
|
||||||
'profile': models.Profile,
|
'profile': models.Profile,
|
||||||
'event': models.Event,
|
'event': models.Event,
|
||||||
'supplier': asset_models.Supplier
|
|
||||||
}
|
}
|
||||||
|
|
||||||
perms = {
|
perms = {
|
||||||
@@ -277,7 +256,6 @@ class SecureAPIRequest(generic.View):
|
|||||||
'organisation': 'RIGS.view_organisation',
|
'organisation': 'RIGS.view_organisation',
|
||||||
'profile': 'RIGS.view_profile',
|
'profile': 'RIGS.view_profile',
|
||||||
'event': None,
|
'event': None,
|
||||||
'supplier': None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|||||||
5
assets/apps.py
Normal file
5
assets/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AssetsConfig(AppConfig):
|
||||||
|
name = 'assets'
|
||||||
9
assets/filters.py
Normal file
9
assets/filters.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import django_filters
|
||||||
|
|
||||||
|
from assets import models
|
||||||
|
|
||||||
|
|
||||||
|
class AssetFilter(django_filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = models.Asset
|
||||||
|
fields = ['asset_id', 'description', 'category', 'status']
|
||||||
@@ -4,11 +4,6 @@ from assets import models
|
|||||||
|
|
||||||
|
|
||||||
class AssetForm(forms.ModelForm):
|
class AssetForm(forms.ModelForm):
|
||||||
related_models = {
|
|
||||||
'asset': models.Asset,
|
|
||||||
'supplier': models.Supplier
|
|
||||||
}
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Asset
|
model = models.Asset
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|||||||
229
assets/management/commands/import_old_db.py
Normal file
229
assets/management/commands/import_old_db.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from assets import models
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Imports old db from XML dump'
|
||||||
|
|
||||||
|
epoch = datetime.date(1970, 1, 1)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.import_categories()
|
||||||
|
self.import_statuses()
|
||||||
|
self.import_suppliers()
|
||||||
|
self.import_collections()
|
||||||
|
self.import_assets()
|
||||||
|
self.import_cables()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xml_path(file):
|
||||||
|
return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_xml(file):
|
||||||
|
tree = ET.parse(file)
|
||||||
|
|
||||||
|
return tree.getroot()
|
||||||
|
|
||||||
|
def import_categories(self):
|
||||||
|
# 0: updated, 1: created
|
||||||
|
tally = [0, 0]
|
||||||
|
root = self.parse_xml(self.xml_path('TEC_Asset_Categories.xml'))
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
obj, created = models.AssetCategory.objects.update_or_create(
|
||||||
|
pk=int(child.find('AssetCategoryID').text),
|
||||||
|
name=child.find('AssetCategory').text
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
tally[1] += 1
|
||||||
|
else:
|
||||||
|
tally[0] += 1
|
||||||
|
|
||||||
|
print('Categories - Updated: {}, Created: {}'.format(tally[0], tally[1]))
|
||||||
|
|
||||||
|
def import_statuses(self):
|
||||||
|
# 0: updated, 1: created
|
||||||
|
tally = [0, 0]
|
||||||
|
root = self.parse_xml(self.xml_path('TEC_Asset_Status_new.xml'))
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
obj, created = models.AssetStatus.objects.update_or_create(
|
||||||
|
pk=int(child.find('StatusID').text),
|
||||||
|
name=child.find('Status').text
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
tally[1] += 1
|
||||||
|
else:
|
||||||
|
tally[0] += 1
|
||||||
|
|
||||||
|
print('Statuses - Updated: {}, Created: {}'.format(tally[0], tally[1]))
|
||||||
|
|
||||||
|
def import_suppliers(self):
|
||||||
|
# 0: updated, 1: created
|
||||||
|
tally = [0, 0]
|
||||||
|
root = self.parse_xml(self.xml_path('TEC_Asset_Suppliers_new.xml'))
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
obj, created = models.Supplier.objects.update_or_create(
|
||||||
|
pk=int(child.find('Supplier_x0020_Id').text),
|
||||||
|
name=child.find('Supplier_x0020_Name').text
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
tally[1] += 1
|
||||||
|
else:
|
||||||
|
tally[0] += 1
|
||||||
|
|
||||||
|
print('Suppliers - Updated: {}, Created: {}'.format(tally[0], tally[1]))
|
||||||
|
|
||||||
|
def import_assets(self):
|
||||||
|
# 0: updated, 1: created
|
||||||
|
tally = [0, 0]
|
||||||
|
root = self.parse_xml(self.xml_path('TEC_Assets.xml'))
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
defaults = dict()
|
||||||
|
|
||||||
|
# defaults['pk'] = int(child.find('ID').text)
|
||||||
|
defaults['asset_id'] = child.find('AssetID').text
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['description'] = child.find('AssetDescription').text
|
||||||
|
except AttributeError:
|
||||||
|
defaults['description'] = 'None'
|
||||||
|
|
||||||
|
defaults['category'] = models.AssetCategory.objects.get(pk=int(child.find('AssetCategoryID').text))
|
||||||
|
defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('StatusID').text))
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['serial_number'] = child.find('SerialNumber').text
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['purchased_from'] = models.Supplier.objects.get(pk=int(child.find('Supplier_x0020_Id').text))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['date_acquired'] = datetime.datetime.strptime(child.find('DateAcquired').text, '%d/%m/%Y').date()
|
||||||
|
except AttributeError:
|
||||||
|
defaults['date_acquired'] = self.epoch
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['date_sold'] = datetime.datetime.strptime(child.find('DateSold').text, '%d/%m/%Y').date()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['purchase_price'] = float(child.find('Replacement_x0020_Value').text)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['salvage_value'] = float(child.find('SalvageValue').text)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['comments'] = child.find('Comments').text
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
date = child.find('NextSchedMaint').text.split('T')[0]
|
||||||
|
defaults['next_sched_maint'] = datetime.datetime.strptime(date, '%Y-%m-%d').date()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(defaults)
|
||||||
|
|
||||||
|
obj, created = models.Asset.objects.update_or_create(**defaults)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
tally[1] += 1
|
||||||
|
else:
|
||||||
|
tally[0] += 1
|
||||||
|
|
||||||
|
print('Assets - Updated: {}, Created: {}'.format(tally[0], tally[1]))
|
||||||
|
|
||||||
|
def import_collections(self):
|
||||||
|
tally = [0, 0]
|
||||||
|
root = self.parse_xml(self.xml_path('TEC_Cable_Collections.xml'))
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
defaults = dict()
|
||||||
|
|
||||||
|
defaults['pk'] = int(child.find('ID').text)
|
||||||
|
defaults['name'] = child.find('Cable_x0020_Trunk').text
|
||||||
|
|
||||||
|
obj, created = models.Collection.objects.update_or_create(**defaults)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
tally[1] += 1
|
||||||
|
else:
|
||||||
|
tally[0] += 1
|
||||||
|
|
||||||
|
print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))
|
||||||
|
|
||||||
|
def import_cables(self):
|
||||||
|
tally = [0, 0]
|
||||||
|
root = self.parse_xml(self.xml_path('TEC_Cables.xml'))
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
defaults = dict()
|
||||||
|
|
||||||
|
defaults['asset_id'] = child.find('Asset_x0020_Number').text
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['description'] = child.find('Type_x0020_of_x0020_Cable').text
|
||||||
|
except AttributeError:
|
||||||
|
defaults['description'] = 'None'
|
||||||
|
|
||||||
|
defaults['is_cable'] = True
|
||||||
|
defaults['category'] = models.AssetCategory.objects.get(pk=9)
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['length'] = child.find('Length_x0020__x0028_m_x0029_').text
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('Status').text))
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['comments'] = child.find('Comments').text
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
collection_id = int(child.find('Collection').text)
|
||||||
|
if collection_id != 0:
|
||||||
|
defaults['collection'] = models.Collection.objects.get(pk=collection_id)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
defaults['purchase_price'] = float(child.find('Purchase_x0020_Price').text)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
defaults['date_acquired'] = self.epoch
|
||||||
|
|
||||||
|
print(defaults)
|
||||||
|
|
||||||
|
obj, created = models.Asset.objects.update_or_create(**defaults)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
tally[1] += 1
|
||||||
|
else:
|
||||||
|
tally[0] += 1
|
||||||
|
|
||||||
|
print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))
|
||||||
110
assets/management/commands/update_old_db_file.py
Normal file
110
assets/management/commands/update_old_db_file.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Imports old db from XML dump'
|
||||||
|
|
||||||
|
epoch = datetime.date(1970, 1, 1)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# self.update_statuses()
|
||||||
|
# self.update_suppliers()
|
||||||
|
self.update_cable_statuses()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def xml_path(file):
|
||||||
|
return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_xml(file):
|
||||||
|
tree = ET.parse(file)
|
||||||
|
|
||||||
|
return tree.getroot()
|
||||||
|
|
||||||
|
def update_statuses(self):
|
||||||
|
file = self.xml_path('TEC_Assets.xml')
|
||||||
|
tree = ET.parse(file)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# map old status pk to new status pk
|
||||||
|
status_map = {
|
||||||
|
2: 2,
|
||||||
|
3: 4,
|
||||||
|
4: 3,
|
||||||
|
5: 5,
|
||||||
|
6: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
status = int(child.find('StatusID').text)
|
||||||
|
child.find('StatusID').text = str(status_map[status])
|
||||||
|
|
||||||
|
tree.write(file)
|
||||||
|
|
||||||
|
def update_suppliers(self):
|
||||||
|
old_file = self.xml_path('TEC_Asset_Suppliers.xml')
|
||||||
|
old_tree = ET.parse(old_file)
|
||||||
|
old_root = old_tree.getroot()
|
||||||
|
|
||||||
|
new_file = self.xml_path('TEC_Asset_Suppliers_new.xml')
|
||||||
|
new_tree = ET.parse(new_file)
|
||||||
|
new_root = new_tree.getroot()
|
||||||
|
|
||||||
|
# map old supplier pk to new supplier pk
|
||||||
|
supplier_map = dict()
|
||||||
|
|
||||||
|
def find_in_old(name, root):
|
||||||
|
for child in root:
|
||||||
|
found_id = child.find('Supplier_x0020_Id').text
|
||||||
|
found_name = child.find('Supplier_x0020_Name').text
|
||||||
|
|
||||||
|
if found_name == name:
|
||||||
|
return found_id
|
||||||
|
|
||||||
|
for new_child in new_root:
|
||||||
|
new_id = new_child.find('Supplier_x0020_Id').text
|
||||||
|
new_name = new_child.find('Supplier_x0020_Name').text
|
||||||
|
|
||||||
|
old_id = find_in_old(new_name, old_root)
|
||||||
|
|
||||||
|
supplier_map[int(old_id)] = int(new_id)
|
||||||
|
|
||||||
|
file = self.xml_path('TEC_Assets.xml')
|
||||||
|
tree = ET.parse(file)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
try:
|
||||||
|
supplier = int(child.find('Supplier_x0020_Id').text)
|
||||||
|
child.find('Supplier_x0020_Id').text = str(supplier_map[supplier])
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tree.write(file)
|
||||||
|
|
||||||
|
def update_cable_statuses(self):
|
||||||
|
file = self.xml_path('TEC_Cables.xml')
|
||||||
|
tree = ET.parse(file)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# map old status pk to new status pk
|
||||||
|
status_map = {
|
||||||
|
0: 7,
|
||||||
|
1: 3,
|
||||||
|
3: 2,
|
||||||
|
4: 5,
|
||||||
|
6: 6,
|
||||||
|
7: 1,
|
||||||
|
8: 4,
|
||||||
|
9: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in root:
|
||||||
|
status = int(child.find('Status').text)
|
||||||
|
child.find('Status').text = str(status_map[status])
|
||||||
|
|
||||||
|
tree.write(file)
|
||||||
@@ -57,7 +57,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='asset',
|
model_name='asset',
|
||||||
name='parent',
|
name='parent',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Asset'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Asset'),
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='asset',
|
model_name='asset',
|
||||||
@@ -85,7 +85,7 @@ class Migration(migrations.Migration):
|
|||||||
('circuits', models.IntegerField(blank=True, null=True)),
|
('circuits', models.IntegerField(blank=True, null=True)),
|
||||||
('cores', 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')),
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetCategory')),
|
||||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Cable')),
|
('parent', models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Cable')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 3.0.3 on 2020-02-19 14:44
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0009_auto_20200103_2215'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='asset',
|
|
||||||
options={'ordering': ['asset_id_prefix', 'asset_id_number'], 'permissions': [('asset_finance', 'Can see financial data for assets')]},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='supplier',
|
|
||||||
options={'ordering': ['name']},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -6,17 +6,11 @@ from django.urls import reverse
|
|||||||
from django.db.models.signals import pre_save
|
from django.db.models.signals import pre_save
|
||||||
from django.dispatch.dispatcher import receiver
|
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 AssetCategory(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Asset Category'
|
verbose_name = 'Asset Category'
|
||||||
verbose_name_plural = 'Asset Categories'
|
verbose_name_plural = 'Asset Categories'
|
||||||
ordering = ['name']
|
|
||||||
|
|
||||||
name = models.CharField(max_length=80)
|
name = models.CharField(max_length=80)
|
||||||
|
|
||||||
@@ -28,24 +22,22 @@ class AssetStatus(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Asset Status'
|
verbose_name = 'Asset Status'
|
||||||
verbose_name_plural = 'Asset Statuses'
|
verbose_name_plural = 'Asset Statuses'
|
||||||
ordering = ['name']
|
|
||||||
|
|
||||||
name = models.CharField(max_length=80)
|
name = models.CharField(max_length=80)
|
||||||
should_show = models.BooleanField(
|
should_show = models.BooleanField(default=True, help_text="Should this be shown by default in the asset list.")
|
||||||
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
class Supplier(models.Model):
|
||||||
class Supplier(models.Model, RevisionMixin):
|
|
||||||
class Meta:
|
|
||||||
ordering = ['name']
|
|
||||||
|
|
||||||
name = models.CharField(max_length=80)
|
name = models.CharField(max_length=80)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_supplier', 'Can view a supplier'),
|
||||||
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('supplier_list')
|
return reverse('supplier_list')
|
||||||
|
|
||||||
@@ -63,22 +55,21 @@ class Connector(models.Model):
|
|||||||
return self.description
|
return self.description
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
class Asset(models.Model):
|
||||||
class Asset(models.Model, RevisionMixin):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||||
permissions = [
|
permissions = (
|
||||||
('asset_finance', 'Can see financial data for assets')
|
('asset_finance', 'Can see financial data for assets'),
|
||||||
]
|
('view_asset', 'Can view an asset')
|
||||||
|
)
|
||||||
|
|
||||||
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
parent = models.ForeignKey(to='self', related_name='asset_parent', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
blank=True, null=True, on_delete=models.SET_NULL)
|
|
||||||
asset_id = models.CharField(max_length=15, unique=True)
|
asset_id = models.CharField(max_length=15, unique=True)
|
||||||
description = models.CharField(max_length=120)
|
description = models.CharField(max_length=120)
|
||||||
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
|
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
|
||||||
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
|
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
|
||||||
serial_number = models.CharField(max_length=150, blank=True)
|
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")
|
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True)
|
||||||
date_acquired = models.DateField()
|
date_acquired = models.DateField()
|
||||||
date_sold = models.DateField(blank=True, null=True)
|
date_sold = models.DateField(blank=True, null=True)
|
||||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||||
@@ -88,14 +79,10 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
# Cable assets
|
# Cable assets
|
||||||
is_cable = models.BooleanField(default=False)
|
is_cable = models.BooleanField(default=False)
|
||||||
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL,
|
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='plug', blank=True, null=True)
|
||||||
related_name='plug', blank=True, null=True)
|
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='socket', blank=True, null=True)
|
||||||
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL,
|
length = models.DecimalField(decimal_places=1, max_digits=10, blank=True, null=True, help_text='m')
|
||||||
related_name='socket', blank=True, null=True)
|
csa = models.DecimalField(decimal_places=2, max_digits=10, blank=True, null=True, help_text='mm^2')
|
||||||
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)
|
circuits = models.IntegerField(blank=True, null=True)
|
||||||
cores = models.IntegerField(blank=True, null=True)
|
cores = models.IntegerField(blank=True, null=True)
|
||||||
|
|
||||||
@@ -138,8 +125,7 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
self.asset_id = self.asset_id.upper()
|
self.asset_id = self.asset_id.upper()
|
||||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id)
|
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id)
|
||||||
if asset_search is None:
|
if asset_search is None:
|
||||||
errdict["asset_id"] = [
|
errdict["asset_id"] = ["An Asset ID can only consist of letters and numbers, with a final number"]
|
||||||
"An Asset ID can only consist of letters and numbers, with a final number"]
|
|
||||||
|
|
||||||
if self.purchase_price and self.purchase_price < 0:
|
if self.purchase_price and self.purchase_price < 0:
|
||||||
errdict["purchase_price"] = ["A price cannot be negative"]
|
errdict["purchase_price"] = ["A price cannot be negative"]
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
{% extends 'base_assets.html' %}
|
{% extends 'base_assets.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
{% load asset_templatetags %}
|
||||||
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
{% extends 'base_embed.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% 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 %}
|
|
||||||
@@ -11,22 +11,22 @@
|
|||||||
|
|
||||||
<form id="asset-search-form" method="get" class="form-inline pull-right">
|
<form id="asset-search-form" method="get" class="form-inline pull-right">
|
||||||
<div class="input-group pull-right" style="width: auto;">
|
<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"%}
|
{% render_field form.query|add_class:'form-control' placeholder='Search by Asset ID/Description' style="width: 250px"%}
|
||||||
<label for="query" class="sr-only">Asset ID/Description/Serial Number:</label>
|
<label for="query" class="sr-only">Asset ID/Description:</label>
|
||||||
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
|
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div style="margin-top: 1em;" class="pull-right">
|
<div style="margin-top: 1em;" class="pull-right">
|
||||||
<div id="category-group" class="form-group">
|
<div class="form-group">
|
||||||
<label for="category" class="sr-only">Category</label>
|
<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" %}
|
{% 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>
|
||||||
<div id="status-group" class="form-group">
|
<div class="form-group">
|
||||||
<label for="status" class="sr-only">Status</label>
|
<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" %}
|
{% render_field form.status|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
||||||
</div>
|
</div>
|
||||||
<!---TODO: Auto filter whenever an option is selected, instead of using a button -->
|
<!---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>
|
<button type="submit" class="btn btn-default">Filter</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{% extends 'base_assets.html' %}
|
{% extends 'base_assets.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
{% load asset_templatetags %}
|
||||||
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -39,19 +41,10 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% include 'partials/asset_buttons.html' %}
|
{% include 'partials/asset_buttons.html' %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js%}
|
{% block js%}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||||
{% elif duplicate %}
|
{% elif duplicate %}
|
||||||
<!--duplicate-->
|
<!--duplicate-->
|
||||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
|
<button type="submit" class="btn btn-default"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
|
||||||
{% elif create %}
|
{% elif create %}
|
||||||
<!--create-->
|
<!--create-->
|
||||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
{% load asset_templatetags %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
Asset Details
|
Asset Details
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
{% for item in object_list %}
|
{% for item in object_list %}
|
||||||
{# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
|
{# <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-->
|
<!---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="
|
||||||
<tr class="{{ item.status.display_class|default:'' }} assetRow">
|
{% if item.status.name == 'Broken' %}
|
||||||
<td style="vertical-align: middle;"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
|
danger
|
||||||
<td class="assetDesc" style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
|
{% elif item.status.name == 'Scrapped'%}
|
||||||
<td class="assetCategory" style="vertical-align: middle;">{{ item.category }}</td>
|
warning
|
||||||
<td class="assetStatus" style="vertical-align: middle;">{{ item.status }}</td>
|
{% elif item.status.name == 'Sold'%}
|
||||||
|
warning
|
||||||
|
{% elif item.status.name == 'Lost'%}
|
||||||
|
danger
|
||||||
|
{% elif item.status.name == 'Not Built Yet'%}
|
||||||
|
info
|
||||||
|
{% elif item.status.name == 'Active'%}
|
||||||
|
success
|
||||||
|
{% endif %}
|
||||||
|
">
|
||||||
|
<td style="vertical-align: middle;"><a href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
|
||||||
|
<td style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
|
||||||
|
<td style="vertical-align: middle;">{{ item.category }}</td>
|
||||||
|
<td style="vertical-align: middle;">{{ item.status }}</td>
|
||||||
<td class="hidden-xs">
|
<td class="hidden-xs">
|
||||||
<div class="btn-group" role="group">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
{% load asset_templatetags %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
Cable Details
|
Cable Details
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
{% load asset_templatetags %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
Collection Details
|
Collection Details
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if create or edit or duplicate %}
|
{% if create or edit or duplicate %}
|
||||||
<div class="form-group" id="parent-group">
|
<div class="form-group">
|
||||||
<label for="selectpicker">Set Parent</label>
|
<label for="selectpicker">Set Parent</label>
|
||||||
{% include 'partials/asset_picker.html' %}
|
{% include 'partials/asset_picker.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,14 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load static %}
|
{% load asset_templatetags %}
|
||||||
|
|
||||||
|
|
||||||
{% 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 panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
Purchase Details
|
Purchase Details
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if create or edit or duplicate %}
|
{% if create or edit or duplicate %}
|
||||||
<div class="form-group" id="purchased-from-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
<label for="{{ form.purchased_from.id_for_label }}">Purchased From</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' %}">
|
{% include 'partials/supplier_picker.html' %}
|
||||||
{% 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>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
64
assets/templates/partials/supplier_picker.html
Normal file
64
assets/templates/partials/supplier_picker.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<select name="purchased_from" id="supplier_id" class="selectpicker">
|
||||||
|
{% if object.parent%}
|
||||||
|
<option value="{{object.parent.pk}}" selected>{{object.parent.name}}</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>
|
||||||
|
$('#supplier_id')
|
||||||
|
.selectpicker({
|
||||||
|
liveSearch: true
|
||||||
|
})
|
||||||
|
.ajaxSelectPicker({
|
||||||
|
ajax: {
|
||||||
|
url: '{% url 'supplier_search_json'%}',
|
||||||
|
type: "get",
|
||||||
|
data: function () {
|
||||||
|
var params = {
|
||||||
|
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
|
||||||
|
};
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
emptyTitle: 'Search for supplier...'
|
||||||
|
},
|
||||||
|
preprocessData: function(data){
|
||||||
|
var suppliers = [];
|
||||||
|
if(data.length){
|
||||||
|
var len = data.length;
|
||||||
|
for(var i = 0; i < len; i++){
|
||||||
|
var curr = data[i];
|
||||||
|
suppliers.push(
|
||||||
|
{
|
||||||
|
'value': curr.id,
|
||||||
|
'text': curr.name,
|
||||||
|
'disabled': false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
suppliers.push(
|
||||||
|
{
|
||||||
|
'value': null,
|
||||||
|
'text': "(no selection)"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return suppliers;
|
||||||
|
},
|
||||||
|
preserveSelected: false
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock js %}
|
||||||
@@ -1,73 +1,6 @@
|
|||||||
{% extends 'base_assets.html' %}
|
{% extends 'base_assets.html' %}
|
||||||
{% block title %}Supplier | {{ object.name }}{% endblock %}
|
{% block title %}Detail{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
{{ object }}
|
||||||
{% if not request.is_ajax %}
|
{% endblock %}
|
||||||
<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 %}
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="input-group pull-right" style="width: auto;">
|
<div class="input-group pull-right" style="width: auto;">
|
||||||
{% render_field form.query|add_class:'form-control' placeholder='Search by Name' style="width: 250px"%}
|
{% render_field form.query|add_class:'form-control' placeholder='Search by Name' style="width: 250px"%}
|
||||||
<label for="query" class="sr-only">Name:</label>
|
<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>
|
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -27,10 +27,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="asset_table_body">
|
<tbody id="asset_table_body">
|
||||||
{% for item in object_list %}
|
{% for item in object_list %}
|
||||||
<tr class="supplierRow">
|
<tr>
|
||||||
<td class="supplierName">{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
<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>
|
<a href="{% url 'supplier_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'form_errors.html' %}
|
{{ form }}
|
||||||
{{ form }}
|
|
||||||
<input type="submit" value="Save" class="btn btn-success">
|
<input type="submit" value="Save" class="btn btn-success">
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
21
assets/templatetags/asset_templatetags.py
Normal file
21
assets/templatetags/asset_templatetags.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from django import template
|
||||||
|
from django.template.defaultfilters import stringfilter
|
||||||
|
from django.utils.safestring import SafeData, mark_safe
|
||||||
|
from django.utils.text import normalize_newlines
|
||||||
|
from django.utils.html import escape
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(is_safe=True, needs_autoescape=True)
|
||||||
|
@stringfilter
|
||||||
|
def linebreaksn(value, autoescape=True):
|
||||||
|
"""
|
||||||
|
Convert all newlines in a piece of plain text to jQuery line breaks
|
||||||
|
(`\n`).
|
||||||
|
"""
|
||||||
|
autoescape = autoescape and not isinstance(value, SafeData)
|
||||||
|
value = normalize_newlines(value)
|
||||||
|
if autoescape:
|
||||||
|
value = escape(value)
|
||||||
|
return mark_safe(value.replace('\n', '\\n'))
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
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.assertEqual("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)
|
|
||||||
@@ -1,44 +1,22 @@
|
|||||||
from django.conf.urls import url
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from assets import views, models
|
from assets import views
|
||||||
from RIGS import versioning
|
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from PyRIGS.decorators import permission_required_with_403
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
|
||||||
from PyRIGS.decorators import has_oembed, permission_required_with_403
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', login_required(views.AssetList.as_view()), name='asset_index'),
|
path('', views.AssetList.as_view(), name='asset_index'),
|
||||||
path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'),
|
path('asset/list/', 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/id/<str:pk>/', views.AssetDetail.as_view(), name='asset_detail'),
|
||||||
path('asset/create/', permission_required_with_403('assets.add_asset')
|
path('asset/create/', permission_required_with_403('assets.add_asset')(views.AssetCreate.as_view()), name='asset_create'),
|
||||||
(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>/edit/', permission_required_with_403('assets.change_asset')
|
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset')(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
||||||
(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/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/list', views.SupplierList.as_view(), name='supplier_list'),
|
||||||
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
|
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
|
||||||
path('supplier/create', permission_required_with_403('assets.add_supplier')
|
path('supplier/create', permission_required_with_403('assets.add_supplier')(views.SupplierCreate.as_view()), name='supplier_create'),
|
||||||
(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>/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'),
|
path('supplier/search/', views.SupplierSearch.as_view(), name='supplier_search_json'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.http import HttpResponse, Http404
|
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from assets import models, forms
|
from assets import models, forms
|
||||||
from RIGS import versioning
|
|
||||||
|
|
||||||
import simplejson
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
@@ -41,8 +36,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
|||||||
if len(query_string) == 0:
|
if len(query_string) == 0:
|
||||||
queryset = self.model.objects.all()
|
queryset = self.model.objects.all()
|
||||||
elif len(query_string) >= 3:
|
elif len(query_string) >= 3:
|
||||||
queryset = self.model.objects.filter(
|
queryset = self.model.objects.filter(Q(asset_id__exact=query_string) | Q(description__icontains=query_string))
|
||||||
Q(asset_id__exact=query_string) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
|
|
||||||
else:
|
else:
|
||||||
queryset = self.model.objects.filter(Q(asset_id__exact=query_string))
|
queryset = self.model.objects.filter(Q(asset_id__exact=query_string))
|
||||||
|
|
||||||
@@ -52,8 +46,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
|||||||
if len(form.cleaned_data['status']) > 0:
|
if len(form.cleaned_data['status']) > 0:
|
||||||
queryset = queryset.filter(status__in=form.cleaned_data['status'])
|
queryset = queryset.filter(status__in=form.cleaned_data['status'])
|
||||||
elif self.hide_hidden_status:
|
elif self.hide_hidden_status:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(status__in=models.AssetStatus.objects.filter(should_show=True))
|
||||||
status__in=models.AssetStatus.objects.filter(should_show=True))
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@@ -87,7 +80,8 @@ class AssetIDUrlMixin:
|
|||||||
# Get the single item from the filtered queryset
|
# Get the single item from the filtered queryset
|
||||||
obj = queryset.get()
|
obj = queryset.get()
|
||||||
except queryset.model.DoesNotExist:
|
except queryset.model.DoesNotExist:
|
||||||
raise Http404("No assets found matching the query")
|
raise Http404(_("No %(verbose_name)s found matching the query") %
|
||||||
|
{'verbose_name': queryset.model._meta.verbose_name})
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@@ -151,28 +145,6 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
|
|||||||
return context
|
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):
|
class SupplierList(generic.ListView):
|
||||||
model = models.Supplier
|
model = models.Supplier
|
||||||
template_name = 'supplier_list.html'
|
template_name = 'supplier_list.html'
|
||||||
@@ -212,6 +184,7 @@ class SupplierSearch(SupplierList):
|
|||||||
|
|
||||||
for supplier in context["object_list"]:
|
for supplier in context["object_list"]:
|
||||||
result.append({"id": supplier.pk, "name": supplier.name})
|
result.append({"id": supplier.pk, "name": supplier.name})
|
||||||
|
|
||||||
return JsonResponse(result, safe=False)
|
return JsonResponse(result, safe=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -230,25 +203,3 @@ class SupplierUpdate(generic.UpdateView):
|
|||||||
model = models.Supplier
|
model = models.Supplier
|
||||||
form_class = forms.SupplierForm
|
form_class = forms.SupplierForm
|
||||||
template_name = 'supplier_update.html'
|
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
|
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
diff-match-patch==20181111
|
beautifulsoup4==4.6.0
|
||||||
|
contextlib2==0.5.5
|
||||||
|
diff-match-patch==20121119
|
||||||
dj-database-url==0.5.0
|
dj-database-url==0.5.0
|
||||||
dj-static==0.0.6
|
dj-static==0.0.6
|
||||||
Django==3.0.3
|
Django==2.0.13
|
||||||
django-debug-toolbar==2.2
|
django-filter==2.0.0
|
||||||
django-ical==1.7.0
|
django-widget-tweaks==1.4.3
|
||||||
django-recaptcha==2.0.6
|
django-debug-toolbar==1.9.1
|
||||||
django-registration-redux==2.7
|
django-ical==1.4
|
||||||
django-reversion==3.0.7
|
django-recaptcha==1.4.0
|
||||||
django-widget-tweaks==1.4.5
|
django-registration-redux==2.4
|
||||||
gunicorn==20.0.4
|
django-reversion==2.0.13
|
||||||
icalendar==4.0.4
|
django-toolbelt==0.0.1
|
||||||
lxml==4.5.0
|
premailer==3.2.0
|
||||||
premailer==3.6.1
|
git+git://github.com/jazzband/django-widget-tweaks.git@1.4.2
|
||||||
psycopg2==2.8.4
|
gunicorn==19.8.1
|
||||||
|
icalendar==4.0.1
|
||||||
|
lxml==4.2.1
|
||||||
|
Markdown==2.6.11
|
||||||
|
Pillow==5.1.0
|
||||||
|
psycopg2==2.7.4
|
||||||
|
Pygments==2.2.0
|
||||||
PyPDF2==1.26.0
|
PyPDF2==1.26.0
|
||||||
PyPOM==2.2.0
|
python-dateutil==2.7.3
|
||||||
pytz==2019.3
|
pytz==2018.4
|
||||||
raven==6.10.0
|
raven==6.8.0
|
||||||
requests==2.23.0
|
reportlab==3.4.0
|
||||||
selenium==3.141.0
|
selenium==3.12.0
|
||||||
simplejson==3.17.0
|
simplejson==3.15.0
|
||||||
whitenoise==5.0.1
|
six==1.11.0
|
||||||
z3c.rml==3.9.1
|
sqlparse==0.2.4
|
||||||
|
static3==0.7.0
|
||||||
|
svg2rlg==0.3
|
||||||
|
yolk==0.4.3
|
||||||
|
whitenoise==4.1.2
|
||||||
|
z3c.rml==3.5.0
|
||||||
|
zope.event==4.3.0
|
||||||
|
zope.interface==4.5.0
|
||||||
|
zope.schema==4.5.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
{% block title %}Bad Request{% endblock %}
|
{% block title %}Bad Request{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
{% block title %}Unauthorized{% endblock %}
|
{% block title %}Unauthorized{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
{% block title %}Forbidden{% endblock %}
|
{% block title %}Forbidden{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
{% block title %}Page Not Found{% endblock %}
|
{% block title %}Page Not Found{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
{% block title %}Server error{% endblock %}
|
{% block title %}Server error{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% load static %}
|
{% load static from staticfiles %}
|
||||||
{% load raven %}
|
{% load raven %}
|
||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html
|
||||||
dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}"
|
dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}"
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li class="dropdown" id="user">
|
<li class="dropdown">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
<span class="glyphicon glyphicon-user"></span>
|
<span class="glyphicon glyphicon-user"></span>
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block extrahead %}
|
|
||||||
<meta name="google" content="notranslate">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block titleheader %}
|
{% block titleheader %}
|
||||||
<a class="nav navbar-brand navbar-left" href="/"><i class="glyphicon glyphicon-circle-arrow-left" style="vertical-align: middle !important;"></i> RIGS</a>
|
<a class="nav navbar-brand navbar-left" href="/"><i class="glyphicon glyphicon-circle-arrow-left" style="vertical-align: middle !important;"></i> RIGS</a>
|
||||||
<a class="nav navbar-brand" href="{% url 'asset_index' %}">Assets</a>
|
<a class="nav navbar-brand" href="{% url 'asset_index' %}">Assets</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block titleelements %}
|
{% block titleelements %}
|
||||||
{# % if perms.assets.view_asset % #}
|
{% if perms.assets.view_asset%}
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
@@ -20,20 +15,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{# % endif % #}
|
{% endif %}
|
||||||
{# % if perms.assets.view_supplier % #}
|
{% if perms.assets.view_supplier%}
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
|
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
|
||||||
List Suppliers</a></li>
|
List Suppliers</a></li>
|
||||||
{% if perms.assets.add_supplier %}
|
{% if perms.assets.add_asset %}
|
||||||
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
|
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{# % endif % #}
|
|
||||||
{% if perms.assets.view_asset %}
|
|
||||||
<li><a href="{% url 'asset_activity_table' %}">Recent Changes</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% load static %}
|
{% load static from staticfiles %}
|
||||||
{% load raven %}
|
{% load raven %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
{% load static %}
|
{% load static from staticfiles %}
|
||||||
{% load raven %}
|
{% load raven %}
|
||||||
|
|
||||||
|
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<table class="main-table">
|
<table class="main-table">
|
||||||
<tr class="client-header">
|
<tr class="client-header">
|
||||||
<td align="center">
|
<td align="center">
|
||||||
@@ -29,7 +32,7 @@
|
|||||||
<!--[if mso]>
|
<!--[if mso]>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
</center>
|
</center>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -44,9 +47,12 @@
|
|||||||
<!--[if mso]>
|
<!--[if mso]>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
</center>
|
</center>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% load static %}
|
{% load static from staticfiles %}
|
||||||
{% load raven %}
|
{% load raven %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|||||||
@@ -5,10 +5,8 @@
|
|||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
{% with form|nice_errors as qq %}
|
{% with form|nice_errors as qq %}
|
||||||
{% for error_name,desc in qq.items %}
|
{% for error_name,desc in qq.items %}
|
||||||
<span>
|
|
||||||
<dt>{{error_name}}</dt>
|
<dt>{{error_name}}</dt>
|
||||||
<dd>{{desc}}</dd>
|
<dd>{{desc}}</dd>
|
||||||
</span>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load staticfiles %}
|
||||||
{% block title %}Login Required{% endblock %}
|
{% block title %}Login Required{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
<h2>Activation Complete</h2>
|
<h2>Activation Complete</h2>
|
||||||
<p>Your user account is now awaiting administrator approval. Won't be long!</p>
|
<p>You user account is now fully registered. Enjoy RIGS</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% include 'form_errors.html' %}
|
{% include 'form_errors.html' %}
|
||||||
<div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4">
|
<div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4">
|
||||||
|
|
||||||
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="id_username">{{ form.username.label }}</label>
|
<label for="id_username">{{ form.username.label }}</label>
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="{% url 'registration_register' %}" class="btn">Register</a>
|
<a href="{% url 'registration_register' %}" class="btn">Register</a>
|
||||||
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
||||||
<input type="submit" id="id_submit" value="Login" class="btn btn-primary"/>
|
<input type="submit" value="Login" class="btn btn-primary"/>
|
||||||
<input type="hidden" name="next" value="{{ next }}"/>
|
<input type="hidden" name="next" value="{{ next }}"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user