Compare commits

..

9 Commits

Author SHA1 Message Date
5c3c84fd07 CHORE: pep8
And another random bit of wierd whitespace I found
2019-12-31 12:08:50 +00:00
Matthew Smith
5054858585 Refactored out duplicated code from `AssetVersionHistory 2019-12-29 22:36:32 +00:00
d3ba770400 CHORE: *sings* And a pep8 in a broken tree... 2019-12-26 14:20:18 +00:00
2ca6786745 FEAT: Make revision history for suppliers accessible 2019-12-26 14:12:55 +00:00
08600daf7c FIX: Individual asset version history is now correctly filtered 2019-12-26 14:05:17 +00:00
9953ac0dc1 FIX: Asset history table 'branding' 2019-12-18 13:17:06 +00:00
f803dbb028 CHORE: Fix pep8 2019-12-17 20:44:49 +00:00
d50a2e8423 FEAT: Initial implementation of asset activity stream 2019-12-17 19:30:01 +00:00
206b54dab0 FEAT: Initial work on revision history for assets
The revision history for individual items mostly works, though it shows database ID where it should show asset ID. Recent changes feed isn't yet done.
2019-12-13 20:19:17 +00:00
109 changed files with 1472 additions and 3307 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,69 +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
from PyRIGS.tests import regions
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\")});")
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)
@property
def errors(self):
try:
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
return error_page.errors
except NoSuchElementException:
return None
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()

View File

@@ -1,157 +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)
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

View File

@@ -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),
] ]

View File

@@ -1,13 +1,20 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
[![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg)](https://travis-ci.org/nottinghamtec/PyRIGS) [![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg?branch=develop)](https://travis-ci.org/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS) [![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop)
[![Dependency Status](https://gemnasium.com/badges/github.com/nottinghamtec/PyRIGS.svg)](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
``` ```
[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](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.

View File

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

View File

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

View File

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

View File

@@ -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),
),
]

View File

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

View File

@@ -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={},
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &amp; Lighting</p>
{% endblock %}

View File

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

View File

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

View File

@@ -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>&nbsp;</dd> <dd>&nbsp;</dd>
<dt>Event Description</dt> <dt>Event Description</dt>
<dd class="dont-break-out">{{ event.description|linebreaksbr }}</dd> <dd>{{ event.description|linebreaksbr }}</dd>
<dd>&nbsp;</dd> <dd>&nbsp;</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>&nbsp;</dd> <dd>&nbsp;</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|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
</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>

View File

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

View File

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

View File

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

View File

@@ -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>&pound;{{ object.amount }}</b> for <b>&pound;{{ 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>

View File

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

View File

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

View File

@@ -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>
@@ -88,7 +73,7 @@
</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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>£&nbsp;<span class="cost">{{item.cost|floatformat:2}}</span></td> <td>£&nbsp;<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>£&nbsp;<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td> <td>£&nbsp;<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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/$',

View File

@@ -25,7 +25,7 @@ class FieldComparison(object):
self._new = new self._new = new
def display_value(self, value): def display_value(self, value):
if isinstance(self.field, IntegerField) and self.field.choices is not None and len(self.field.choices) > 0: if isinstance(self.field, IntegerField) and len(self.field.choices) > 0:
return [x[1] for x in self.field.choices if x[0] == value][0] return [x[1] for x in self.field.choices if x[0] == value][0]
if self.field.name == "risk_assessment_edit_url": if self.field.name == "risk_assessment_edit_url":
return "completed" if value else "" return "completed" if value else ""
@@ -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,7 +206,7 @@ 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") return RIGSVersion.objects.get_for_object(self.get_object()).select_related("revision", "revision__user").all()
def get_object(self, **kwargs): def get_object(self, **kwargs):
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk']) return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
@@ -224,7 +225,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 +235,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

View File

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

View File

@@ -4,7 +4,6 @@
"scripts": { "scripts": {
"postdeploy": "python manage.py migrate && python manage.py generateSampleData" "postdeploy": "python manage.py migrate && python manage.py generateSampleData"
}, },
"stack": "heroku-18",
"env": { "env": {
"DEBUG": { "DEBUG": {
"required": true "required": true

View File

@@ -23,15 +23,10 @@ class SupplierAdmin(admin.ModelAdmin):
@admin.register(assets.Asset) @admin.register(assets.Asset)
class AssetAdmin(admin.ModelAdmin): class AssetAdmin(admin.ModelAdmin):
list_display = ['id', 'asset_id', 'description', 'category', 'status'] list_display = ['id', 'asset_id', 'description', 'category', 'status']
list_filter = ['is_cable', 'category', 'status'] list_filter = ['is_cable', 'category']
search_fields = ['id', 'asset_id', 'description'] search_fields = ['id', 'asset_id', 'description']
@admin.register(assets.CableType)
class CableTypeAdmin(admin.ModelAdmin):
list_display = ['id', '__str__', 'plug', 'socket', 'cores', 'circuits']
@admin.register(assets.Connector) @admin.register(assets.Connector)
class ConnectorAdmin(admin.ModelAdmin): class ConnectorAdmin(admin.ModelAdmin):
list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins'] list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins']

5
assets/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AssetsConfig(AppConfig):
name = 'assets'

9
assets/filters.py Normal file
View 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']

View File

@@ -1,19 +1,13 @@
from django import forms from django import forms
from assets import models from assets import models
from django.db.models import Q
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__'
exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by'] exclude = ['asset_id_prefix', 'asset_id_number']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -21,13 +15,6 @@ class AssetForm(forms.ModelForm):
self.fields['date_acquired'].widget.format = '%Y-%m-%d' self.fields['date_acquired'].widget.format = '%Y-%m-%d'
class AssetAuditForm(AssetForm):
class Meta(AssetForm.Meta):
# Prevents assets losing existing data that isn't included in the audit form
exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by',
'parent', 'purchased_from', 'purchase_price', 'comments']
class AssetSearchForm(forms.Form): class AssetSearchForm(forms.Form):
query = forms.CharField(required=False) query = forms.CharField(required=False)
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False) category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
@@ -42,17 +29,3 @@ class SupplierForm(forms.ModelForm):
class SupplierSearchForm(forms.Form): class SupplierSearchForm(forms.Form):
query = forms.CharField(required=False) query = forms.CharField(required=False)
class CableTypeForm(forms.ModelForm):
class Meta:
model = models.CableType
fields = '__all__'
def clean(self):
form_data = self.cleaned_data
queryset = models.CableType.objects.filter(Q(plug=form_data['plug']) & Q(socket=form_data['socket']) & Q(circuits=form_data['circuits']) & Q(cores=form_data['cores']))
# Being identical to itself shouldn't count...
if queryset.exists() and self.instance.pk != queryset[0].pk:
raise forms.ValidationError("A cable type that exactly matches this one already exists, please use that instead.", code="notunique")
return form_data

View File

@@ -1,9 +1,7 @@
import random import random
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone from django.utils import timezone
from reversion import revisions as reversion
from assets import models from assets import models
from RIGS import models as rigsmodels
class Command(BaseCommand): class Command(BaseCommand):
@@ -17,7 +15,6 @@ class Command(BaseCommand):
random.seed('Some object to see the random number generator') random.seed('Some object to see the random number generator')
self.create_profile()
self.create_categories() self.create_categories()
self.create_statuses() self.create_statuses()
self.create_suppliers() self.create_suppliers()
@@ -25,13 +22,6 @@ class Command(BaseCommand):
self.create_connectors() self.create_connectors()
self.create_cables() self.create_cables()
# Make sure that there's at least one profile if this command is run standalone
def create_profile(self):
name = "Fred Johnson"
models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
email=name.replace(" ", "") + "@example.com",
initials="".join([j[0].upper() for j in name.split()]))
def create_categories(self): def create_categories(self):
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging'] categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
@@ -39,19 +29,17 @@ class Command(BaseCommand):
models.AssetCategory.objects.create(name=cat) models.AssetCategory.objects.create(name=cat)
def create_statuses(self): def create_statuses(self):
statuses = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')] statuses = [('In Service', True), ('Lost', False), ('Binned', False), ('Sold', False), ('Broken', False)]
for stat in statuses: for stat in statuses:
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2]) models.AssetStatus.objects.create(name=stat[0], should_show=stat[1])
def create_suppliers(self): def create_suppliers(self):
suppliers = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa suppliers = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa "Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
with reversion.create_revision(): for supplier in suppliers:
for supplier in suppliers: models.Supplier.objects.create(name=supplier)
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
models.Supplier.objects.create(name=supplier)
def create_assets(self): def create_assets(self):
asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink'] asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink']
@@ -60,24 +48,22 @@ class Command(BaseCommand):
statuses = models.AssetStatus.objects.all() statuses = models.AssetStatus.objects.all()
suppliers = models.Supplier.objects.all() suppliers = models.Supplier.objects.all()
with reversion.create_revision(): for i in range(100):
for i in range(100): asset = models.Asset(
reversion.set_user(random.choice(rigsmodels.Profile.objects.all())) asset_id='{}'.format(models.Asset.get_available_asset_id()),
asset = models.Asset( description=random.choice(asset_description),
asset_id='{}'.format(models.Asset.get_available_asset_id()), category=random.choice(categories),
description=random.choice(asset_description), status=random.choice(statuses),
category=random.choice(categories), date_acquired=timezone.now().date()
status=random.choice(statuses), )
date_acquired=timezone.now().date()
)
if i % 4 == 0: if i % 4 == 0:
asset.parent = models.Asset.objects.order_by('?').first() asset.parent = models.Asset.objects.order_by('?').first()
if i % 3 == 0: if i % 3 == 0:
asset.purchased_from = random.choice(suppliers) asset.purchased_from = random.choice(suppliers)
asset.clean() asset.clean()
asset.save() asset.save()
def create_cables(self): def create_cables(self):
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep'] asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
@@ -92,9 +78,6 @@ class Command(BaseCommand):
suppliers = models.Supplier.objects.all() suppliers = models.Supplier.objects.all()
connectors = models.Connector.objects.all() connectors = models.Connector.objects.all()
for i in range(len(connectors)):
models.CableType.objects.create(plug=random.choice(connectors), socket=random.choice(connectors), circuits=random.choice(circuits), cores=random.choice(cores))
for i in range(100): for i in range(100):
asset = models.Asset( asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()), asset_id='{}'.format(models.Asset.get_available_asset_id()),
@@ -104,9 +87,12 @@ class Command(BaseCommand):
date_acquired=timezone.now().date(), date_acquired=timezone.now().date(),
is_cable=True, is_cable=True,
cable_type=random.choice(models.CableType.objects.all()), plug=random.choice(connectors),
socket=random.choice(connectors),
csa=random.choice(csas), csa=random.choice(csas),
length=random.choice(lengths), length=random.choice(lengths),
circuits=random.choice(circuits),
cores=random.choice(circuits)
) )
if i % 5 == 0: if i % 5 == 0:

View 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]))

View 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)

View 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,

View File

@@ -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'),
),
]

View File

@@ -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']},
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 2.0.13 on 2020-02-18 16:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_auto_20200219_1444'),
]
operations = [
migrations.CreateModel(
name='CableType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('circuits', models.IntegerField(blank=True, null=True)),
('cores', models.IntegerField(blank=True, null=True)),
('plug', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector')),
('socket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector')),
],
),
migrations.AddField(
model_name='asset',
name='cable_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.CableType'),
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 2.0.13 on 2020-02-18 16:27
from django.db import migrations
from django.db.models import Q
def move_cable_type_data(apps, schema_editor):
Asset = apps.get_model('assets', 'Asset')
CableType = apps.get_model('assets', 'CableType')
for asset in Asset.objects.filter(is_cable=True):
# Only create one type per...well...type
if(not CableType.objects.filter(Q(plug=asset.plug) & Q(socket=asset.socket))):
cabletype = CableType.objects.create(plug=asset.plug, socket=asset.socket, circuits=asset.circuits, cores=asset.cores)
asset.save()
cabletype.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0011_auto_20200218_1617'),
]
operations = [
migrations.RunPython(move_cable_type_data)
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 2.0.13 on 2020-02-18 16:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0012_auto_20200218_1627'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='circuits',
),
migrations.RemoveField(
model_name='asset',
name='cores',
),
migrations.RemoveField(
model_name='asset',
name='plug',
),
migrations.RemoveField(
model_name='asset',
name='socket',
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 2.0.13 on 2020-02-18 18:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0013_auto_20200218_1639'),
]
operations = [
migrations.AlterModelOptions(
name='cabletype',
options={'ordering': ['plug', 'socket', '-circuits']},
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 3.0.3 on 2020-04-13 15:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0014_auto_20200218_1840'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='next_sched_maint',
),
]

View File

@@ -1,34 +0,0 @@
# Generated by Django 3.0.3 on 2020-04-13 15:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0015_remove_asset_next_sched_maint'),
]
operations = [
migrations.AlterField(
model_name='cabletype',
name='circuits',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='cabletype',
name='cores',
field=models.IntegerField(default=3),
),
migrations.AlterField(
model_name='cabletype',
name='plug',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plug', to='assets.Connector'),
),
migrations.AlterField(
model_name='cabletype',
name='socket',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='socket', to='assets.Connector'),
),
]

View File

@@ -1,31 +0,0 @@
# Generated by Django 3.0.3 on 2020-04-13 00:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0016_auto_20200413_1632'),
]
operations = [
migrations.AddField(
model_name='asset',
name='last_audited_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='asset',
name='last_audited_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audited_by', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='asset',
name='csa',
field=models.DecimalField(blank=True, decimal_places=2, help_text='mm²', max_digits=10, null=True),
),
]

View File

@@ -9,14 +9,13 @@ from django.dispatch.dispatcher import receiver
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
from RIGS.models import RevisionMixin, Profile 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,12 +27,10 @@ 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
@@ -41,11 +38,13 @@ class AssetStatus(models.Model):
@reversion.register @reversion.register
class Supplier(models.Model, RevisionMixin): 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,32 +62,14 @@ class Connector(models.Model):
return self.description return self.description
# Things are nullable that shouldn't be because I didn't properly fix the data structure when moving this to its own model...
class CableType(models.Model):
class Meta:
ordering = ['plug', 'socket', '-circuits']
circuits = models.IntegerField(default=1)
cores = models.IntegerField(default=3)
plug = models.ForeignKey(Connector, on_delete=models.CASCADE,
related_name='plug', null=True)
socket = models.ForeignKey(Connector, on_delete=models.CASCADE,
related_name='socket', null=True)
def __str__(self):
if self.plug and self.socket:
return "%s%s" % (self.plug.description, self.socket.description)
else:
return "Unknown"
@reversion.register @reversion.register
class Asset(models.Model, RevisionMixin): 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)
@@ -97,24 +78,26 @@ class Asset(models.Model, RevisionMixin):
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)
salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10) salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
next_sched_maint = models.DateField(blank=True, null=True)
# Audit
last_audited_at = models.DateTimeField(blank=True, null=True)
last_audited_by = models.ForeignKey(Profile, on_delete=models.SET_NULL, related_name='audited_by', blank=True, null=True)
# Cable assets # Cable assets
is_cable = models.BooleanField(default=False) is_cable = models.BooleanField(default=False)
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL) plug = models.ForeignKey(Connector, on_delete=models.SET_NULL,
related_name='plug', blank=True, null=True)
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL,
related_name='socket', blank=True, null=True)
length = models.DecimalField(decimal_places=1, max_digits=10, length = models.DecimalField(decimal_places=1, max_digits=10,
blank=True, null=True, help_text='m') blank=True, null=True, help_text='m')
csa = models.DecimalField(decimal_places=2, max_digits=10, csa = models.DecimalField(decimal_places=2, max_digits=10,
blank=True, null=True, help_text='mm²') blank=True, null=True, help_text='mm^2')
circuits = models.IntegerField(blank=True, null=True)
cores = models.IntegerField(blank=True, null=True)
# Hidden asset_id components # Hidden asset_id components
# For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001" # For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001"
@@ -144,7 +127,7 @@ class Asset(models.Model, RevisionMixin):
def __str__(self): def __str__(self):
out = str(self.asset_id) + ' - ' + self.description out = str(self.asset_id) + ' - ' + self.description
if self.is_cable: if self.is_cable:
out += '{} - {}m - {}'.format(self.cable_type.plug, self.length, self.cable_type.socket) out += '{} - {}m - {}'.format(self.plug, self.length, self.socket)
return out return out
def clean(self): def clean(self):
@@ -169,16 +152,14 @@ class Asset(models.Model, RevisionMixin):
errdict["length"] = ["The length of a cable must be more than 0"] errdict["length"] = ["The length of a cable must be more than 0"]
if not self.csa or self.csa <= 0: if not self.csa or self.csa <= 0:
errdict["csa"] = ["The CSA of a cable must be more than 0"] errdict["csa"] = ["The CSA of a cable must be more than 0"]
if not self.cable_type: if not self.circuits or self.circuits <= 0:
errdict["cable_type"] = ["A cable must have a type"] errdict["circuits"] = ["There must be at least one circuit in a cable"]
# if not self.circuits or self.circuits <= 0: if not self.cores or self.cores <= 0:
# errdict["circuits"] = ["There must be at least one circuit in a cable"] errdict["cores"] = ["There must be at least one core in a cable"]
# if not self.cores or self.cores <= 0: if self.socket is None:
# errdict["cores"] = ["There must be at least one core in a cable"] errdict["socket"] = ["A cable must have a socket"]
# if self.socket is None: if self.plug is None:
# errdict["socket"] = ["A cable must have a socket"] errdict["plug"] = ["A cable must have a plug"]
# if self.plug is None:
# errdict["plug"] = ["A cable must have a plug"]
if errdict != {}: # If there was an error when validation if errdict != {}: # If there was an error when validation
raise ValidationError(errdict) raise ValidationError(errdict)

View File

@@ -1,142 +0,0 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% load widget_tweaks %}
{% block title %}Audit Asset {{ object.asset_id }}{% endblock %}
{% block content %}
<script>
function setAcquired(today) {
var date = new Date(1970, 0, 1);
if(today) {
date = new Date();
}
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
}
function setLength(length) {
$('#id_length').val(length);
}
function setCSA(CSA) {
$('#id_csa').val(CSA);
}
function checkIfCableHidden() {
if (document.getElementById("id_is_cable").checked) {
document.getElementById("cable-table").hidden = false;
} else {
document.getElementById("cable-table").hidden = true;
}
}
checkIfCableHidden();
</script>
<form class="form-horizontal" method="POST" id="asset_audit_form" action="{{ form.action|default:request.path }}">
{% include 'form_errors.html' %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
<div class="form-group">
<label for="{{ form.asset_id.id_for_label }}" class="col-sm-2 control-label">Asset ID</label>
<div class="col-sm-10">
{% render_field form.asset_id|add_class:'form-control' value=object.asset_idz %}
</div>
</div>
<div class="form-group">
<label for="{{ form.description.id_for_label }}" class="col-sm-2 control-label">Description</label>
<div class="col-sm-10">
{% render_field form.description|add_class:'form-control' value=object.description %}
</div>
</div>
<div class="form-group">
<label for="{{ form.category.id_for_label }}" class="col-sm-2 control-label">Category</label>
<div class="col-sm-10">
{% render_field form.category|add_class:'form-control'%}
</div>
</div>
<div class="form-group">
<label for="{{ form.status.id_for_label }}" class="col-sm-2 control-label">Status</label>
<div class="col-sm-10">
{% render_field form.status|add_class:'form-control'%}
</div>
</div>
<div class="form-group">
<label for="{{ form.serial_number.id_for_label }}" class="col-sm-2 control-label">Serial Number</label>
<div class="col-sm-10">
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
</div>
</div>
<div class="form-group">
<label for="{{ form.date_acquired.id_for_label }}" class="col-sm-2 control-label">Date Acquired</label>
<div class="col-sm-6">
{% render_field form.date_acquired|add_class:'form-control' value=object.date_acquired %}
</div>
<div class="col-sm-4">
<btn class="btn btn-default" onclick="setAcquired(true);" tabindex="-1">Today</btn>
<btn class="btn btn-default" onclick="setAcquired(false);" tabindex="-1">Unknown</btn>
</div>
</div>
<div class="form-group">
<label for="{{ form.date_sold.id_for_label }}" class="col-sm-2 control-label">Date Sold</label>
<div class="col-sm-6">
{% render_field form.date_sold|add_class:'form-control' value=object.date_sold %}
</div>
</div>
<div class="form-group">
<label for="{{ form.salvage_value.id_for_label }}" class="col-sm-2 control-label">Salvage Value</label>
<div class="col-sm-10">
<div class="input-group">
<span class="input-group-addon">£</span>
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
</div>
</div>
</div>
<hr>
<div class="form-group">
<label for="{{ form.is_cable.id_for_label }}" class="col-sm-2 control-label">Cable?</label>
<div class="col-sm-10">
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %}
</div>
</div>
<div id="cable-table">
<div class="form-group">
<label for="{{ form.cable_type.id_for_label }}" class="col-sm-2 control-label">Cable Type</label>
<div class="col-sm-10">
{% render_field form.cable_type|add_class:'form-control' %}
</div>
</div>
<div class="form-group">
<label for="{{ form.length.id_for_label }}" class="col-sm-2 control-label">Length</label>
<div class="col-sm-6">
<div class="input-group">
{% render_field form.length|add_class:'form-control' %}
<span class="input-group-addon">{{ form.length.help_text }}</span>
</div>
</div>
<div class="col-sm-4">
<btn class="btn btn-danger" onclick="setLength('5');" tabindex="-1">5{{ form.length.help_text }}</btn>
<btn class="btn btn-success" onclick="setLength('10');" tabindex="-1">10{{ form.length.help_text }}</btn>
<btn class="btn btn-info" onclick="setLength('20');" tabindex="-1">20{{ form.length.help_text }}</btn>
</div>
</div>
<div class="form-group">
<label for="{{ form.csa.id_for_label }}" class="col-sm-2 control-label">Cross Sectional Area</label>
<div class="col-sm-6">
<div class="input-group">
{% render_field form.csa|add_class:'form-control' value=object.csa %}
<span class="input-group-addon">{{ form.csa.help_text }}</span>
</div>
</div>
<div class="col-sm-4">
<btn class="btn btn-default" onclick="setCSA('1.5');" tabindex="-1">1.5{{ form.csa.help_text }}</btn>
<btn class="btn btn-default" onclick="setCSA('2.5');" tabindex="-1">2.5{{ form.csa.help_text }}</btn>
</div>
</div>
</div>
{% if not request.is_ajax %}
<div class="form-group pull-right">
<button class="btn btn-success" type="submit" form="asset_audit_form" id="id_mark_audited">Mark Audited</button>
</div>
{% endif %}
</form>
{% endblock %}
{% block footer %}
<div class="form-group">
<button class="btn btn-success pull-right" type="submit" form="asset_audit_form" onclick="onAuditClick({{form.asset_id.value}});" id="id_mark_audited">Mark Audited</button>
</div>
{% endblock %}

View File

@@ -1,87 +0,0 @@
{% extends 'base_assets.html' %}
{% block title %}Asset Audit List{% endblock %}
{% load static %}
{% load paginator from filters %}
{% load widget_tweaks %}
{% block js %}
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script src="{% static "js/interaction.js" %}"></script>
<script src="{% static "js/modal.js" %}"></script>
<script>
$('document').ready(function(){
$('#asset-search-form').submit(function () {
$('#searchButton').focus().click();
return false;
});
$('#searchButton').click(function (e) {
e.preventDefault();
var url = "{% url 'asset_audit' None %}";
var id = $("#{{form.query.id_for_label}}").val();
url = url.replace('None', id);
$.ajax({
url: url,
success: function(){
$link = $(this);
// Anti modal inception
if ($link.parents('#modal').length == 0) {
modaltarget = $link.data('target');
modalobject = "";
$('#modal').load(url, function (e) {
$('#modal').modal();
});
}
},
error:function(){
$("#error404").attr("hidden", false);
}
});
});
});
function onAuditClick(assetID) {
$('#' + assetID).remove();
}
</script>
{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="text-center">Asset Audit List</h1>
</div>
<div id="error404" class="alert alert-danger alert-dismissable" hidden=true>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<span>Asset with that ID does not exist!</span>
</div>
<h3>Audit Asset:</h3>
<form id="asset-search-form" class="form-horizontal" method="POST">
<div class="input-group input-group-lg" style="width: auto;">
{% render_field form.query|add_class:'form-control' placeholder='Enter Asset ID' autofocus="true" %}
<label for="query" class="sr-only">Asset ID:</label>
<span class="input-group-btn"><a id="searchButton" class="btn btn-default" class="submit" type="submit">Search</a></span>
</div>
</form>
<h3>Assets Requiring Audit:</h3>
<table class="table">
<thead>
<tr>
<th>Asset ID</th>
<th>Description</th>
<th>Category</th>
<th>Status</th>
<th class="hidden-xs"></th>
</tr>
</thead>
<tbody id="asset_table_body">
{% include 'partials/asset_list_table_body.html' with audit="true" %}
</tbody>
</table>
{% if is_paginated %}
<div class="text-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}

View File

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

View File

@@ -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>
&nbsp;
<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 %}

View File

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

View File

@@ -1,7 +1,9 @@
{% extends request.is_ajax|yesno:'base_ajax.html,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">
@@ -23,31 +25,27 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4"
{% if not object.is_cable %} hidden="true" {% endif %} id="cable-table">
{% include 'partials/cable_form.html' %}
</div>
{% if perms.assets.asset_finance %} {% if perms.assets.asset_finance %}
<div class="col-md-4"> <div class="col-md-6">
{% include 'partials/purchasedetails_form.html' %} {% include 'partials/purchasedetails_form.html' %}
</div> </div>
{%endif%} {%endif%}
<div class="col-md-6"
{% if not object.is_cable %} hidden="true" {% endif %} id="cable-table">
{% include 'partials/cable_form.html' %}
</div>
<div class="col-md-4"> <div class="col-md-4">
{% include 'partials/parent_form.html' %} {% include 'partials/parent_form.html' %}
</div> </div>
{% if not edit %}
<div class="col-md-4">
{% include 'partials/audit_details.html' %}
</div>
{% endif %}
</div> </div>
<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 %} {% if not edit %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
<div> <div>
<a href="{% url 'asset_history' object.asset_id %}" title="View Revision History"> <a href="{% url 'asset_history' object.asset_id %}" title="View Revision History">

View File

@@ -1,61 +0,0 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% block title %}Cable Type{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% if create %}Create{% elif edit %}Edit{% endif %} Cable Type
</h1>
</div>
{% if create %}
<form method="POST" action="{% url 'cable_type_create'%}">
{% elif edit %}
<form method="POST" action="{% url 'cable_type_update' object.id %}">
{% endif %}
{% include 'form_errors.html' %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
<div class="row">
<div class="col-sm-12">
{% if create or edit %}
<div class="form-group">
<label for="{{ form.plug.id_for_label }}">Plug</label>
{% render_field form.plug|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.socket.id_for_label }}">Socket</label>
{% render_field form.socket|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
</div>
<div class="form-group">
<label for="{{ form.cores.id_for_label }}">Cores</label>
{% render_field form.cores|add_class:'form-control' value=object.cores %}
</div>
<div class="pull-left">
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
<br>
<button type="reset" class="btn btn-link">Cancel</button>
</div>
{% else %}
<dl>
<dt>Socket</dt>
<dd>{{ object.socket|default_if_none:'-' }}</dd>
<dt>Plug</dt>
<dd>{{ object.plug|default_if_none:'-' }}</dd>
<dt>Circuits</dt>
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
<dt>Cores</dt>
<dd>{{ object.cores|default_if_none:'-' }}</dd>
</dl>
{% endif %}
</div>
</div>
</form>
{% endblock %}

View File

@@ -1,41 +0,0 @@
{% extends 'base_assets.html' %}
{% block title %}Supplier List{% endblock %}
{% load paginator from filters %}
{% load widget_tweaks %}
{% block content %}
<div class="page-header">
<h1>Cable Type List</h1>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Cable Type</th>
<th>Circuits</th>
<th>Cores</th>
<th>Quick Links</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr>
<td>{{ item }}</td>
<td>{{ item.circuits }}</td>
<td>{{ item.cores }}</td>
<td>
<a href="{% url 'cable_type_detail' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-eye-open"></i> View</a>
<a href="{% url 'cable_type_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<div class="text-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}

View File

@@ -1,28 +1,25 @@
{% if perms.assets.change_asset %} {% if edit and object %}
{% if edit and object %} <!--edit-->
<!--edit--> <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> <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-default"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
<button type="submit" class="btn btn-success"><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> {% else %}
{% else %} <!--detail view-->
<!--detail view--> <div class="btn-group">
<div class="btn-group"> <a href="{% url 'asset_update' object.asset_id %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
<a href="{% url 'asset_update' object.asset_id %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a> <a class="btn btn-default" href="{% url 'asset_duplicate' object.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
<a class="btn btn-default" href="{% url 'asset_duplicate' object.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a> </div>
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><i class="glyphicon glyphicon-object-align-left"></i> Audit</a> {% endif %}
</div> {% if create or edit or duplicate %}
{% endif %} <br>
{% if create or edit or duplicate %} <button type="reset" class="btn btn-link" onclick="
<br> {%if duplicate%}
<button type="reset" class="btn btn-link" onclick=" {% url 'asset_detail' previous_asset_id %}
{%if duplicate%} {%else%}
{% url 'asset_detail' previous_asset_id %} history.back(){%endif%}">Cancel</button>
{%else%}
history.back(){%endif%}">Cancel</button>
{% endif %}
{% endif %} {% endif %}

View File

@@ -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
@@ -23,6 +24,7 @@
<label for="{{ form.category.id_for_label }}" >Category</label> <label for="{{ form.category.id_for_label }}" >Category</label>
{% render_field form.category|add_class:'form-control'%} {% render_field form.category|add_class:'form-control'%}
</div> </div>
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %} <label for="{{ form.is_cable.id_for_label }}">Cable?</label>
<div class="form-group"> <div class="form-group">
<label for="{{ form.status.id_for_label }}" >Status</label> <label for="{{ form.status.id_for_label }}" >Status</label>
{% render_field form.status|add_class:'form-control'%} {% render_field form.status|add_class:'form-control'%}
@@ -31,10 +33,6 @@
<label for="{{ form.serial_number.id_for_label }}">Serial Number</label> <label for="{{ form.serial_number.id_for_label }}">Serial Number</label>
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %} {% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
</div> </div>
<div class="form-group">
<label for="{{ form.is_cable.id_for_label }}">Cable?</label>
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %}
</div>
<!---TODO: Lower default number of lines in comments box--> <!---TODO: Lower default number of lines in comments box-->
<div class="form-group"> <div class="form-group">
<label for="{{ form.comments.id_for_label }}">Comments</label> <label for="{{ form.comments.id_for_label }}">Comments</label>

View File

@@ -1,21 +1,32 @@
{% for item in object_list %} {% for item in object_list %}
<tr class="{{ item.status.display_class|default:'' }} assetRow" id="{{item.asset_id}}"> {# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
<td style="vertical-align: middle;"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td> <!---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="
<td class="assetDesc" style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td> {% if item.status.name == 'Broken' %}
<td class="assetCategory" style="vertical-align: middle;">{{ item.category }}</td> danger
<td class="assetStatus" style="vertical-align: middle;">{{ item.status }}</td> {% elif item.status.name == 'Scrapped'%}
warning
{% 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">
{% if audit %}
<a type="button" class="btn btn-info modal-href" href="{% url 'asset_audit' item.asset_id %}"><i class="glyphicon glyphicon-object-align-left"></i> Audit</a>
{% else %}
<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>
{% if perms.assets.change_asset %} {% if perms.assets.change_asset %}
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_update' item.asset_id %}"><i class="glyphicon glyphicon-edit"></i> Edit</a> <a type="button" class="btn btn-default btn-sm" href="{% url 'asset_update' item.asset_id %}"><i class="glyphicon glyphicon-edit"></i> Edit</a>
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_duplicate' item.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a> <a type="button" class="btn btn-default btn-sm" href="{% url 'asset_duplicate' item.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -1,8 +0,0 @@
<div class="panel {% if object.last_audited_at is not None %} panel-success {% else %} panel-warning {% endif %}">
<div class="panel-heading">
Audit Details
</div>
<div class="panel-body">
<p>Audited at <span class="label label-default">{{ object.last_audited_at|default_if_none:'-' }}</span> by <span class="label label-info">{{ object.last_audited_by|default_if_none:'-' }}</span></p>
</div>
</div>

View File

@@ -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
@@ -6,10 +7,12 @@
<div class="panel-body"> <div class="panel-body">
{% if create or edit or duplicate %} {% if create or edit or duplicate %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.cable_type.id_for_label }}">Cable Type</label> <label for="{{ form.plug.id_for_label }}">Plug</label>
<div class="input-group"> {% render_field form.plug|add_class:'form-control'%}
{% render_field form.cable_type|add_class:'form-control' %} </div>
</div> <div class="form-group">
<label for="{{ form.socket.id_for_label }}">Socket</label>
{% render_field form.socket|add_class:'form-control'%}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.length.id_for_label }}">Length</label> <label for="{{ form.length.id_for_label }}">Length</label>
@@ -25,16 +28,33 @@
<span class="input-group-addon">{{ form.csa.help_text }}</span> <span class="input-group-addon">{{ form.csa.help_text }}</span>
</div> </div>
</div> </div>
<div class="form-group">
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
</div>
<div class="form-group">
<label for="{{ form.cores.id_for_label }}">Cores</label>
{% render_field form.cores|add_class:'form-control' value=object.cores %}
</div>
{% else %} {% else %}
<dl> <dl>
<dt>Cable Type</dt> <dt>Socket</dt>
<dd>{{ object.cable_type|default_if_none:'-' }}</dd> <dd>{{ object.socket|default_if_none:'-' }}</dd>
<dt>Plug</dt>
<dd>{{ object.plug|default_if_none:'-' }}</dd>
<dt>Length</dt> <dt>Length</dt>
<dd>{{ object.length|default_if_none:'-' }}m</dd> <dd>{{ object.length|default_if_none:'-' }}m</dd>
<dt>Cross Sectional Area</dt> <dt>Cross Sectional Area</dt>
<dd>{{ object.csa|default_if_none:'-' }}m</dd> <dd>{{ object.csa|default_if_none:'-' }}m^2</dd>
<dt>Circuits</dt>
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
<dt>Cores</dt>
<dd>{{ object.cores|default_if_none:'-' }}</dd>
</dl> </dl>
{% endif %} {% endif %}
</div> </div>

View File

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

View File

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

View 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 %}

View File

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

View File

@@ -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,11 +27,11 @@
</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>
<a href="{% url 'supplier_history' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-time"></i> History</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

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

View 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'))

View File

@@ -1,262 +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
from selenium.common.exceptions import NoSuchElementException
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')),
'cable_type': (regions.SingleSelectPicker, (By.ID, 'id_cable_type')),
'length': (regions.TextBox, (By.ID, 'id_length')),
'csa': (regions.TextBox, (By.ID, 'id_csa')),
}
@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))
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')),
}
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
class AssetAuditList(AssetList):
URL_TEMPLATE = reverse('asset_audit_list')
_search_text_locator = (By.ID, 'id_query')
_go_button_locator = (By.ID, 'searchButton')
_modal_locator = (By.ID, 'modal')
_errors_selector = (By.CLASS_NAME, "alert-danger")
@property
def modal(self):
return self.AssetAuditModal(self, self.find_element(*self._modal_locator))
@property
def query(self):
return self.find_element(*self._search_text_locator).text
def set_query(self, queryString):
element = self.find_element(*self._search_text_locator)
element.clear()
element.send_keys(queryString)
def search(self):
self.find_element(*self._go_button_locator).click()
@property
def error(self):
try:
return self.find_element(*self._errors_selector)
except NoSuchElementException:
return None
class AssetAuditModal(Region):
_errors_selector = (By.CLASS_NAME, "alert-danger")
# Don't use the usual success selector - that tries and fails to hit the '10m long cable' helper button...
_submit_locator = (By.ID, "id_mark_audited")
form_items = {
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
'description': (regions.TextBox, (By.ID, 'id_description')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
'plug': (regions.SingleSelectPicker, (By.ID, 'id_plug')),
'socket': (regions.SingleSelectPicker, (By.ID, 'id_socket')),
'length': (regions.TextBox, (By.ID, 'id_length')),
'csa': (regions.TextBox, (By.ID, 'id_csa')),
'circuits': (regions.TextBox, (By.ID, 'id_circuits')),
'cores': (regions.TextBox, (By.ID, 'id_cores'))
}
@property
def errors(self):
try:
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
return error_page.errors
except NoSuchElementException:
return None
def submit(self):
previous_errors = self.errors
self.root.find_element(*self._submit_locator).click()
# self.wait.until(lambda x: not self.is_displayed) TODO
def remove_all_required(self):
self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
def __getattr__(self, name):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
return form_element.value
else:
return super().__getattribute__(name)
def __setattr__(self, name, value):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
form_element.set_value(value)
else:
self.__dict__[name] = value

View File

@@ -1,650 +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.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from RIGS.test_functional import animation_is_finished
import datetime
from django.utils import timezone
class TestAssetList(AutoLoginTest):
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.cable_type = models.CableType.objects.create(plug=self.connector, socket=self.connector, circuits=1, cores=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.cable_type = "IEC → IEC"
self.page.socket = "IEC"
self.page.length = 10
self.page.csa = "1.5"
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 TestAssetAudit(AutoLoginTest):
def setUp(self):
super().setUp()
self.category = models.AssetCategory.objects.create(name="Haulage")
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
self.supplier = models.Supplier.objects.create(name="The Bazaar")
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1, voltage_rating=40, num_pins=13)
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
self.wait = WebDriverWait(self.driver, 5)
def test_audit_process(self):
asset_id = "1111"
self.page.set_query(asset_id)
self.page.search()
mdl = self.page.modal
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
# Do it wrong on purpose to check error display
mdl.remove_all_required()
mdl.description = ""
mdl.submit()
# self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
self.wait.until(animation_is_finished())
# self.assertTrue(self.driver.find_element_by_id('modal').is_displayed())
self.assertIn("This field is required.", mdl.errors["Description"])
# Now do it properly
new_desc = "A BIG hammer"
mdl.description = new_desc
mdl.submit()
self.wait.until(animation_is_finished())
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
# Check data is correct
audited = models.Asset.objects.get(asset_id="1111")
self.assertEqual(audited.description, new_desc)
# Make sure audit 'log' was filled out
self.assertEqual(self.profile.initials, audited.last_audited_by.initials)
self.assertEqual(timezone.now().date(), audited.last_audited_at.date())
self.assertEqual(timezone.now().hour, audited.last_audited_at.hour)
self.assertEqual(timezone.now().minute, audited.last_audited_at.minute)
# Check we've removed it from the 'needing audit' list
self.assertNotIn(asset_id, self.page.assets)
def test_audit_list(self):
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
assetRow = self.page.assets[0]
assetRow.find_element(By.CSS_SELECTOR, "td:nth-child(5) > div:nth-child(1) > a:nth-child(1)").click()
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
self.assertEqual(self.page.modal.asset_id, assetRow.id)
# First close button is for the not found error
self.page.find_element(By.XPATH, '(//button[@class="close"])[2]').click()
self.wait.until(animation_is_finished())
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
# Make sure audit log was NOT filled out
audited = models.Asset.objects.get(asset_id=assetRow.id)
self.assertEqual(None, audited.last_audited_by)
# Check that a failed search works
self.page.set_query("NOTFOUND")
self.page.search()
self.wait.until(animation_is_finished())
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
self.assertIn("Asset with that ID does not exist!", self.page.error.text)
class TestSupplierValidation(TestCase):
@classmethod
def setUpTestData(cls):
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_type = models.CableType.objects.create(circuits=11, cores=3, plug=cls.connector, socket=cls.connector)
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, cable_type=cls.cable_type, length=10, csa="1.5")
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', 'cable_type', 'A cable must have a type')
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')
# 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})
# TODO Can't figure out how to select the 'none' option...
# self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
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')
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})
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')
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)

View File

@@ -3,41 +3,24 @@ from django.urls import path
from assets import views, models from assets import views, models
from RIGS import versioning 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') path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset')
(views.AssetEdit.as_view()), name='asset_update'), (views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<str:pk>/history/', permission_required_with_403('assets.view_asset')(views.AssetVersionHistory.as_view()), path('asset/id/<str:pk>/history/', views.AssetVersionHistory.as_view(),
name='asset_history', kwargs={'model': models.Asset}), name='asset_history', kwargs={'model': models.Asset}),
path('activity', permission_required_with_403('assets.view_asset') path('activity', permission_required_with_403('assets.view_asset')
(views.ActivityTable.as_view()), name='asset_activity_table'), (views.ActivityTable.as_view()), name='asset_activity_table'),
path('cabletype/list/', permission_required_with_403('assets.view_cable_type')(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
path('cabletype/<int:pk>/detail/', permission_required_with_403('assets.view_cable_type')(views.CableTypeDetail.as_view()), name='cable_type_detail'),
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('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'),
path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'),
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'),
@@ -45,7 +28,7 @@ urlpatterns = [
(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') path('supplier/<int:pk>/edit', permission_required_with_403('assets.change_supplier')
(views.SupplierUpdate.as_view()), name='supplier_update'), (views.SupplierUpdate.as_view()), name='supplier_update'),
path('supplier/<int:pk>/history/', views.SupplierVersionHistory.as_view(), path('supplier/<str:pk>/history/', views.SupplierVersionHistory.as_view(),
name='supplier_history', kwargs={'model': models.Supplier}), 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'),

View File

@@ -1,21 +1,14 @@
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_lazy, 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 django.shortcuts import get_object_or_404
from django.core import serializers
from django.contrib import messages
from assets import models, forms from assets import models, forms
from RIGS import versioning from RIGS import versioning
import simplejson
import datetime
from django.utils import timezone
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class AssetList(LoginRequiredMixin, generic.ListView): class AssetList(LoginRequiredMixin, generic.ListView):
@@ -46,7 +39,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
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(serial_number__exact=query_string)) Q(asset_id__exact=query_string) | Q(description__icontains=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))
@@ -91,7 +84,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
@@ -113,14 +107,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
return context return context
def get_success_url(self): def get_success_url(self):
if self.request.is_ajax(): return reverse("asset_detail", kwargs={"pk": self.object.asset_id})
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
else:
url = reverse_lazy('asset_detail', kwargs={'pk': self.object.asset_id, })
return url
class AssetCreate(LoginRequiredMixin, generic.CreateView): class AssetCreate(LoginRequiredMixin, generic.CreateView):
@@ -162,52 +149,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'
@method_decorator(csrf_exempt, name='dispatch')
class AssetAuditList(AssetList):
template_name = 'asset_audit_list.html'
hide_hidden_status = False
# TODO Refresh this when the modal is submitted
def get_queryset(self):
self.form = forms.AssetSearchForm(data={})
return self.model.objects.filter(Q(last_audited_at__isnull=True))
class AssetAudit(AssetEdit):
template_name = 'asset_audit.html'
form_class = forms.AssetAuditForm
def get_success_url(self):
# TODO For some reason this doesn't stick when done in form_valid??
asset = self.get_object()
asset.last_audited_by = self.request.user
asset.last_audited_at = timezone.now()
asset.save()
return super().get_success_url()
class SupplierList(generic.ListView): class SupplierList(generic.ListView):
model = models.Supplier model = models.Supplier
template_name = 'supplier_list.html' template_name = 'supplier_list.html'
@@ -247,6 +188,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)
@@ -271,9 +213,8 @@ class SupplierVersionHistory(versioning.VersionHistory):
template_name = "asset_version_history.html" template_name = "asset_version_history.html"
# TODO: Reduce SQL queries
class AssetVersionHistory(versioning.VersionHistory): class AssetVersionHistory(versioning.VersionHistory):
template_name = "asset_version_history.html"
def get_object(self, **kwargs): def get_object(self, **kwargs):
return get_object_or_404(models.Asset, asset_id=self.kwargs['pk']) return get_object_or_404(models.Asset, asset_id=self.kwargs['pk'])
@@ -287,45 +228,3 @@ class ActivityTable(versioning.ActivityTable):
versions = versioning.RIGSVersion.objects.get_for_multiple_models( versions = versioning.RIGSVersion.objects.get_for_multiple_models(
[models.Asset, models.Supplier]) [models.Asset, models.Supplier])
return versions return versions
class CableTypeList(generic.ListView):
model = models.CableType
template_name = 'cable_type_list.html'
paginate_by = 40
# ordering = ['__str__']
class CableTypeDetail(generic.DetailView):
model = models.CableType
template_name = 'cable_type_form.html'
class CableTypeCreate(generic.CreateView):
model = models.CableType
template_name = "cable_type_form.html"
form_class = forms.CableTypeForm
def get_context_data(self, **kwargs):
context = super(CableTypeCreate, self).get_context_data(**kwargs)
context["create"] = True
return context
def get_success_url(self):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
class CableTypeUpdate(generic.UpdateView):
model = models.CableType
template_name = "cable_type_form.html"
form_class = forms.CableTypeForm
def get_context_data(self, **kwargs):
context = super(CableTypeUpdate, self).get_context_data(**kwargs)
context["edit"] = True
return context
def get_success_url(self):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk})

View File

@@ -1,25 +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
selenium==3.141.0
simplejson==3.17.0
whitenoise==5.0.1
reportlab==3.4.0 reportlab==3.4.0
z3c.rml==3.9.1 selenium==3.12.0
simplejson==3.15.0
six==1.11.0
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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More