Compare commits
170 Commits
optimisati
...
6b19d0e8b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
6b19d0e8b8
|
|||
|
3e8cfe4f11
|
|||
|
7a70270dfd
|
|||
|
5160eb7f78
|
|||
|
99e05d91bb
|
|||
|
f094ace862
|
|||
|
945fb393c0
|
|||
|
c6157d3e2b
|
|||
|
2767777d0e
|
|||
|
19e6585e26
|
|||
|
747575b968
|
|||
|
046d0e461d
|
|||
|
b73b8401b6
|
|||
|
3fe388af26
|
|||
|
22dc83d595
|
|||
|
70d4c42676
|
|||
|
0727a23236
|
|||
| f0b3a6daf3 | |||
| 3c5f6da363 | |||
| ee9be86465 | |||
| 5554edf977 | |||
| 14b73f6f50 | |||
| 732affa0b2 | |||
| 7c830ee7e5 | |||
| d47d00d79b | |||
| 3b5b3b84d4 | |||
| aa8be6a6d0 | |||
| 640362c203 | |||
| 71a8823ac2 | |||
| c6de3dc9e2 | |||
|
|
a01e351e89 | ||
| 8696bf5d94 | |||
| 0d9bf89180 | |||
| f4f2fbdc03 | |||
| 67aaada9e8 | |||
| fcae39c93c | |||
| d1970edfb3 | |||
| ce5efff268 | |||
|
|
708a387774 | ||
|
|
af6fe582e0 | ||
|
|
905a144e7d | ||
| 0e64021f01 | |||
| 2eb87a51f8 | |||
| 30fac1d1b9 | |||
|
c4fec483ae
|
|||
|
3028fb92d9
|
|||
| 215697ba64 | |||
|
d966bddfd7
|
|||
|
0d5e48b89c
|
|||
|
bd2c94d3e3
|
|||
|
21d09d951d
|
|||
|
014b00bc30
|
|||
|
3f8fc82260
|
|||
| 41c1c44754 | |||
|
522837c64e
|
|||
| e78decdf92 | |||
| 84a3c9db24 | |||
|
280a1d9604
|
|||
|
a184bbfa26
|
|||
|
4416e5bfcb
|
|||
|
21276bcca0
|
|||
|
bc465d67e9
|
|||
|
a644735cd6
|
|||
|
8a2b107516
|
|||
|
10326f884f
|
|||
|
f8c52803a5
|
|||
|
85d1850f08
|
|||
|
e146d9314a
|
|||
|
2c3dff79ba
|
|||
|
9ee8cd0f8b
|
|||
|
0a0c9f15af
|
|||
|
dbb9e3e530
|
|||
|
55558d1a4a
|
|||
|
d3391d9e3e
|
|||
|
|
0086461d6c | ||
|
8bafeabe5f
|
|||
|
f214f9a835
|
|||
|
b31d53a3c5
|
|||
|
62a891c6ec
|
|||
|
8c0c0941c2
|
|||
|
abb0e35690
|
|||
|
bec0d4aee5
|
|||
|
f1e43b707e
|
|||
| 796f5b44b0 | |||
| 6458f016f0 | |||
| 9ca953423f | |||
| 081c33ebc8 | |||
| 75410db752 | |||
| 06c6b9a36e | |||
| 13b1cea28b | |||
|
|
4c5d958c6d | ||
| 85ca7b0880 | |||
|
cddb76bf7e
|
|||
|
f4f1fb66a2
|
|||
|
44f9509eda
|
|||
|
a2be4cbe5e
|
|||
|
|
bb2f369ab5 | ||
|
2fdb2f260f
|
|||
|
d80aeca01f
|
|||
|
6de3cb5d8c
|
|||
|
45dfe2db51
|
|||
|
7c38af66f6
|
|||
|
f1a624ec8f
|
|||
|
ab01beb2cd
|
|||
|
de5997b9da
|
|||
|
4a121964dc
|
|||
|
df5e4c8e0a
|
|||
|
3601c14ab7
|
|||
|
adde6496f5
|
|||
|
ad734d94b2
|
|||
|
7d3ada822d
|
|||
|
732af53fda
|
|||
|
4fb0529cc0
|
|||
|
aa23b1cd09
|
|||
|
0c4228da57
|
|||
|
246a52d19e
|
|||
|
8b10aaf700
|
|||
|
4d0d4f02aa
|
|||
|
af987c1ebb
|
|||
|
d406a911bb
|
|||
|
63c5a68933
|
|||
|
66f7f830db
|
|||
|
9590c2066d
|
|||
|
8b48b02ca7
|
|||
|
68e7ec2a0d
|
|||
|
11636809ca
|
|||
|
5779ebdf7e
|
|||
|
d7458f6366
|
|||
| febf9cf3ed | |||
| 3322a5ddf8 | |||
| be648c20d5 | |||
| b6ef7c1d89 | |||
| 85f40b358a | |||
| 2698798035 | |||
| dbaab5cf8c | |||
| 0a9f82e480 | |||
| 54f2bd36bd | |||
| e836195fef | |||
| 68a424d62b | |||
| 5e15b8bb59 | |||
| d26c1b535e | |||
| dff5ac2308 | |||
| a3729fa930 | |||
| 458a734331 | |||
| b1646d556c | |||
| f8624d3b7a | |||
| f6836fdab6 | |||
|
|
673bee4215 | ||
| b3949f2903 | |||
|
|
bab31107f7 | ||
|
|
2d8473b698 | ||
| d81ecd9015 | |||
| b42c583897 | |||
| 57e966826e | |||
|
6a5de4a9d6
|
|||
| 56bbf4c17c | |||
|
|
698f0be281 | ||
|
|
483f06e96f | ||
|
|
22193f3c39 | ||
|
|
59b63fe7aa | ||
| 5976ce9ea2 | |||
|
780d05e27c
|
|||
| 8cfa4bd79d | |||
|
36f83ee59b
|
|||
|
6d768832f4
|
|||
|
38da8642fa
|
|||
|
f75e1d5bfc
|
|||
|
3f959f8d56
|
|||
|
b63a01120b
|
|||
| 911336ceec |
@@ -1,3 +1,5 @@
|
||||
[run]
|
||||
plugins = django_coverage_plugin
|
||||
omit = */migrations/*, */tests/*
|
||||
omit = */migrations/*
|
||||
*/tests/*
|
||||
*/site-packages/*
|
||||
*/distutils/*
|
||||
|
||||
46
.github/workflows/django.yml
vendored
@@ -10,38 +10,44 @@ jobs:
|
||||
build:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
runs-on: ubuntu-latest
|
||||
# strategy:
|
||||
# matrix:
|
||||
# browser: ['chrome']
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# BROWSER: ${{ matrix.browser }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: node node_modules/gulp/bin/gulp build
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Cache python deps
|
||||
uses: actions/cache@v2
|
||||
python-version: 3.9.1
|
||||
- uses: actions/cache@v2
|
||||
id: pcache
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }}
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pycodestyle coveralls django_coverage_plugin pytest-cov
|
||||
pip install --upgrade --upgrade-strategy eager -r requirements.txt
|
||||
python manage.py collectstatic --noinput
|
||||
python -m pip install --upgrade pip pipenv
|
||||
pipenv install -d
|
||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||
- name: Cache Static Files
|
||||
id: static-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: 'pipeline/built_assets'
|
||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
||||
- run: node node_modules/gulp/bin/gulp build
|
||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
||||
- name: Basic Checks
|
||||
run: |
|
||||
pycodestyle . --exclude=migrations,node_modules
|
||||
python manage.py check
|
||||
python manage.py makemigrations --check --dry-run
|
||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
||||
pipenv run python manage.py check
|
||||
pipenv run python manage.py makemigrations --check --dry-run
|
||||
pipenv run python manage.py collectstatic --noinput
|
||||
- name: Run Tests
|
||||
run: pytest --cov -n 8
|
||||
run: pipenv run pytest -n auto -vv --cov
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
@@ -49,4 +55,4 @@ jobs:
|
||||
path: screenshots/
|
||||
retention-days: 5
|
||||
- name: Coveralls
|
||||
run: coveralls --service=github
|
||||
run: pipenv run coveralls --service=github
|
||||
|
||||
1
.gitignore
vendored
@@ -26,6 +26,7 @@ var/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
node_modules/
|
||||
data/
|
||||
|
||||
# Continer extras
|
||||
.vagrant
|
||||
|
||||
96
Pipfile
Normal file
@@ -0,0 +1,96 @@
|
||||
[[source]]
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
ansicolors = "~=1.1.8"
|
||||
asgiref = "~=3.3.1"
|
||||
"backports.tempfile" = "~=1.0"
|
||||
"backports.weakref" = "~=1.0.post1"
|
||||
beautifulsoup4 = "~=4.9.3"
|
||||
Brotli = "~=1.0.9"
|
||||
cachetools = "~=4.2.1"
|
||||
certifi = "~=2020.12.5"
|
||||
chardet = "~=4.0.0"
|
||||
configparser = "~=5.0.1"
|
||||
contextlib2 = "~=0.6.0.post1"
|
||||
cssselect = "~=1.1.0"
|
||||
cssutils = "~=1.0.2"
|
||||
dj-database-url = "~=0.5.0"
|
||||
dj-static = "~=0.0.6"
|
||||
Django = "~=3.2"
|
||||
django-debug-toolbar = "~=3.2"
|
||||
django-filter = "~=2.4.0"
|
||||
django-ical = "~=1.7.1"
|
||||
django-recurrence = "~=1.10.3"
|
||||
django-registration-redux = "~=2.9"
|
||||
django-reversion = "~=3.0.9"
|
||||
django-toolbelt = "~=0.0.1"
|
||||
django-widget-tweaks = "~=1.4.8"
|
||||
django-htmlmin = "~=0.11.0"
|
||||
envparse = "~=0.2.0"
|
||||
gunicorn = "~=20.0.4"
|
||||
icalendar = "~=4.0.7"
|
||||
idna = "~=2.10"
|
||||
importlib-metadata = "~=3.4.0"
|
||||
lxml = "~=4.6.3"
|
||||
Markdown = "~=3.3.3"
|
||||
msgpack = "~=1.0.2"
|
||||
pep517 = "~=0.9.1"
|
||||
Pillow = "~=8.3.2"
|
||||
premailer = "~=3.7.0"
|
||||
progress = "~=1.5"
|
||||
psutil = "~=5.8.0"
|
||||
psycopg2 = "~=2.8.6"
|
||||
Pygments = "~=2.7.4"
|
||||
pyparsing = "~=2.4.7"
|
||||
PyPDF2 = "~=1.26.0"
|
||||
PyPOM = "~=2.2.0"
|
||||
python-dateutil = "~=2.8.1"
|
||||
pytoml = "~=0.1.21"
|
||||
pytz = "~=2020.5"
|
||||
reportlab = "~=3.5.59"
|
||||
requests = "~=2.25.1"
|
||||
retrying = "~=1.3.3"
|
||||
simplejson = "~=3.17.2"
|
||||
six = "~=1.15.0"
|
||||
soupsieve = "~=2.1"
|
||||
sqlparse = "~=0.4.2"
|
||||
static3 = "~=0.7.0"
|
||||
svg2rlg = "~=0.3"
|
||||
tini = "~=3.0.1"
|
||||
tornado = "~=6.1"
|
||||
urllib3 = "~=1.26.5"
|
||||
whitenoise = "~=5.2.0"
|
||||
yolk = "~=0.4.3"
|
||||
"z3c.rml" = "~=4.1.2"
|
||||
zipp = "~=3.4.0"
|
||||
"zope.component" = "~=4.6.2"
|
||||
"zope.deferredimport" = "~=4.3.1"
|
||||
"zope.deprecation" = "~=4.4.0"
|
||||
"zope.event" = "~=4.5.0"
|
||||
"zope.hookable" = "~=5.0.1"
|
||||
"zope.interface" = "~=5.2.0"
|
||||
"zope.proxy" = "~=4.3.5"
|
||||
"zope.schema" = "~=6.0.1"
|
||||
sentry-sdk = "*"
|
||||
diff-match-patch = "*"
|
||||
python-barcode = "*"
|
||||
django-hCaptcha = "*"
|
||||
|
||||
[dev-packages]
|
||||
selenium = "~=3.141.0"
|
||||
pycodestyle = "*"
|
||||
coveralls = "*"
|
||||
django-coverage-plugin = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-django = "*"
|
||||
pluggy = "*"
|
||||
pytest-splinter = "*"
|
||||
pytest = "*"
|
||||
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
||||
PyPOM = {extras = [ "splinter",], version = "*"}
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
1526
Pipfile.lock
generated
Normal file
0
PyRIGS/management/__init__.py
Normal file
0
PyRIGS/management/commands/__init__.py
Normal file
@@ -9,27 +9,21 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||
"""
|
||||
|
||||
import datetime
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
|
||||
import raven
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from envparse import env
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
|
||||
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env('DEBUG', cast=bool, default=True)
|
||||
|
||||
STAGING = env('STAGING', cast=bool, default=False)
|
||||
|
||||
CI = env('CI', cast=bool, default=False)
|
||||
|
||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||
@@ -55,6 +49,7 @@ if DEBUG:
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = (
|
||||
'whitenoise.runserver_nostatic',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@@ -66,17 +61,16 @@ INSTALLED_APPS = (
|
||||
'users',
|
||||
'RIGS',
|
||||
'assets',
|
||||
'training',
|
||||
|
||||
'debug_toolbar',
|
||||
'registration',
|
||||
'reversion',
|
||||
'captcha',
|
||||
'widget_tweaks',
|
||||
'raven.contrib.django.raven_compat',
|
||||
'hcaptcha',
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
@@ -87,19 +81,19 @@ MIDDLEWARE = (
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'htmlmin.middleware.HtmlMinifyMiddleware',
|
||||
'htmlmin.middleware.MarkRequestMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'PyRIGS.urls'
|
||||
|
||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
'NAME': str(BASE_DIR / 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,9 +171,12 @@ else:
|
||||
}
|
||||
}
|
||||
|
||||
RAVEN_CONFIG = {
|
||||
'dsn': env('RAVEN_DSN', default=""),
|
||||
}
|
||||
# Error/performance monitoring
|
||||
sentry_sdk.init(
|
||||
dsn=env('SENTRY_DSN', default=""),
|
||||
integrations=[DjangoIntegration()],
|
||||
traces_sample_rate=1.0,
|
||||
)
|
||||
|
||||
# User system
|
||||
AUTH_USER_MODEL = 'RIGS.Profile'
|
||||
@@ -190,12 +187,9 @@ LOGOUT_URL = '/user/logout/'
|
||||
|
||||
ACCOUNT_ACTIVATION_DAYS = 7
|
||||
|
||||
# reCAPTCHA settings
|
||||
RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
|
||||
RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
|
||||
NOCAPTCHA = True
|
||||
|
||||
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
|
||||
# CAPTCHA settings
|
||||
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
|
||||
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
|
||||
|
||||
# Email
|
||||
EMAILER_TEST = False
|
||||
@@ -232,21 +226,18 @@ USE_TZ = True
|
||||
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||
STATIC_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static/')
|
||||
)
|
||||
STATIC_ROOT = str(BASE_DIR / 'static/')
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'pipeline/built_assets/'),
|
||||
str(BASE_DIR / 'pipeline/built_assets'),
|
||||
]
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates')
|
||||
BASE_DIR / 'templates'
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
@@ -269,3 +260,5 @@ USE_GRAVATAR = True
|
||||
|
||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
@@ -11,30 +11,22 @@ from selenium.webdriver.support.wait import WebDriverWait
|
||||
|
||||
from RIGS import models as rigsmodels
|
||||
from . import pages
|
||||
from envparse import env
|
||||
|
||||
from pytest_django.asserts import assertContains
|
||||
|
||||
|
||||
def create_datetime(year, month, day, hour, min):
|
||||
def create_datetime(year, month, day, hour, minute):
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
|
||||
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz)
|
||||
|
||||
|
||||
def create_browser():
|
||||
browser = env('BROWSER', default="chrome")
|
||||
if browser == "firefox":
|
||||
options = webdriver.FirefoxOptions()
|
||||
options.headless = True
|
||||
driver = webdriver.Firefox(options=options)
|
||||
driver.set_window_position(0, 0)
|
||||
# Firefox is pissy about out of bounds otherwise
|
||||
driver.set_window_size(3840, 2160)
|
||||
else:
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
options.add_argument("--headless")
|
||||
if settings.CI:
|
||||
options.add_argument("--no-sandbox")
|
||||
driver = webdriver.Chrome(options=options)
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
options.add_argument("--headless")
|
||||
if settings.CI:
|
||||
options.add_argument("--no-sandbox")
|
||||
driver = webdriver.Chrome(options=options)
|
||||
return driver
|
||||
|
||||
|
||||
@@ -60,6 +52,7 @@ class AutoLoginTest(BaseTest):
|
||||
login_page.login("EventTest", "EventTestPassword")
|
||||
|
||||
|
||||
# FIXME Refactor as a pytest fixture
|
||||
def screenshot_failure(func):
|
||||
def wrapper_func(self, *args, **kwargs):
|
||||
try:
|
||||
@@ -83,5 +76,30 @@ def screenshot_failure_cls(cls):
|
||||
return cls
|
||||
|
||||
|
||||
def assert_times_equal(first_time, second_time):
|
||||
def assert_times_almost_equal(first_time, second_time):
|
||||
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
|
||||
|
||||
|
||||
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
|
||||
# Test the meta tag is in place
|
||||
response = client.get(event_url, follow=True, HTTP_HOST='example.com')
|
||||
assertContains(response, 'application/json+oembed')
|
||||
assertContains(response, oembed_url)
|
||||
# Test that the JSON exists
|
||||
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
assert response.status_code == 200
|
||||
assertContains(response, event_embed_url)
|
||||
# Should also work for non-existant events
|
||||
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
assert response.status_code == 200
|
||||
assertContains(response, alt_event_embed_url)
|
||||
|
||||
|
||||
def login(client, django_user_model):
|
||||
pwd = 'testuser'
|
||||
usr = 'TestUser'
|
||||
user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
|
||||
is_superuser=True,
|
||||
is_active=True, is_staff=True)
|
||||
assert client.login(username=usr, password=pwd)
|
||||
return user
|
||||
|
||||
@@ -71,6 +71,7 @@ class BootstrapSelectElement(Region):
|
||||
self.find_element(*self._deselect_all_locator).click()
|
||||
|
||||
def search(self, query):
|
||||
# self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
|
||||
search_box = self.find_element(*self._search_locator)
|
||||
self.open()
|
||||
search_box.clear()
|
||||
@@ -116,6 +117,15 @@ class TextBox(Region):
|
||||
self.root.send_keys(value)
|
||||
|
||||
|
||||
class SimpleMDETextArea(Region):
|
||||
@property
|
||||
def value(self):
|
||||
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
|
||||
|
||||
def set_value(self, value):
|
||||
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
|
||||
|
||||
|
||||
class CheckBox(Region):
|
||||
def toggle(self):
|
||||
self.root.click()
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
from PyRIGS import urls
|
||||
from assets.tests.test_unit import create_asset_one
|
||||
import pytest
|
||||
from django.urls import URLPattern, URLResolver, reverse
|
||||
from django.core.management import call_command
|
||||
from django.template.defaultfilters import striptags
|
||||
from django.urls import URLPattern, URLResolver
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from pytest_django.asserts import assertContains, assertRedirects, assertTemplateUsed, assertInHTML
|
||||
from pytest_django.asserts import assertRedirects, assertContains, assertNotContains
|
||||
from pytest_django.asserts import assertTemplateUsed, assertInHTML
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
from PyRIGS import urls
|
||||
from RIGS.models import Event
|
||||
from assets.models import Asset
|
||||
from django.db import connection
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.template.defaultfilters import striptags
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
|
||||
from RIGS.models import Event
|
||||
from assets.models import Asset
|
||||
from django.db import connection
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
||||
def find_urls_recursive(patterns):
|
||||
@@ -14,7 +29,7 @@ def find_urls_recursive(patterns):
|
||||
if isinstance(url, URLResolver):
|
||||
urls_to_check += find_urls_recursive(url.url_patterns)
|
||||
elif isinstance(url, URLPattern):
|
||||
# Skip some thinks that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
|
||||
# Skip some things that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
|
||||
if url.name is not None and url.name != "closemodal" and "json" not in str(url):
|
||||
urls_to_check.append(url)
|
||||
return urls_to_check
|
||||
@@ -22,7 +37,6 @@ def find_urls_recursive(patterns):
|
||||
|
||||
def get_request_url(url):
|
||||
pattern = str(url.pattern)
|
||||
request_url = ""
|
||||
try:
|
||||
kwargz = {}
|
||||
if ":pk>" in pattern:
|
||||
@@ -34,32 +48,98 @@ def get_request_url(url):
|
||||
print("Couldn't test url " + pattern)
|
||||
|
||||
|
||||
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
|
||||
create_asset_one()
|
||||
for url in find_urls_recursive(urls.urlpatterns):
|
||||
request_url = get_request_url(url)
|
||||
if request_url and 'user' not in request_url: # User module is full of edge cases
|
||||
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||
assertContains(response, 'Login')
|
||||
if 'application/json+oembed' in response.content.decode():
|
||||
assertTemplateUsed(response, 'login_redirect.html')
|
||||
else:
|
||||
if "embed" in str(url):
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
|
||||
'deleteSampleData'])
|
||||
def test_production_exception(command):
|
||||
from django.core.management.base import CommandError
|
||||
with pytest.raises(CommandError, match=".*production"):
|
||||
call_command(command)
|
||||
|
||||
|
||||
class TestSampleDataGenerator(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def test_sample_data(self):
|
||||
call_command('generateSampleData')
|
||||
assert Asset.objects.all().count() > 50
|
||||
assert Event.objects.all().count() > 100
|
||||
call_command('deleteSampleData')
|
||||
assert Asset.objects.all().count() == 0
|
||||
assert Event.objects.all().count() == 0
|
||||
|
||||
|
||||
class TestSampleDataGenerator(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def setUp(self):
|
||||
call_command('generateSampleData')
|
||||
|
||||
def test_unauthenticated(self): # Nothing should be available to the unauthenticated
|
||||
for url in find_urls_recursive(urls.urlpatterns):
|
||||
request_url = get_request_url(url)
|
||||
if request_url and 'user' not in request_url: # User module is full of edge cases
|
||||
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||
assertContains(response, 'Login')
|
||||
if 'application/json+oembed' in response.content.decode():
|
||||
assertTemplateUsed(response, 'login_redirect.html')
|
||||
else:
|
||||
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
||||
assertRedirects(response, expected_url)
|
||||
if "embed" in str(url):
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
else:
|
||||
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
||||
assertRedirects(response, expected_url)
|
||||
|
||||
def test_page_titles(self):
|
||||
assert self.client.login(username='superuser', password='superuser')
|
||||
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
|
||||
request_url = get_request_url(url)
|
||||
response = self.client.get(request_url)
|
||||
if hasattr(response, "context_data") and "page_title" in response.context_data:
|
||||
expected_title = striptags(response.context_data["page_title"])
|
||||
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title),
|
||||
response.content.decode())
|
||||
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
|
||||
self.client.logout()
|
||||
|
||||
def test_page_titles(admin_client):
|
||||
create_asset_one()
|
||||
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
|
||||
request_url = get_request_url(url)
|
||||
response = admin_client.get(request_url)
|
||||
if hasattr(response, "context_data") and "page_title" in response.context_data:
|
||||
expected_title = response.context_data["page_title"]
|
||||
# try:
|
||||
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title), response.content.decode())
|
||||
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
|
||||
# except:
|
||||
# print(response.content.decode(), file=open('output.html', 'w'))
|
||||
def test_basic_access(self):
|
||||
assert 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
|
||||
assertNotContains(response, 'Edit')
|
||||
assertNotContains(response,
|
||||
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
||||
response = self.client.get(url)
|
||||
assertNotContains(response, 'Purchase Details')
|
||||
assertNotContains(response, 'View Revision History')
|
||||
|
||||
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
|
||||
for url_name in urlz:
|
||||
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse('supplier_create')
|
||||
response = self.client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse('supplier_update', kwargs={'pk': 1})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
self.client.logout()
|
||||
|
||||
def test_keyholder_access(self):
|
||||
assert self.client.login(username="keyholder", password="keyholder")
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = self.client.get(url)
|
||||
# Check edit and duplicate buttons shown in list
|
||||
assertContains(response, 'Edit')
|
||||
assertContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
||||
response = self.client.get(url)
|
||||
assertContains(response, 'Purchase Details')
|
||||
assertContains(response, 'View Revision History')
|
||||
self.client.logout()
|
||||
|
||||
@@ -12,6 +12,7 @@ urlpatterns = [
|
||||
path('', include('versioning.urls')),
|
||||
path('', include('RIGS.urls')),
|
||||
path('assets/', include('assets.urls')),
|
||||
path('training/', include('training.urls')),
|
||||
|
||||
path('', login_required(views.Index.as_view()), name='index'),
|
||||
|
||||
@@ -27,13 +28,14 @@ urlpatterns = [
|
||||
path('', include('users.urls')),
|
||||
|
||||
path('admin/', admin.site.urls),
|
||||
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
import debug_toolbar
|
||||
urlpatterns = [
|
||||
urlpatterns += [
|
||||
path('__debug__/', include(debug_toolbar.urls)),
|
||||
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
|
||||
] + urlpatterns
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@ import operator
|
||||
from functools import reduce
|
||||
|
||||
import simplejson
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.core import serializers
|
||||
from django.core.exceptions import PermissionDenied
|
||||
@@ -11,18 +12,18 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.views import generic
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
|
||||
from RIGS import models
|
||||
from assets import models as asset_models
|
||||
from training import models as training_models
|
||||
|
||||
|
||||
def is_ajax(request):
|
||||
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
||||
|
||||
# Displays the current rig count along with a few other bits and pieces
|
||||
|
||||
|
||||
class Index(generic.TemplateView):
|
||||
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
|
||||
template_name = 'index.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -38,7 +39,8 @@ class SecureAPIRequest(generic.View):
|
||||
'organisation': models.Organisation,
|
||||
'profile': models.Profile,
|
||||
'event': models.Event,
|
||||
'supplier': asset_models.Supplier
|
||||
'supplier': asset_models.Supplier,
|
||||
'training_item': training_models.TrainingItem,
|
||||
}
|
||||
|
||||
perms = {
|
||||
@@ -47,7 +49,8 @@ class SecureAPIRequest(generic.View):
|
||||
'organisation': 'RIGS.view_organisation',
|
||||
'profile': 'RIGS.view_profile',
|
||||
'event': None,
|
||||
'supplier': None
|
||||
'supplier': None,
|
||||
'training_item': None, # TODO
|
||||
}
|
||||
|
||||
'''
|
||||
@@ -75,6 +78,9 @@ class SecureAPIRequest(generic.View):
|
||||
fields = request.GET.get('fields', None)
|
||||
if fields:
|
||||
fields = fields.split(",")
|
||||
filters = request.GET.get('filters', [])
|
||||
if filters:
|
||||
filters = filters.split(",")
|
||||
|
||||
# Supply data for one record
|
||||
if pk:
|
||||
@@ -95,6 +101,9 @@ class SecureAPIRequest(generic.View):
|
||||
for field in fields:
|
||||
q = Q(**{field + "__icontains": part})
|
||||
qs.append(q)
|
||||
for filter in filters:
|
||||
q = Q(**{field: True})
|
||||
qs.append(q)
|
||||
queries.append(reduce(operator.or_, qs))
|
||||
|
||||
# Build the data response list
|
||||
@@ -230,15 +239,29 @@ class SearchHelp(generic.TemplateView):
|
||||
template_name = 'search_help.html'
|
||||
|
||||
|
||||
"""
|
||||
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
||||
May optionally also include some javascript in a success message to cause a load of
|
||||
the new information onto the page.
|
||||
"""
|
||||
|
||||
|
||||
class CloseModal(generic.TemplateView):
|
||||
"""
|
||||
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
||||
May optionally also include some javascript in a success message to cause a load of
|
||||
the new information onto the page.
|
||||
"""
|
||||
template_name = 'closemodal.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {'messages': messages.get_messages(self.request)}
|
||||
|
||||
|
||||
class OEmbedView(generic.View):
|
||||
def get(self, request, pk=None):
|
||||
embed_url = reverse(self.url_name, 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")
|
||||
|
||||
@@ -14,7 +14,7 @@ from reversion.admin import VersionAdmin
|
||||
from RIGS import models
|
||||
from users import forms as user_forms
|
||||
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(models.VatRate, VersionAdmin)
|
||||
admin.site.register(models.Event, VersionAdmin)
|
||||
admin.site.register(models.EventItem, VersionAdmin)
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse_lazy
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
@@ -33,20 +33,7 @@ class InvoiceIndex(generic.ListView):
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||
sql = "SELECT * FROM " \
|
||||
"(SELECT " \
|
||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||
"AS sub " \
|
||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||
"ORDER BY invoice_date"
|
||||
|
||||
query = self.model.objects.raw(sql)
|
||||
|
||||
return query
|
||||
return self.model.objects.outstanding_invoices()
|
||||
|
||||
|
||||
class InvoiceDetail(generic.DetailView):
|
||||
@@ -55,7 +42,13 @@ class InvoiceDetail(generic.DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(InvoiceDetail, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Invoice {} ({})".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
|
||||
context['page_title'] = "Invoice {} ({}) ".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
|
||||
if self.object.void:
|
||||
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
|
||||
elif self.object.is_closed:
|
||||
context['page_title'] += "<span class='badge badge-success float-right'>PAID</span>"
|
||||
else:
|
||||
context['page_title'] += "<span class='badge badge-info float-right'>OUTSTANDING</span>"
|
||||
return context
|
||||
|
||||
|
||||
@@ -67,12 +60,6 @@ class InvoicePrint(generic.View):
|
||||
|
||||
context = {
|
||||
'object': object,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
'invoice': invoice,
|
||||
'current_user': request.user,
|
||||
'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
|
||||
@@ -98,8 +85,8 @@ class InvoiceVoid(generic.View):
|
||||
object.save()
|
||||
|
||||
if object.void:
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_list'))
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
|
||||
return HttpResponseRedirect(reverse('invoice_list'))
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk}))
|
||||
|
||||
|
||||
class InvoiceDelete(generic.DeleteView):
|
||||
@@ -110,14 +97,14 @@ class InvoiceDelete(generic.DeleteView):
|
||||
obj = self.get_object()
|
||||
if obj.payment_set.all().count() > 0:
|
||||
messages.info(self.request, 'To delete an invoice, delete the payments first.')
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return super(InvoiceDelete, self).get(pk)
|
||||
|
||||
def post(self, request, pk):
|
||||
obj = self.get_object()
|
||||
if obj.payment_set.all().count() > 0:
|
||||
messages.info(self.request, 'To delete an invoice, delete the payments first.')
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return super(InvoiceDelete, self).post(pk)
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -172,30 +159,14 @@ class InvoiceWaiting(generic.ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
|
||||
total = 0
|
||||
for obj in self.get_objects():
|
||||
objects = self.get_queryset()
|
||||
for obj in objects:
|
||||
total += obj.sum_total
|
||||
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(self.get_objects()), total)
|
||||
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total)
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_objects()
|
||||
|
||||
def get_objects(self):
|
||||
# @todo find a way to select items
|
||||
events = self.model.objects.filter(
|
||||
(
|
||||
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||
) & Q(invoice__isnull=True) & # Has not already been invoiced
|
||||
Q(is_rig=True) # Is a rig (not non-rig)
|
||||
|
||||
).order_by('start_date') \
|
||||
.select_related('person',
|
||||
'organisation',
|
||||
'venue', 'mic') \
|
||||
.prefetch_related('items')
|
||||
|
||||
return events
|
||||
return self.model.objects.waiting_invoices()
|
||||
|
||||
|
||||
class InvoiceEvent(generic.View):
|
||||
@@ -216,7 +187,7 @@ class InvoiceEvent(generic.View):
|
||||
invoice.save()
|
||||
messages.warning(self.request, 'Invoice voided')
|
||||
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk}))
|
||||
|
||||
|
||||
class PaymentCreate(generic.CreateView):
|
||||
@@ -242,7 +213,7 @@ class PaymentCreate(generic.CreateView):
|
||||
|
||||
def get_success_url(self):
|
||||
messages.info(self.request, "location.reload()")
|
||||
return reverse_lazy('closemodal')
|
||||
return reverse('closemodal')
|
||||
|
||||
|
||||
class PaymentDelete(generic.DeleteView):
|
||||
|
||||
17
RIGS/hs.py
@@ -70,12 +70,20 @@ class EventRiskAssessmentDetail(generic.DetailView):
|
||||
model = models.RiskAssessment
|
||||
template_name = 'risk_assessment_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Risk Assessment for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
|
||||
return context
|
||||
|
||||
|
||||
class EventRiskAssessmentList(generic.ListView):
|
||||
paginate_by = 20
|
||||
model = models.RiskAssessment
|
||||
template_name = 'hs_object_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
|
||||
context['title'] = 'Risk Assessment'
|
||||
@@ -83,7 +91,6 @@ class EventRiskAssessmentList(generic.ListView):
|
||||
context['edit'] = 'ra_edit'
|
||||
context['review'] = 'ra_review'
|
||||
context['perm'] = 'perms.RIGS.review_riskassessment'
|
||||
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
return context
|
||||
|
||||
|
||||
@@ -105,7 +112,7 @@ class EventChecklistDetail(generic.DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistDetail, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Event Checklist for Event {} {}".format(self.object.event.display_id, self.object.event.name)
|
||||
context['page_title'] = "Event Checklist for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
|
||||
return context
|
||||
|
||||
|
||||
@@ -180,6 +187,9 @@ class EventChecklistList(generic.ListView):
|
||||
model = models.EventChecklist
|
||||
template_name = 'hs_object_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistList, self).get_context_data(**kwargs)
|
||||
context['title'] = 'Event Checklist'
|
||||
@@ -187,7 +197,6 @@ class EventChecklistList(generic.ListView):
|
||||
context['edit'] = 'ec_edit'
|
||||
context['review'] = 'ec_review'
|
||||
context['perm'] = 'perms.RIGS.review_eventchecklist'
|
||||
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
return context
|
||||
|
||||
|
||||
@@ -209,7 +218,7 @@ class HSList(generic.ListView):
|
||||
template_name = 'hs_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Event.objects.all().order_by('-start_date')
|
||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(HSList, self).get_context_data(**kwargs)
|
||||
|
||||
41
RIGS/management/commands/deleteSampleData.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from assets import models
|
||||
from RIGS import models as rigsmodels
|
||||
from training import models as tmodels
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deletes testing sample data'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if not settings.DEBUG:
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
self.delete_objects(models.AssetCategory)
|
||||
self.delete_objects(models.AssetStatus)
|
||||
self.delete_objects(models.Supplier)
|
||||
self.delete_objects(models.Connector)
|
||||
self.delete_objects(models.Asset)
|
||||
self.delete_objects(rigsmodels.VatRate)
|
||||
self.delete_objects(rigsmodels.Profile)
|
||||
self.delete_objects(rigsmodels.Person)
|
||||
self.delete_objects(rigsmodels.Organisation)
|
||||
self.delete_objects(rigsmodels.Venue)
|
||||
self.delete_objects(Group)
|
||||
self.delete_objects(rigsmodels.Event)
|
||||
self.delete_objects(rigsmodels.EventItem)
|
||||
self.delete_objects(rigsmodels.Invoice)
|
||||
self.delete_objects(rigsmodels.Payment)
|
||||
self.delete_objects(rigsmodels.RiskAssessment)
|
||||
self.delete_objects(rigsmodels.EventChecklist)
|
||||
self.delete_objects(tmodels.TrainingCategory)
|
||||
self.delete_objects(tmodels.TrainingItem)
|
||||
self.delete_objects(tmodels.TrainingLevel)
|
||||
|
||||
def delete_objects(self, model):
|
||||
for obj in model.objects.all():
|
||||
obj.delete()
|
||||
@@ -1,11 +1,15 @@
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Adds sample data to use for testing'
|
||||
can_import_settings = True
|
||||
|
||||
def handle(self, *args, **options):
|
||||
call_command('generateSampleUserData')
|
||||
call_command('generateSampleRIGSData')
|
||||
call_command('generateSampleAssetsData')
|
||||
call_command('generateSampleTrainingData')
|
||||
|
||||
@@ -17,13 +17,11 @@ class Command(BaseCommand):
|
||||
people = []
|
||||
organisations = []
|
||||
venues = []
|
||||
profiles = []
|
||||
|
||||
keyholder_group = None
|
||||
finance_group = None
|
||||
hs_group = None
|
||||
events = []
|
||||
profiles = models.Profile.objects.all()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
print("Generating rigboard data")
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG or settings.STAGING):
|
||||
@@ -34,20 +32,13 @@ class Command(BaseCommand):
|
||||
|
||||
with transaction.atomic():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
self.setup_people()
|
||||
self.setup_organisations()
|
||||
self.setup_venues()
|
||||
self.setup_events()
|
||||
print("Done generating rigboard data")
|
||||
|
||||
self.setupGenericProfiles()
|
||||
|
||||
self.setupPeople()
|
||||
self.setupOrganisations()
|
||||
self.setupVenues()
|
||||
|
||||
self.setupGroups()
|
||||
|
||||
self.setupEvents()
|
||||
|
||||
self.setupUsefulProfiles()
|
||||
|
||||
def setupPeople(self):
|
||||
def setup_people(self):
|
||||
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
|
||||
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
|
||||
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
|
||||
@@ -62,25 +53,25 @@ class Command(BaseCommand):
|
||||
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
reversion.set_user(random.choice(models.Profile.objects.all()))
|
||||
person = models.Person.objects.create(name=name)
|
||||
|
||||
newPerson = models.Person.objects.create(name=name)
|
||||
if i % 3 == 0:
|
||||
newPerson.email = "address@person.com"
|
||||
person.email = "address@person.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newPerson.phone = "01234 567894"
|
||||
person.phone = "01234 567894"
|
||||
|
||||
newPerson.save()
|
||||
self.people.append(newPerson)
|
||||
person.save()
|
||||
self.people.append(person)
|
||||
|
||||
def setupOrganisations(self):
|
||||
def setup_organisations(self):
|
||||
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars",
|
||||
"ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
|
||||
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
|
||||
@@ -109,27 +100,28 @@ class Command(BaseCommand):
|
||||
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newOrganisation = models.Organisation.objects.create(name=name)
|
||||
reversion.set_user(random.choice(models.Profile.objects.all()))
|
||||
new_organisation = models.Organisation.objects.create(name=name)
|
||||
|
||||
if i % 2 == 0:
|
||||
newOrganisation.has_su_account = True
|
||||
new_organisation.has_su_account = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newOrganisation.email = "address@organisation.com"
|
||||
new_organisation.email = "address@organisation.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newOrganisation.phone = "01234 567894"
|
||||
new_organisation.phone = "01234 567894"
|
||||
|
||||
newOrganisation.save()
|
||||
self.organisations.append(newOrganisation)
|
||||
new_organisation.save()
|
||||
self.organisations.append(new_organisation)
|
||||
|
||||
def setupVenues(self):
|
||||
def setup_venues(self):
|
||||
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
|
||||
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
|
||||
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
|
||||
@@ -145,108 +137,27 @@ class Command(BaseCommand):
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newVenue = models.Venue.objects.create(name=name)
|
||||
new_venue = models.Venue.objects.create(name=name)
|
||||
|
||||
if i % 2 == 0:
|
||||
newVenue.three_phase_available = True
|
||||
new_venue.three_phase_available = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newVenue.email = "address@venue.com"
|
||||
new_venue.email = "address@venue.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newVenue.phone = "01234 567894"
|
||||
new_venue.phone = "01234 567894"
|
||||
|
||||
newVenue.save()
|
||||
self.venues.append(newVenue)
|
||||
new_venue.save()
|
||||
self.venues.append(new_venue)
|
||||
|
||||
def setupGroups(self):
|
||||
self.keyholder_group = Group.objects.create(name='Keyholders')
|
||||
self.finance_group = Group.objects.create(name='Finance')
|
||||
self.hs_group = Group.objects.create(name='H&S')
|
||||
|
||||
keyholderPerms = ["add_event", "change_event", "view_event",
|
||||
"add_eventitem", "change_eventitem", "delete_eventitem",
|
||||
"add_organisation", "change_organisation", "view_organisation",
|
||||
"add_person", "change_person", "view_person", "view_profile",
|
||||
"add_venue", "change_venue", "view_venue",
|
||||
"add_asset", "change_asset", "delete_asset",
|
||||
"view_asset", "view_supplier", "change_supplier", "asset_finance",
|
||||
"add_supplier", "view_cabletype", "change_cabletype",
|
||||
"add_cabletype", "view_eventchecklist", "change_eventchecklist",
|
||||
"add_eventchecklist", "view_riskassessment", "change_riskassessment",
|
||||
"add_riskassessment", "add_eventchecklistcrew", "change_eventchecklistcrew",
|
||||
"delete_eventchecklistcrew", "view_eventchecklistcrew", "add_eventchecklistvehicle",
|
||||
"change_eventchecklistvehicle",
|
||||
"delete_eventchecklistvehicle", "view_eventchecklistvehicle", ]
|
||||
financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice",
|
||||
"add_payment", "change_payment", "delete_payment"]
|
||||
hsPerms = keyholderPerms + ["review_riskassessment", "review_eventchecklist"]
|
||||
|
||||
for permId in keyholderPerms:
|
||||
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
for permId in financePerms:
|
||||
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
for permId in hsPerms:
|
||||
self.hs_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
def setupGenericProfiles(self):
|
||||
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble",
|
||||
"Jack Harkness", "Mickey Smith", "Rose Tyler"]
|
||||
for i, name in enumerate(names):
|
||||
newProfile = models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0],
|
||||
last_name=name.split(" ")[-1],
|
||||
email=name.replace(" ", "") + "@example.com",
|
||||
initials="".join([j[0].upper() for j in name.split()]))
|
||||
if i % 2 == 0:
|
||||
newProfile.phone = "01234 567894"
|
||||
|
||||
newProfile.save()
|
||||
self.profiles.append(newProfile)
|
||||
|
||||
def setupUsefulProfiles(self):
|
||||
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User",
|
||||
initials="SU",
|
||||
email="superuser@example.com", is_superuser=True, is_active=True,
|
||||
is_staff=True)
|
||||
superUser.set_password('superuser')
|
||||
superUser.save()
|
||||
|
||||
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User",
|
||||
initials="FU",
|
||||
email="financeuser@example.com", is_active=True, is_approved=True)
|
||||
financeUser.groups.add(self.finance_group)
|
||||
financeUser.groups.add(self.keyholder_group)
|
||||
financeUser.set_password('finance')
|
||||
financeUser.save()
|
||||
|
||||
hsUser = models.Profile.objects.create(username="hs", first_name="HS", last_name="User",
|
||||
initials="HSU",
|
||||
email="hsuser@example.com", is_active=True, is_approved=True)
|
||||
hsUser.groups.add(self.hs_group)
|
||||
hsUser.groups.add(self.keyholder_group)
|
||||
hsUser.set_password('hs')
|
||||
hsUser.save()
|
||||
|
||||
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User",
|
||||
initials="KU",
|
||||
email="keyholderuser@example.com", is_active=True, is_approved=True)
|
||||
keyholderUser.groups.add(self.keyholder_group)
|
||||
keyholderUser.set_password('keyholder')
|
||||
keyholderUser.save()
|
||||
|
||||
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
|
||||
email="basicuser@example.com", is_active=True, is_approved=True)
|
||||
basicUser.set_password('basic')
|
||||
basicUser.save()
|
||||
|
||||
def setupEvents(self):
|
||||
def setup_events(self):
|
||||
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball",
|
||||
"Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
|
||||
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
|
||||
@@ -257,7 +168,7 @@ class Command(BaseCommand):
|
||||
notes = ["The client came into the office at some point", "Who knows if this will happen",
|
||||
"Probably should check this event", "Maybe not happening", "Run away!"]
|
||||
|
||||
itemOptions = [
|
||||
item_options = [
|
||||
{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
|
||||
'cost': 200.00},
|
||||
{'name': 'Projector',
|
||||
@@ -274,7 +185,7 @@ class Command(BaseCommand):
|
||||
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
|
||||
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
|
||||
|
||||
dayDelta = -120 # start adding events from 4 months ago
|
||||
day_delta = -120 # start adding events from 4 months ago
|
||||
|
||||
for i in range(150): # Let's add 100 events
|
||||
with reversion.create_revision():
|
||||
@@ -282,70 +193,71 @@ class Command(BaseCommand):
|
||||
|
||||
name = names[i % len(names)]
|
||||
|
||||
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
|
||||
dayDelta = dayDelta + random.randint(0, 3)
|
||||
start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
|
||||
day_delta = day_delta + random.randint(0, 3)
|
||||
|
||||
newEvent = models.Event.objects.create(name=name, start_date=startDate)
|
||||
new_event = models.Event.objects.create(name=name, start_date=start_date)
|
||||
|
||||
if random.randint(0, 2) > 1: # 1 in 3 have a start time
|
||||
newEvent.start_time = datetime.time(random.randint(15, 20))
|
||||
new_event.start_time = datetime.time(random.randint(15, 20))
|
||||
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
|
||||
newEvent.end_time = datetime.time(random.randint(21, 23))
|
||||
new_event.end_time = datetime.time(random.randint(21, 23))
|
||||
elif random.randint(0, 1) > 0: # half of the others finish early the next day
|
||||
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
|
||||
newEvent.end_time = datetime.time(random.randint(0, 5))
|
||||
new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
|
||||
new_event.end_time = datetime.time(random.randint(0, 5))
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
|
||||
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
|
||||
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4))
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have MIC
|
||||
newEvent.mic = random.choice(self.profiles)
|
||||
new_event.mic = random.choice(self.profiles)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have organisation
|
||||
newEvent.organisation = random.choice(self.organisations)
|
||||
new_event.organisation = random.choice(self.organisations)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have person
|
||||
newEvent.person = random.choice(self.people)
|
||||
new_event.person = random.choice(self.people)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have venue
|
||||
newEvent.venue = random.choice(self.venues)
|
||||
new_event.venue = random.choice(self.venues)
|
||||
|
||||
# Could have any status, equally weighted
|
||||
newEvent.status = random.choice(
|
||||
new_event.status = random.choice(
|
||||
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
|
||||
|
||||
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
|
||||
new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have description
|
||||
newEvent.description = random.choice(descriptions)
|
||||
new_event.description = random.choice(descriptions)
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have notes
|
||||
newEvent.notes = random.choice(notes)
|
||||
new_event.notes = random.choice(notes)
|
||||
|
||||
newEvent.save()
|
||||
new_event.save()
|
||||
|
||||
# Now add some items
|
||||
for j in range(random.randint(1, 5)):
|
||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
||||
newItem.save()
|
||||
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||
new_item.save()
|
||||
|
||||
while newEvent.sum_total < 0:
|
||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
||||
newItem.save()
|
||||
while new_event.sum_total < 0:
|
||||
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||
new_item.save()
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
|
||||
if new_event.start_date < datetime.date.today(): # think about adding an invoice
|
||||
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
|
||||
newInvoice = models.Invoice.objects.create(event=newEvent)
|
||||
if newEvent.status is models.Event.CANCELLED: # void cancelled events
|
||||
newInvoice.void = True
|
||||
new_invoice = models.Invoice.objects.create(event=new_event)
|
||||
if new_event.status is models.Event.CANCELLED: # void cancelled events
|
||||
new_invoice.void = True
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
||||
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance,
|
||||
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
|
||||
date=datetime.date.today())
|
||||
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
|
||||
models.RiskAssessment.objects.create(event=newEvent, supervisor_consulted=bool(random.getrandbits(1)), nonstandard_equipment=bool(random.getrandbits(1)),
|
||||
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
|
||||
nonstandard_equipment=bool(random.getrandbits(1)),
|
||||
nonstandard_use=bool(random.getrandbits(1)),
|
||||
contractors=bool(random.getrandbits(1)),
|
||||
other_companies=bool(random.getrandbits(1)),
|
||||
@@ -366,8 +278,15 @@ class Command(BaseCommand):
|
||||
suspended_structures=bool(random.getrandbits(1)),
|
||||
outside=bool(random.getrandbits(1)))
|
||||
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
|
||||
models.EventChecklist.objects.create(event=newEvent, power_mic=random.choice(self.profiles), safe_parking=bool(random.getrandbits(1)),
|
||||
safe_packing=bool(random.getrandbits(1)), exits=bool(random.getrandbits(1)), trip_hazard=bool(random.getrandbits(1)), warning_signs=bool(random.getrandbits(1)),
|
||||
ear_plugs=bool(random.getrandbits(1)), hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot", earthing=bool(random.getrandbits(1)), pat=bool(random.getrandbits(1)),
|
||||
models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles),
|
||||
safe_parking=bool(random.getrandbits(1)),
|
||||
safe_packing=bool(random.getrandbits(1)),
|
||||
exits=bool(random.getrandbits(1)),
|
||||
trip_hazard=bool(random.getrandbits(1)),
|
||||
warning_signs=bool(random.getrandbits(1)),
|
||||
ear_plugs=bool(random.getrandbits(1)),
|
||||
hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot",
|
||||
earthing=bool(random.getrandbits(1)),
|
||||
pat=bool(random.getrandbits(1)),
|
||||
date=timezone.now(), venue=random.choice(self.venues))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 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
|
||||
# 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):
|
||||
@@ -15,5 +15,5 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(approve_legacy)
|
||||
migrations.RunPython(approve_legacy, migrations.RunPython.noop)
|
||||
]
|
||||
|
||||
67
RIGS/migrations/0040_auto_20210302_1148.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 11:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def postgres_migration_prep(apps, schema_editor):
|
||||
model = apps.get_model("RIGS", "Event")
|
||||
for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventAuthorisation")
|
||||
for field in ["account_code", "uni_id"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventChecklist")
|
||||
for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventItem")
|
||||
for field in ["description"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Organisation")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Payment")
|
||||
for field in ["method"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Person")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Profile")
|
||||
for field in ["phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "RiskAssessment")
|
||||
for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Venue")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0039_auto_20210123_1910'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
|
||||
]
|
||||
201
RIGS/migrations/0041_auto_20210302_1204.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 12:04
|
||||
|
||||
import RIGS.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0040_auto_20210302_1148'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='meet_info',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='payment_method',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='payment_received',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='dark_theme',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='auth_request_to',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='collector',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='collected by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='purchase_order',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='PO'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='account_code',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='uni_id',
|
||||
field=models.CharField(blank=True, default='', max_length=10, verbose_name='University ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='extinguishers_location',
|
||||
field=models.CharField(blank=True, default='', help_text='Location of fire extinguishers', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='hs_location',
|
||||
field=models.CharField(blank=True, default='', help_text='Location of Safety Bag/Box', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventitem',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='method',
|
||||
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], default='', max_length=2),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='api_key',
|
||||
field=models.CharField(blank=True, default='', editable=False, max_length=40),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='general_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='persons_responsible_structures',
|
||||
field=models.TextField(blank=True, default='', help_text='Who are the persons on site responsible for their use?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='power_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='power_plan',
|
||||
field=models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='rigging_plan',
|
||||
field=models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='sound_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
]
|
||||
34
RIGS/migrations/0042_auto_20211007_2338.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-07 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0041_auto_20210302_1204'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
]
|
||||
|
||||
18
RIGS/migrations/0043_auto_20211027_1519.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-27 14:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0042_auto_20211007_2338'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='initials',
|
||||
field=models.CharField(max_length=5, null=True),
|
||||
),
|
||||
]
|
||||
187
RIGS/models.py
@@ -12,7 +12,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from reversion import revisions as reversion
|
||||
@@ -20,12 +20,15 @@ from reversion.models import Version
|
||||
|
||||
|
||||
class Profile(AbstractUser):
|
||||
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, blank=True, default='')
|
||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||
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...
|
||||
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||
dark_theme = models.BooleanField(default=False)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@classmethod
|
||||
def make_api_key(cls):
|
||||
@@ -51,7 +54,7 @@ class Profile(AbstractUser):
|
||||
|
||||
@property
|
||||
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', 'riskassessment', 'invoice').prefetch_related('checklists')
|
||||
|
||||
@classmethod
|
||||
def admins(cls):
|
||||
@@ -64,8 +67,6 @@ class Profile(AbstractUser):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# TODO move to versioning - currently get import errors with that
|
||||
|
||||
|
||||
class RevisionMixin(object):
|
||||
@property
|
||||
@@ -102,12 +103,12 @@ class RevisionMixin(object):
|
||||
|
||||
class Person(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=50)
|
||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||
email = models.EmailField(blank=True, null=True)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, null=True)
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
@@ -133,17 +134,17 @@ class Person(models.Model, RevisionMixin):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
||||
return reverse('person_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class Organisation(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=50)
|
||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||
email = models.EmailField(blank=True, null=True)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, null=True)
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
union_account = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
@@ -170,7 +171,7 @@ class Organisation(models.Model, RevisionMixin):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
||||
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class VatManager(models.Manager):
|
||||
@@ -178,7 +179,6 @@ class VatManager(models.Manager):
|
||||
return self.find_rate(timezone.now())
|
||||
|
||||
def find_rate(self, date):
|
||||
# return self.filter(startAt__lte=date).latest()
|
||||
try:
|
||||
return self.filter(start_at__lte=date).latest()
|
||||
except VatRate.DoesNotExist:
|
||||
@@ -211,12 +211,12 @@ class VatRate(models.Model, RevisionMixin):
|
||||
|
||||
class Venue(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=255)
|
||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||
email = models.EmailField(blank=True, null=True)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
three_phase_available = models.BooleanField(default=False)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, null=True)
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
@@ -229,24 +229,23 @@ class Venue(models.Model, RevisionMixin):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
||||
return reverse('venue_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class EventManager(models.Manager):
|
||||
def current_events(self):
|
||||
events = self.filter(
|
||||
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Ends after
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Active dry hire
|
||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
||||
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
||||
'organisation',
|
||||
'venue', 'mic')
|
||||
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
return events
|
||||
|
||||
def events_in_bounds(self, start, end):
|
||||
@@ -269,16 +268,29 @@ class EventManager(models.Manager):
|
||||
|
||||
def rig_count(self):
|
||||
event_count = self.filter(
|
||||
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
||||
is_rig=True) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
|
||||
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Ends after
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
|
||||
status=Event.CANCELLED)) # Active dry hire
|
||||
).count()
|
||||
return event_count
|
||||
|
||||
def waiting_invoices(self):
|
||||
events = self.filter(
|
||||
(
|
||||
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
|
||||
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
|
||||
models.Q(is_rig=True) # Is a rig (not non-rig)
|
||||
).order_by('start_date') \
|
||||
.select_related('person', 'organisation', 'venue', 'mic') \
|
||||
.prefetch_related('items')
|
||||
|
||||
return events
|
||||
|
||||
|
||||
@reversion.register(follow=['items'])
|
||||
class Event(models.Model, RevisionMixin):
|
||||
@@ -298,8 +310,8 @@ class Event(models.Model, RevisionMixin):
|
||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
description = models.TextField(blank=True, default='')
|
||||
notes = models.TextField(blank=True, default='')
|
||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||
dry_hire = models.BooleanField(default=False)
|
||||
is_rig = models.BooleanField(default=True)
|
||||
@@ -313,7 +325,6 @@ class Event(models.Model, RevisionMixin):
|
||||
end_time = models.TimeField(blank=True, null=True)
|
||||
access_at = models.DateTimeField(blank=True, null=True)
|
||||
meet_at = models.DateTimeField(blank=True, null=True)
|
||||
meet_info = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# Crew management
|
||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||
@@ -322,22 +333,23 @@ class Event(models.Model, RevisionMixin):
|
||||
verbose_name="MIC", on_delete=models.CASCADE)
|
||||
|
||||
# Monies
|
||||
payment_method = models.CharField(max_length=255, blank=True, null=True)
|
||||
payment_received = models.CharField(max_length=255, blank=True, null=True)
|
||||
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
|
||||
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
|
||||
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||
|
||||
# Authorisation request details
|
||||
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
||||
auth_request_at = models.DateTimeField(null=True, blank=True)
|
||||
auth_request_to = models.EmailField(null=True, blank=True)
|
||||
auth_request_to = models.EmailField(blank=True, default='')
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
if self.is_rig:
|
||||
return str("N%05d" % self.pk)
|
||||
if self.pk:
|
||||
if self.is_rig:
|
||||
return str("N%05d" % self.pk)
|
||||
else:
|
||||
return self.pk
|
||||
else:
|
||||
return self.pk
|
||||
return "????"
|
||||
|
||||
# Calculated values
|
||||
"""
|
||||
@@ -346,7 +358,7 @@ class Event(models.Model, RevisionMixin):
|
||||
|
||||
@property
|
||||
def sum_total(self):
|
||||
total = EventItem.objects.filter(event=self).aggregate(
|
||||
total = self.items.aggregate(
|
||||
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||
)['sum_total']
|
||||
@@ -360,6 +372,9 @@ class Event(models.Model, RevisionMixin):
|
||||
|
||||
@property
|
||||
def vat(self):
|
||||
# No VAT is owed on internal transfers
|
||||
if self.internal:
|
||||
return 0
|
||||
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
||||
|
||||
"""
|
||||
@@ -456,7 +471,7 @@ class Event(models.Model, RevisionMixin):
|
||||
objects = EventManager()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'pk': self.pk})
|
||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "{}: {}".format(self.display_id, self.name)
|
||||
@@ -490,7 +505,7 @@ class Event(models.Model, RevisionMixin):
|
||||
class EventItem(models.Model, RevisionMixin):
|
||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
description = models.TextField(blank=True, default='')
|
||||
quantity = models.IntegerField()
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
order = models.IntegerField()
|
||||
@@ -505,7 +520,7 @@ class EventItem(models.Model, RevisionMixin):
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
|
||||
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name)
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
@@ -517,19 +532,36 @@ class EventAuthorisation(models.Model, RevisionMixin):
|
||||
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
||||
email = models.EmailField()
|
||||
name = models.CharField(max_length=255)
|
||||
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)
|
||||
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
||||
account_code = models.CharField(max_length=50, default='', blank=True)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
|
||||
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
|
||||
|
||||
|
||||
class InvoiceManager(models.Manager):
|
||||
def outstanding_invoices(self):
|
||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||
sql = "SELECT * FROM " \
|
||||
"(SELECT " \
|
||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||
"AS sub " \
|
||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||
"ORDER BY invoice_date"
|
||||
|
||||
query = self.raw(sql)
|
||||
return query
|
||||
|
||||
|
||||
@reversion.register(follow=['payment_set'])
|
||||
class Invoice(models.Model, RevisionMixin):
|
||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||
@@ -538,6 +570,8 @@ class Invoice(models.Model, RevisionMixin):
|
||||
|
||||
reversion_perm = 'RIGS.view_invoice'
|
||||
|
||||
objects = InvoiceManager()
|
||||
|
||||
@property
|
||||
def sum_total(self):
|
||||
return self.event.sum_total
|
||||
@@ -562,11 +596,11 @@ class Invoice(models.Model, RevisionMixin):
|
||||
return self.balance == 0 or self.void
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('invoice_detail', kwargs={'pk': self.pk})
|
||||
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return "#{} for Event {}".format(self.display_id, "N%05d" % self.event.pk)
|
||||
return "#{} for Event {}".format(self.display_id, self.event.display_id)
|
||||
|
||||
def __str__(self):
|
||||
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
||||
@@ -597,7 +631,7 @@ class Payment(models.Model, RevisionMixin):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
||||
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
|
||||
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@@ -632,10 +666,9 @@ class RiskAssessment(models.Model, RevisionMixin):
|
||||
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
|
||||
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
|
||||
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
|
||||
general_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
|
||||
# Power
|
||||
# event_size = models.IntegerField(blank=True, null=True, choices=SIZES)
|
||||
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
|
||||
# If yes to the above two, you must answer...
|
||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
|
||||
@@ -645,12 +678,12 @@ class RiskAssessment(models.Model, RevisionMixin):
|
||||
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
|
||||
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
|
||||
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
|
||||
power_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
power_plan = models.URLField(blank=True, null=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
|
||||
# Sound
|
||||
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
|
||||
sound_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
|
||||
# Site
|
||||
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
|
||||
@@ -663,8 +696,8 @@ class RiskAssessment(models.Model, RevisionMixin):
|
||||
# Structures
|
||||
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
|
||||
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
|
||||
persons_responsible_structures = models.TextField(blank=True, null=True, help_text="Who are the persons on site responsible for their use?")
|
||||
rigging_plan = models.URLField(blank=True, null=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
|
||||
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
|
||||
# Blimey that was a lot of options
|
||||
|
||||
@@ -708,6 +741,10 @@ class RiskAssessment(models.Model, RevisionMixin):
|
||||
('review_riskassessment', 'Can review Risk Assessments')
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def fieldz(self):
|
||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
|
||||
@property
|
||||
def event_size(self):
|
||||
# Confirm event size. Check all except generators, since generators entails outside
|
||||
@@ -723,7 +760,7 @@ class RiskAssessment(models.Model, RevisionMixin):
|
||||
return str(self.event)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('ra_detail', kwargs={'pk': self.pk})
|
||||
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "%i - %s" % (self.pk, self.event)
|
||||
@@ -746,8 +783,8 @@ class EventChecklist(models.Model, RevisionMixin):
|
||||
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
|
||||
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
|
||||
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
|
||||
hs_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of Safety Bag/Box")
|
||||
extinguishers_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of fire extinguishers")
|
||||
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
||||
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
|
||||
|
||||
# Small Electrical Checks
|
||||
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
|
||||
@@ -765,21 +802,21 @@ class EventChecklist(models.Model, RevisionMixin):
|
||||
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
|
||||
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
|
||||
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
|
||||
fd_earth_fault = models.IntegerField(blank=True, null=True, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
|
||||
# Worst case points
|
||||
w1_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
|
||||
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w1_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w2_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
|
||||
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w2_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w3_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
|
||||
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
|
||||
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
||||
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
||||
@@ -796,12 +833,16 @@ class EventChecklist(models.Model, RevisionMixin):
|
||||
('review_eventchecklist', 'Can review Event Checklists')
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def fieldz(self):
|
||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return str(self.event)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('ec_detail', kwargs={'pk': self.pk})
|
||||
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "%i - %s" % (self.pk, self.event)
|
||||
|
||||
@@ -11,7 +11,7 @@ import simplejson
|
||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core import signing
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
@@ -27,6 +27,7 @@ from django.views import generic
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from PyRIGS import decorators
|
||||
from PyRIGS.views import OEmbedView, is_ajax
|
||||
from RIGS import models, forms
|
||||
|
||||
__author__ = 'ghost'
|
||||
@@ -40,7 +41,7 @@ class RigboardIndex(generic.TemplateView):
|
||||
context = super(RigboardIndex, self).get_context_data(**kwargs)
|
||||
|
||||
# call out method to get current events
|
||||
context['events'] = models.Event.objects.current_events()
|
||||
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
|
||||
context['page_title'] = "Rigboard"
|
||||
return context
|
||||
|
||||
@@ -59,29 +60,24 @@ class EventDetail(generic.DetailView):
|
||||
template_name = 'event_detail.html'
|
||||
model = models.Event
|
||||
|
||||
|
||||
class EventOembed(generic.View):
|
||||
model = models.Event
|
||||
|
||||
def get(self, request, pk=None):
|
||||
embed_url = reverse('event_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")
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventDetail, self).get_context_data(**kwargs)
|
||||
title = "{} | {}".format(self.object.display_id, self.object.name)
|
||||
if self.object.dry_hire:
|
||||
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
||||
context['page_title'] = title
|
||||
return context
|
||||
|
||||
|
||||
class EventEmbed(EventDetail):
|
||||
template_name = 'event_embed.html'
|
||||
|
||||
|
||||
class EventOEmbed(OEmbedView):
|
||||
model = models.Event
|
||||
url_name = 'event_embed'
|
||||
|
||||
|
||||
class EventCreate(generic.CreateView):
|
||||
model = models.Event
|
||||
form_class = forms.EventForm
|
||||
@@ -155,9 +151,10 @@ class EventDuplicate(EventUpdate):
|
||||
# Clear checked in by if it's a dry hire
|
||||
if new.dry_hire is True:
|
||||
new.checked_in_by = None
|
||||
new.collector = None
|
||||
|
||||
# Remove all the authorisation information from the new event
|
||||
new.auth_request_to = None
|
||||
new.auth_request_to = ''
|
||||
new.auth_request_by = None
|
||||
new.auth_request_at = None
|
||||
|
||||
@@ -185,15 +182,9 @@ class EventPrint(generic.View):
|
||||
|
||||
context = {
|
||||
'object': object,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
'quote': True,
|
||||
'current_user': request.user,
|
||||
'filename': 'Event {} {} {}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
|
||||
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
|
||||
}
|
||||
|
||||
rml = template.render(context)
|
||||
@@ -284,6 +275,7 @@ class EventArchive(generic.ListView):
|
||||
class EventAuthorise(generic.UpdateView):
|
||||
template_name = 'eventauthorisation_form.html'
|
||||
success_template = 'eventauthorisation_success.html'
|
||||
preview = False
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save()
|
||||
@@ -311,6 +303,7 @@ class EventAuthorise(generic.UpdateView):
|
||||
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
|
||||
if self.event.dry_hire:
|
||||
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
|
||||
context['preview'] = self.preview
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
@@ -359,7 +352,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
||||
return self.get_object()
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.is_ajax():
|
||||
if is_ajax(self.request):
|
||||
url = reverse_lazy('closemodal')
|
||||
messages.info(self.request, "location.reload()")
|
||||
else:
|
||||
@@ -397,7 +390,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
||||
to=[email],
|
||||
reply_to=[self.request.user.email],
|
||||
)
|
||||
css = staticfiles_storage.path('css/email.css')
|
||||
css = finders.find('css/email.css')
|
||||
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
|
||||
external_styles=css).transform()
|
||||
msg.attach_alternative(html, 'text/html')
|
||||
@@ -412,8 +405,7 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||
model = models.Event
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
css = staticfiles_storage.path('css/email.css')
|
||||
css = finders.find('css/email.css')
|
||||
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
|
||||
assert isinstance(response, HttpResponse)
|
||||
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
|
||||
@@ -427,4 +419,5 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||
'sent_by': self.request.user.pk,
|
||||
})
|
||||
context['to_name'] = self.request.GET.get('to_name', None)
|
||||
context['target'] = 'event_authorise_form_preview'
|
||||
return context
|
||||
|
||||
@@ -6,7 +6,7 @@ from io import BytesIO
|
||||
|
||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.cache import cache
|
||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
from django.db.models.signals import post_save
|
||||
@@ -25,12 +25,6 @@ def send_eventauthorisation_success_email(instance):
|
||||
# Generate PDF first to prevent context conflicts
|
||||
context = {
|
||||
'object': instance.event,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
'receipt': True,
|
||||
'current_user': False,
|
||||
}
|
||||
@@ -69,7 +63,7 @@ def send_eventauthorisation_success_email(instance):
|
||||
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
||||
)
|
||||
|
||||
css = staticfiles_storage.path('css/email.css')
|
||||
css = finders.find('css/email.css')
|
||||
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
|
||||
external_styles=css).transform()
|
||||
client_email.attach_alternative(html, 'text/html')
|
||||
@@ -127,7 +121,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
||||
to=[admin.email],
|
||||
reply_to=[user.email],
|
||||
)
|
||||
css = staticfiles_storage.path('css/email.css')
|
||||
css = finders.find('css/email.css')
|
||||
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
||||
external_styles=css).transform()
|
||||
email.attach_alternative(html, 'text/html')
|
||||
|
||||
BIN
RIGS/static/imgs/assets.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
RIGS/static/imgs/logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
RIGS/static/imgs/logo.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
RIGS/static/imgs/rigs.jpg
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
RIGS/static/imgs/square_logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
RIGS/static/imgs/tappytaptap.gif
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
BIN
RIGS/static/imgs/training.jpg
Normal file
|
After Width: | Height: | Size: 852 KiB |
BIN
RIGS/static/imgs/wof2014-1-small.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
6
RIGS/static/js/marked.min.js
vendored
Normal file
@@ -1,9 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load invoices_waiting from filters %}
|
||||
{% load invoices_outstanding from filters %}
|
||||
{% load total_invoices_todo from filters %}
|
||||
|
||||
{% block titleheader %}
|
||||
<a class="navbar-brand" href="/">RIGS</a>
|
||||
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block titleelements %}
|
||||
@@ -44,14 +47,17 @@
|
||||
{% endif %}
|
||||
{% if perms.RIGS.view_invoice %}
|
||||
<li class="nav-item dropdown">
|
||||
{% total_invoices_todo as todo %}
|
||||
{% invoices_waiting as waiting %}
|
||||
{% invoices_outstanding as outstanding %}
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Invoices
|
||||
Invoices <span class="badge {% if todo == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ todo }}</span>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a class="dropdown-item" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting</a>
|
||||
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ waiting }}</span></a>
|
||||
{% endif %}
|
||||
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding</a>
|
||||
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding <span class="badge {% if outstanding == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ outstanding }}</span></a>
|
||||
<a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a>
|
||||
</div>
|
||||
</li>
|
||||
@@ -74,6 +80,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
<script src="{% static 'js/popover.js' %}"></script>
|
||||
<script>
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
{% block title %}Calendar{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{% static 'css/main.min.css' %}" rel='stylesheet' />
|
||||
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'js/moment.js' %}"></script>
|
||||
<script src="{% static 'js/main.min.js' %}"></script>
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
<script>
|
||||
viewToUrl = {
|
||||
'timeGridWeek':'week',
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
{% load static %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/selects.js' %}" async></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12 text-right my-3">
|
||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
|
||||
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,7 +32,11 @@
|
||||
</dd>
|
||||
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
|
||||
<dd class="col-6">
|
||||
{% if object.power_mic %}
|
||||
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
<p>List vehicles and their drivers</p>
|
||||
@@ -98,6 +102,10 @@
|
||||
<td>{{crew.role}}</td>
|
||||
<td>{{crew.end}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -105,9 +113,27 @@
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
|
||||
{% if object.event.riskassessment.event_size != 2 %}
|
||||
<div class="card-body">
|
||||
{% if object.event.riskassessment.event_size == 1 %}
|
||||
{% if object.event.riskassessment.event_size == 0 %}
|
||||
<dl class="row">
|
||||
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.rcds|yesnoi }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.supply_test|yesnoi }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.earthing|yesnoi }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.pat|yesnoi }}
|
||||
</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<dl class="row">
|
||||
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
@@ -212,28 +238,8 @@
|
||||
</dl>
|
||||
<hr>
|
||||
{% include 'partials/ec_power_info.html' %}
|
||||
{% else %}
|
||||
<dl class="row">
|
||||
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.rcds|yesnoi }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.supply_test|yesnoi }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.earthing|yesnoi }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.pat|yesnoi }}
|
||||
</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-12 text-right">
|
||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||
|
||||
@@ -7,24 +7,18 @@
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
|
||||
<script src="{% static 'js/selects.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise-->
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
<script src="{% static 'js/modal.js' %}"></script>
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
|
||||
{% include 'partials/datetime-fix.html' %}
|
||||
|
||||
@@ -134,14 +128,14 @@
|
||||
<tbody id="vehiclest" data-pk="-1">
|
||||
<tr id="vehicles_new" style="display: none;">
|
||||
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td>
|
||||
<td><select class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" name="driver_new" disabled="true"></select></td>
|
||||
<td><select data-container="body" class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" name="driver_new" disabled="true"></select></td>
|
||||
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
|
||||
</tr>
|
||||
{% for i in object.vehicles.all %}
|
||||
<tr id="vehicles_{{i.pk}}">
|
||||
<td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td>
|
||||
<td>
|
||||
<select name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||
<select data-container="body" name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||
{% if i.driver != '' %}
|
||||
<option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option>
|
||||
{% endif %}
|
||||
@@ -202,7 +196,7 @@
|
||||
<tbody id="crewmemberst" data-pk="-1">
|
||||
<tr id="crew_new" style="display: none;">
|
||||
<td>
|
||||
<select name="crewmember_new" class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select>
|
||||
<select name="crewmember_new" class="form-control" data-container="body" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select>
|
||||
</td>
|
||||
<td style="min-width: 15ch"><input name="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled=""/></td>
|
||||
<td style="min-width: 15ch"><input name="role_new" type="text" class="form-control" value="{{ i.role }}" disabled="true"/></td>
|
||||
@@ -212,7 +206,7 @@
|
||||
{% for crew in object.crew.all %}
|
||||
<tr id="crew_{{crew.pk}}">
|
||||
<td>
|
||||
<select name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||
<select data-container="body" name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||
{% if crew.crewmember != '' %}
|
||||
<option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option>
|
||||
{% endif %}
|
||||
@@ -250,12 +244,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif event.riskassessment.event_size == 1 %}
|
||||
{% else %}
|
||||
<div class="row my-3" id="size-1">
|
||||
<div class="col-12">
|
||||
{% if event.riskassessment.event_size == 1 %}
|
||||
<div class="card border-warning">
|
||||
<div class="card-header">Electrical Checks <small>for ‘Medium’ TEC Events </small></div>
|
||||
<div class="card-body">
|
||||
{% else %}
|
||||
<div class="card border-danger">
|
||||
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
|
||||
{% endif %}
|
||||
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
|
||||
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
|
||||
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
|
||||
@@ -345,17 +346,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row my-3" id="size-2">
|
||||
<div class="col-12">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
|
||||
<div class="card-body">
|
||||
<p>Outside the scope of this assessment. <strong>I really hope you checked with a supervisor...</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-sm-12 text-right">
|
||||
|
||||
@@ -1,71 +1,20 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load linkornone from filters %}
|
||||
{% load namewithnotes from filters %}
|
||||
|
||||
{% block title %}{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %} | {{object.name}}{% endblock %}
|
||||
{% load markdown_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3 py-3">
|
||||
{% if not request.is_ajax %}
|
||||
<div class="col-sm-12">
|
||||
<h1>
|
||||
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
|
||||
| {{ object.name }} {% if event.dry_hire %}<span class="badge badge-secondary">Dry Hire</span>{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
{% if perms.RIGS.view_event %}
|
||||
<div class="col-sm-12 text-right">
|
||||
{% include 'event_detail_buttons.html' %}
|
||||
{% include 'partials/event_detail_buttons.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if object.is_rig and perms.RIGS.view_event %}
|
||||
{# only need contact details for a rig #}
|
||||
<div class="col-md-6">
|
||||
{% if event.person %}
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">Contact Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Person</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% if object.person %}
|
||||
<a href="{% url 'person_detail' object.person.pk %}" class="modal-href">
|
||||
{{ object.person|namewithnotes:'person_detail' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt class="col-sm-6">Email</dt>
|
||||
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
|
||||
<dt class="col-sm-6">Phone Number</dt>
|
||||
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.organisation %}
|
||||
<div class="card card-default">
|
||||
<div class="card-header">Organisation</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Organisation</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% if object.organisation %}
|
||||
<a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href">
|
||||
{{ object.organisation|namewithnotes:'organisation_detail' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt class="col-sm-6">Email</dt>
|
||||
<dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd>
|
||||
<dt class="col-sm-6">Phone Number</dt>
|
||||
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
|
||||
<dt class="col-sm-6">Has SU Account</dt>
|
||||
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'partials/contact_details.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-6">
|
||||
@@ -85,7 +34,7 @@
|
||||
{% endif %}
|
||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||
<div class="col-sm-12 text-right">
|
||||
{% include 'event_detail_buttons.html' %}
|
||||
{% include 'partials/event_detail_buttons.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.is_rig %}
|
||||
@@ -96,16 +45,16 @@
|
||||
{% if perms.RIGS.view_event %}
|
||||
<h4>Notes</h4>
|
||||
<hr>
|
||||
<p class="dont-break-out">{{ event.notes|linebreaksbr }}</p>
|
||||
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% include 'item_table.html' %}
|
||||
{% include 'partials/item_table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||
<div class="col-sm-12 text-right">
|
||||
{% include 'event_detail_buttons.html' %}
|
||||
{% include 'partials/event_detail_buttons.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,100 +1,89 @@
|
||||
{% extends 'base_embed.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<a href="/">
|
||||
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
|
||||
</a>
|
||||
</div>
|
||||
{% block extra-head %}
|
||||
<link href="{% static 'fontawesome_free/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
|
||||
<link href="{% static 'fontawesome_free/css/solid.css' %}" rel="stylesheet" type="text/css">
|
||||
{% endblock %}
|
||||
|
||||
<div class="col-sm-12">
|
||||
<span class="pull-right">
|
||||
{% block content %}
|
||||
<span class="float-right">
|
||||
{% if object.mic %}
|
||||
<div class="text-center">
|
||||
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/>
|
||||
</div>
|
||||
{% elif object.is_rig %}
|
||||
<span class="fas fa-exclamation-sign"></span>
|
||||
<span class="fas fa-exclamation-sign"></span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<h3>
|
||||
<a href="{% url 'event_detail' object.pk %}">
|
||||
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
|
||||
| {{ object.name }} </a>
|
||||
{% if object.venue %}
|
||||
<small>at {{ object.venue }}</small>
|
||||
{% endif %}
|
||||
<br/><small>
|
||||
<h3>
|
||||
<a href="{% url 'event_detail' object.pk %}">{{ object.display_id }} | {{ object.name }}</a>
|
||||
{% if object.venue %}
|
||||
<small>at {{ object.venue }}</small>
|
||||
{% endif %}
|
||||
<br/><small>
|
||||
{{ object.start_date|date:"D d/m/Y" }}
|
||||
{% if object.has_start_time %}
|
||||
{{ object.start_time|date:"H:i" }}
|
||||
{{ object.start_time|date:"H:i" }}
|
||||
{% endif %}
|
||||
{% if object.end_date or object.has_end_time %}
|
||||
–
|
||||
–
|
||||
{% endif %}
|
||||
{% if object.end_date and object.end_date != object.start_date %}
|
||||
{{ object.end_date|date:"D d/m/Y" }}
|
||||
{{ object.end_date|date:"D d/m/Y" }}
|
||||
{% endif %}
|
||||
{% if object.has_end_time %}
|
||||
{{ object.end_time|date:"H:i" }}
|
||||
{{ object.end_time|date:"H:i" }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
{{ object.get_status_display }}
|
||||
</p>
|
||||
<p>
|
||||
{% if object.is_rig %}
|
||||
<strong>Client:</strong> {{ object.person.name }}
|
||||
{% if object.organisation %}
|
||||
for {{ object.organisation.name }}
|
||||
{% endif %}
|
||||
{% if object.dry_hire %}(Dry Hire){% endif %}
|
||||
{% else %}
|
||||
<strong>Non-Rig</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>MIC:</strong>
|
||||
{% if object.mic %}
|
||||
{{object.mic.name}}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
{% if object.meet_at %}
|
||||
<p>
|
||||
<strong>Crew meet:</strong>
|
||||
{{ object.meet_at|date:"H:i" }} {{ object.meet_at|date:"(Y-m-d)" }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if object.access_at %}
|
||||
<p>
|
||||
<strong>Access at:</strong>
|
||||
{{ object.access_at|date:"H:i" }} {{ object.access_at|date:"(Y-m-d)" }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>Last updated:</strong>
|
||||
{{ object.last_edited_at }} by "{{ object.last_edited_by.initials }}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if object.description %}
|
||||
</h3>
|
||||
{% include 'partials/event_status.html' %}
|
||||
<div class="row ml-2">
|
||||
<div class="col-xs-6 pr-2">
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{{ object.description|linebreaksbr }}
|
||||
{% if object.is_rig %}
|
||||
<strong>Client:</strong> {{ object.person.name }}
|
||||
{% if object.organisation %}
|
||||
for {{ object.organisation.name }}
|
||||
{% endif %}
|
||||
{% if object.dry_hire %}(Dry Hire){% endif %}
|
||||
{% else %}
|
||||
<strong>Non-Rig</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>MIC:</strong>
|
||||
{% if object.mic %}
|
||||
{{object.mic.name}}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-xs-6 px-2">
|
||||
{% if object.meet_at %}
|
||||
<p>
|
||||
<strong>Crew meet:</strong>
|
||||
{{ object.meet_at|date:"H:i" }} {{ object.meet_at|date:"(Y-m-d)" }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if object.access_at %}
|
||||
<p>
|
||||
<strong>Access at:</strong>
|
||||
{{ object.access_at|date:"H:i" }} {{ object.access_at|date:"(Y-m-d)" }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>Last updated:</strong>
|
||||
{{ object.last_edited_at }} by "{{ object.last_edited_by.initials }}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if object.description %}
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{{ object.description|linebreaksbr }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,25 +7,21 @@
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'css/flatpickr.css' %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
|
||||
<script src="{% static 'js/selects.js' %}"></script>
|
||||
<script src="{% static 'js/simplemde.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise--->
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
<script src="{% static 'js/modal.js' %}"></script>
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
|
||||
{% include 'partials/datetime-fix.html' %}
|
||||
|
||||
@@ -33,18 +29,26 @@
|
||||
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
|
||||
$(document).ready(function () {
|
||||
dur = matches ? 0 : 500;
|
||||
{% if not object.pk and not form.errors %}
|
||||
$('.form-hws').slideUp(dur, function () {
|
||||
$('.form-is_rig').slideUp(dur);
|
||||
});
|
||||
{% elif not object.pk and form.errors %}
|
||||
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
|
||||
{% if object.pk %}
|
||||
// Editing
|
||||
{% if not object.is_rig %}
|
||||
$('.form-is_rig').hide();
|
||||
}
|
||||
{% endif %}
|
||||
{% if not object.pk %}
|
||||
{% endif %}
|
||||
//Creation
|
||||
{% else %}
|
||||
// If there were errors, apply the previous Rig/not-Rig selection
|
||||
{% if form.errors %}
|
||||
$('.form-hws').show();
|
||||
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
|
||||
$('.form-is_rig').hide();
|
||||
}
|
||||
{% else %}
|
||||
//Initial hide
|
||||
$('.form-hws').slideUp(dur);
|
||||
{% endif %}
|
||||
//Button handling
|
||||
$('#is_rig-selector button').on('click', function () {
|
||||
$('.form-non_rig').slideDown(dur);
|
||||
$('.form-non_rig').slideDown(dur); //Non rig stuff also needed for rig, so always slide down
|
||||
if ($(this).data('is_rig') === 1) {
|
||||
$('#{{form.is_rig.auto_id}}').prop('checked', true);
|
||||
if ($('.form-non_rig').is(':hidden')) {
|
||||
@@ -54,7 +58,6 @@
|
||||
}
|
||||
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
|
||||
} else {
|
||||
|
||||
$('#{{form.is_rig.auto_id}}').prop('checked', false);
|
||||
$('.form-is_rig').slideUp(dur);
|
||||
}
|
||||
@@ -62,24 +65,27 @@
|
||||
{% endif %}
|
||||
});
|
||||
$(document).ready(function () {
|
||||
setupMDE('#id_description');
|
||||
setupMDE('#id_notes');
|
||||
setupMDE('#item_description');
|
||||
|
||||
$('#itemModal').on('shown.bs.modal', function (e) {
|
||||
$('#item_description').data('mde_editor').value(
|
||||
$('#item_description').val()
|
||||
);
|
||||
});
|
||||
|
||||
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
|
||||
});
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
})
|
||||
});
|
||||
</script>
|
||||
<noscript>
|
||||
<style>
|
||||
.form-hws {
|
||||
display: inherit !important;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'item_modal.html' %}
|
||||
<form class=" itemised_form" role="form" method="POST">
|
||||
{% include 'partials/item_modal.html' %}
|
||||
<form class="itemised_form" role="form" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@@ -174,7 +180,7 @@
|
||||
<label for="{{ form.description.id_for_label }}"
|
||||
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
|
||||
|
||||
<div class="col-sm-8">
|
||||
<div class="col-sm-12">
|
||||
{% render_field form.description class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,7 +338,7 @@
|
||||
|
||||
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
||||
<label for="{{ form.purchase_order.id_for_label }}"
|
||||
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
|
||||
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
|
||||
|
||||
<div class="col-sm-8">
|
||||
{% render_field form.purchase_order class+="form-control" %}
|
||||
@@ -351,10 +357,10 @@
|
||||
<div class="col-sm-12">
|
||||
<div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
|
||||
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
|
||||
{% render_field form.notes class+="form-control" %}
|
||||
{% render_field form.notes class+="form-control md-enabled" %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'item_table.html' %}
|
||||
{% include 'partials/item_table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
{% load multiply from filters %}
|
||||
{% load static %}
|
||||
<!DOCTYPE document SYSTEM "rml.dtd">
|
||||
|
||||
<document filename="{{filename}}">
|
||||
<docinit>
|
||||
<registerTTFont faceName="OpenSans" fileName="{{ fonts.opensans.regular }}"/>
|
||||
<registerTTFont faceName="OpenSans-Bold" fileName="{{ fonts.opensans.bold }}"/>
|
||||
<registerTTFont faceName="OpenSans" fileName="static/fonts/OpenSans-Regular.tff"/>
|
||||
<registerTTFont faceName="OpenSans-Bold" fileName="static/fonts/OpenSans-Bold.tff"/>
|
||||
<registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/>
|
||||
</docinit>
|
||||
|
||||
@@ -77,16 +74,24 @@
|
||||
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
|
||||
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
|
||||
</blockTableStyle>
|
||||
|
||||
<listStyle name="ol"
|
||||
bulletFormat="%s."
|
||||
bulletFontSize="10" />
|
||||
|
||||
<listStyle name="ul"
|
||||
start="bulletchar"
|
||||
bulletFontSize="10"/>
|
||||
</stylesheet>
|
||||
|
||||
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||
<pageTemplate id="Headed" >
|
||||
<pageGraphics>
|
||||
<image file="RIGS/static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
||||
<image file="RIGS/static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
||||
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
||||
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
||||
|
||||
{# logo positioned 42 from left, 33 from top #}
|
||||
<image file="RIGS/static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
|
||||
<image file="static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
|
||||
|
||||
<setFont name="OpenSans-Bold" size="22.5" leading="10"/>
|
||||
<drawString x="137" y="780">TEC PA & Lighting</drawString>
|
||||
@@ -110,8 +115,8 @@
|
||||
|
||||
<pageTemplate id="Main">
|
||||
<pageGraphics>
|
||||
<image file="RIGS/static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
|
||||
<image file="RIGS/static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
||||
<image file="static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
|
||||
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
||||
|
||||
<setFont name="OpenSans" size="10"/>
|
||||
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{% load markdown_tags %}
|
||||
{% load filters %}
|
||||
|
||||
<setNextFrame name="main"/>
|
||||
<nextFrame/>
|
||||
<blockTable style="headLayout" colWidths="330,165">
|
||||
@@ -10,10 +12,8 @@
|
||||
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
||||
</para>
|
||||
|
||||
<keepInFrame>
|
||||
<para style="style.event_description">
|
||||
{{ object.description|default_if_none:""|linebreaksxml }}
|
||||
</para>
|
||||
<keepInFrame maxHeight="500" onOverflow="shrink">
|
||||
{{ object.description|default_if_none:""|markdown:"rml" }}
|
||||
</keepInFrame>
|
||||
</td>
|
||||
<td>
|
||||
@@ -184,25 +184,27 @@
|
||||
{% if item.description %}
|
||||
</para>
|
||||
<para style="item_description">
|
||||
<em>{{ item.description|linebreaksxml }}</em>
|
||||
{{ item.description|markdown:"rml" }}
|
||||
</para>
|
||||
<para>
|
||||
{% endif %}
|
||||
</para>
|
||||
</td>
|
||||
<td>£ {{ item.cost|floatformat:2 }}</td>
|
||||
<td>£{{ item.cost|floatformat:2 }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>£ {{ item.total_cost|floatformat:2 }}</td>
|
||||
<td>£{{ item.total_cost|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</blockTable>
|
||||
<keepTogether>
|
||||
<blockTable style="totalTable" colWidths="300,115,80">
|
||||
{% if object.vat > 0 %}
|
||||
<tr>
|
||||
<td>{% if quote %}VAT Registration Number: 170734807{% endif %}</td>
|
||||
<td>Total (ex. VAT)</td>
|
||||
<td>{% if quote %}VAT Registration Number: 170734807</td>
|
||||
<td>Total (ex. VAT){% endif %}</td>
|
||||
<td>£ {{ object.sum_total|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if quote %}
|
||||
@@ -211,8 +213,10 @@
|
||||
</para>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if object.vat > 0 %}
|
||||
<td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td>
|
||||
<td>£ {{ object.vat|floatformat:2 }}</td>
|
||||
<td>£{{ object.vat|floatformat:2 }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -224,7 +228,7 @@
|
||||
</td>
|
||||
{% if invoice %}
|
||||
<td>Total</td>
|
||||
<td>£ {{ object.total|floatformat:2 }}</td>
|
||||
<td>£{{ object.total|floatformat:2 }}</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<para>
|
||||
@@ -233,7 +237,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<para>
|
||||
<b>£ {{ object.total|floatformat:2 }}</b>
|
||||
<b>£{{ object.total|floatformat:2 }}</b>
|
||||
</para>
|
||||
</td>
|
||||
{% endif %}
|
||||
@@ -267,7 +271,7 @@
|
||||
<tr>
|
||||
<td>{{ payment.get_method_display }}</td>
|
||||
<td>{{ payment.date }}</td>
|
||||
<td>£ {{ payment.amount|floatformat:2 }}</td>
|
||||
<td>£{{ payment.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</blockTable>
|
||||
@@ -275,18 +279,18 @@
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>Payment Total</td>
|
||||
<td>£ {{ object.invoice.payment_total|floatformat:2 }}</td>
|
||||
<td>£{{ object.invoice.payment_total|floatformat:2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<para>
|
||||
<b>Balance</b> (ex. VAT)
|
||||
<b>Balance</b> {% if object.vat > 0 %}(ex. VAT){% endif %}
|
||||
</para>
|
||||
</td>
|
||||
<td>
|
||||
<para>
|
||||
<b>£ {{ object.invoice.balance|floatformat:2 }}</b>
|
||||
<b>£{{ object.invoice.balance|floatformat:2 }}</b>
|
||||
</para>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -316,7 +320,7 @@
|
||||
<tr>
|
||||
<td>General Enquires and 24 Hour Emergency Contact: 0115 84 68720</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% elif object.vat > 0 %}
|
||||
<tr>
|
||||
<td>
|
||||
<para>VAT Registration Number: 170734807</para>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<blockTable style="signatureTable" colWidths="50,120,60,120,35,110">
|
||||
<tr>
|
||||
<td>Signature</td>
|
||||
<td></td>
|
||||
<td>Print Name</td>
|
||||
<td></td>
|
||||
<td>Date</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="col-sm-12">
|
||||
<div class="card">
|
||||
{% with object=event auth=True %}
|
||||
{% include 'item_table.html' %}
|
||||
{% include 'partials/item_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
{% extends 'base_client_email.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<p>Hi {{ to_name|default:"there" }},</p>
|
||||
|
||||
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>N{{ object.pk|stringformat:"05d" }}
|
||||
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{{ object.person.name }}</b>{% endif %}.</p>
|
||||
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>{{ object.display_id }}
|
||||
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{% if object.person %}{{ object.person.name }}{% else %}{{ object.organisation.name }}{% endif %}</b>{% endif %}.</p>
|
||||
|
||||
<p>
|
||||
Please find the link below to complete the event booking process.
|
||||
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
|
||||
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
|
||||
this
|
||||
email on.
|
||||
{% endif %}
|
||||
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
|
||||
this
|
||||
email on.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -23,7 +21,7 @@
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="button" align="center">
|
||||
<a href="{{ request.scheme }}://{{ request.get_host }}{% url 'event_authorise' object.pk hmac %}">
|
||||
<a href="{{ request.scheme }}://{{ request.get_host }}{% url target|default:'event_authorise' object.pk hmac %}">
|
||||
Complete Authorisation Form
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Hi {{ to_name|default:"there" }},
|
||||
|
||||
{{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {{ object.person.name }}{% endif %}.
|
||||
{{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {% if object.person %}{{ object.person.name }}{% else %}{{ object.organisation.name }}{% endif %}{% endif %}.
|
||||
|
||||
Please find the link below to complete the event booking process.
|
||||
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
{% extends 'eventauthorisation.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block js %}
|
||||
{% endblock %}
|
||||
|
||||
{% block authorisation %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
@@ -86,7 +83,7 @@
|
||||
|
||||
<div class="text-right">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary btn-lg" type="submit">Authorise</button>
|
||||
<button class="btn btn-primary btn-lg" type="submit" {% if preview %}disabled="" data-toggle="tooltip" title="This is only a preview!"{%endif%}>Authorise</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
{% load button from filters %}
|
||||
|
||||
{% block title %}Request Authorisation{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
<script src="{% static 'js/popover.js' %}"></script>
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
var clipboard = new ClipboardJS('.btn');
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
$(e.trigger).popover('show');
|
||||
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
|
||||
e.clearSelection();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="alert alert-warning">
|
||||
<div class="alert alert-warning pb-0">
|
||||
<h1>Send authorisation request email.</h1>
|
||||
<p>Pressing send will email the address provided. Please triple check everything before continuing.</p>
|
||||
<p>Pressing send will email the address provided. <strong>Please triple check everything before continuing.</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<div class="alert alert-info pb-0">
|
||||
{% if object.person.email or object.organisation.email %}
|
||||
<dl class="dl-horizontal">
|
||||
{% if object.person.email %}
|
||||
<dt>Person Email</dt>
|
||||
<dd>{{ object.person.email }}</dd>
|
||||
|
||||
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
|
||||
{% endif %}
|
||||
{% if object.organisation.email %}
|
||||
<dt>Organisation Email</dt>
|
||||
<dd>{{ object.organisation.email }}</dd>
|
||||
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>No email addresses saved to the client ಠ_ಠ</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form">
|
||||
{% csrf_token %}
|
||||
@@ -30,14 +54,6 @@
|
||||
{% render_field form.email type="email" class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right col-sm-3 offset-sm-9">
|
||||
<div class="form-group">
|
||||
<button type="submit" class="form-control btn btn-primary">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,3 +64,14 @@
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<div class="form-row">
|
||||
<div class="btn-group" role="group">
|
||||
<a type="button" target="_blank" href="{% url 'event_authorise_preview' object.pk %}" class="btn btn-info text-nowrap"><span class="fas fa-drafting-compass"></span> Preview</a>
|
||||
<button type="submit" class="form-control btn btn-primary" form="auth-request-form">
|
||||
<span class="fas fa-paper-plane"></span> Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<table class="table mb-0 table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Event</th>
|
||||
<th scope="col">MIC</th>
|
||||
<th scope="col">Dates</th>
|
||||
<th scope="col">RA</th>
|
||||
<th scope="col">Checklists</th>
|
||||
@@ -16,7 +17,8 @@
|
||||
<tbody>
|
||||
{% for event in object_list %}
|
||||
<tr id="event_row">
|
||||
<th scope="row" id="event_number"><a href="{% url 'event_detail' event.pk %}">{{ event }}</a></th>
|
||||
<th scope="row" id="event_number"><a href="{% url 'event_detail' event.pk %}">{{ event }}</a><br><small>{{ event.get_status_display }}</small></th>
|
||||
<td>{% if event.mic is not None %}<a href="{% url 'profile_detail' event.mic.pk %}">{% else %}<span class="text-danger">{% endif %}{{ event.mic }}{% if event.mic is not None %}</a>{% else %}</span>{%endif%}</td>
|
||||
<!--Dates-->
|
||||
<td id="event_dates">
|
||||
<span><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></span>
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<table class="table mb-0 table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Event</th>
|
||||
{# mmm hax #}
|
||||
{% if object_list.0 != None %}
|
||||
{% for field in fields %}
|
||||
{% for field in object_list.0.fieldz %}
|
||||
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -32,8 +32,8 @@
|
||||
{% for object in object_list %}
|
||||
<tr class="{% if object.reviewed_by %}table-success{%endif%}">
|
||||
{# General #}
|
||||
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a></th>
|
||||
{% for field in fields %}
|
||||
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
|
||||
{% for field in object_list.0.fieldz %}
|
||||
<td>{{ object|get_field:field }}</td>
|
||||
{% endfor %}
|
||||
{# Buttons #}
|
||||
|
||||
@@ -2,102 +2,88 @@
|
||||
{% load button from filters %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row justify-content-end py-3">
|
||||
<div class="col-sm-4 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
|
||||
<span class="fas fa-times"></span> <span
|
||||
class="d-none d-sm-inline">Delete</span>
|
||||
</a>
|
||||
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice">
|
||||
<span class="fas fa-ban"></span> <span
|
||||
class="d-none d-sm-inline">Void</span>
|
||||
</a>
|
||||
{% button 'print' url='invoice_print' pk=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="card card-default">
|
||||
<div class="card-header">Invoice Details<span class="float-right">
|
||||
{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(OUTSTANDING){% endif %}
|
||||
</span></div>
|
||||
<div class="card-body">
|
||||
{% if object.event.organisation %}
|
||||
{{ object.event.organisation.name }}<br/>
|
||||
{{ object.event.organisation.address|linebreaksbr }}
|
||||
{% else %}
|
||||
{{ object.event.person.name }}<br/>
|
||||
{{ object.event.person.address|linebreaksbr }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
{% include 'partials/event_details.html' %}
|
||||
</div>
|
||||
{% if object.event.internal %}
|
||||
<div class="col-sm-6">
|
||||
{% include 'partials/auth_details.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row py-4">
|
||||
<div class="col-sm-6">
|
||||
<div class="card card-default">
|
||||
<div class="card-body">
|
||||
<div class="text-right py-3">
|
||||
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
|
||||
class="btn btn-success modal-href"
|
||||
data-target="#{{ form.person.id_for_label }}">
|
||||
<span class="fas fa-plus"></span> Add
|
||||
</a>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Amount</th>
|
||||
<th scope="col">Method</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for payment in object.payment_set.all %}
|
||||
<tr>
|
||||
<th scope="row">{{ payment.date }}</th>
|
||||
<td>{{ payment.amount|floatformat:2 }}</td>
|
||||
<td>{{ payment.get_method_display }}</td>
|
||||
<td>
|
||||
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"></span></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="text-right"><strong>Balance:</strong></td>
|
||||
<td>{{ object.balance|floatformat:2 }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
{% with object.event as object %}
|
||||
{% include 'item_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 text-right">
|
||||
{% include 'partials/last_edited.html' with target="invoice_history" %}
|
||||
<div class="row py-4">
|
||||
<div class="col-sm-12 text-right px-0">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
|
||||
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
|
||||
<span class="fas fa-times"></span> <span
|
||||
class="d-none d-sm-inline">Delete</span>
|
||||
</a>
|
||||
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice">
|
||||
<span class="fas fa-ban"></span> <span
|
||||
class="d-none d-sm-inline">Void</span>
|
||||
</a>
|
||||
{% button 'print' url='invoice_print' pk=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row py-4">
|
||||
{% with object.event as object %}
|
||||
<div class="col-sm-6">
|
||||
{% include 'partials/contact_details.html' %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
{% include 'partials/event_details.html' %}
|
||||
</div>
|
||||
{% if object.event.internal %}
|
||||
<div class="col-sm-6">
|
||||
{% include 'partials/auth_details.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="row py-4">
|
||||
<div class="col-sm-6">
|
||||
<div class="card card-default">
|
||||
<div class="card-body">
|
||||
<div class="text-right py-3">
|
||||
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
|
||||
class="btn btn-success modal-href"
|
||||
data-target="#{{ form.person.id_for_label }}">
|
||||
<span class="fas fa-plus"></span> Add
|
||||
</a>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Amount</th>
|
||||
<th scope="col">Method</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for payment in object.payment_set.all %}
|
||||
<tr>
|
||||
<th scope="row">{{ payment.date }}</th>
|
||||
<td>{{ payment.amount|floatformat:2 }}</td>
|
||||
<td>{{ payment.get_method_display }}</td>
|
||||
<td>
|
||||
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"></span></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="text-right"><strong>Balance:</strong></td>
|
||||
<td>{{ object.balance|floatformat:2 }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
{% with object.event as object %}
|
||||
{% include 'partials/item_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 text-right">
|
||||
{% include 'partials/last_edited.html' with target="invoice_history" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<td>{{ invoice.event.start_date }}</td>
|
||||
<td>{{ invoice.invoice_date }}</td>
|
||||
<td>
|
||||
{{ invoice.balance|floatformat:2 }}
|
||||
£{{ invoice.balance|floatformat:2 }}
|
||||
{% if not invoice.event.internal %}
|
||||
<br />
|
||||
<span class="text-muted">{{ invoice.event.purchase_order }}</span>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ event.sum_total|floatformat:2 }}
|
||||
£{{ event.sum_total|floatformat:2 }}
|
||||
<br />
|
||||
<span class="text-muted">{% if not event.internal %}{{ event.purchase_order }}{% endif %}</span>
|
||||
</td>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<div class="card card-default
|
||||
{% if object.authorised %}
|
||||
card-success
|
||||
{% if event.authorised %}
|
||||
border-success
|
||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||
card-warning
|
||||
border-warning
|
||||
{% elif event.auth_request_to %}
|
||||
card-info
|
||||
border-info
|
||||
{% endif %}
|
||||
">
|
||||
<div class="card-header">Client Authorisation</div>
|
||||
<div class="card-body row">
|
||||
<dl class="col-md-6">
|
||||
<dl class="col-sm-6">
|
||||
<dt>Authorisation Request</dt>
|
||||
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<dt>To</dt>
|
||||
<dd>{{ object.auth_request_to }}</dd>
|
||||
</dl>
|
||||
<dd class="d-block d-sm-none"> </dd>
|
||||
<dl class="col-md-6">
|
||||
<dl class="col-sm-6">
|
||||
<hr class="d-block d-sm-none">
|
||||
<dt>Authorised</dt>
|
||||
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<div class="col-sm-6">
|
||||
{% if event.person %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Contact Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">Person</dt>
|
||||
<dd class="col-sm-7">
|
||||
{% if event.person %}
|
||||
{{ event.person.name }}
|
||||
{% endif %}
|
||||
{{ event.person.name }}
|
||||
</dd>
|
||||
|
||||
{% if event.person.email %}
|
||||
<dt class="col-sm-5">Email</dt>
|
||||
<dd class="col-sm-7">
|
||||
<span class="overflow-ellipsis">{{ event.person.email }}</span>
|
||||
</dd>
|
||||
|
||||
{% endif %}
|
||||
{% if event.person.phone %}
|
||||
<dt class="col-sm-5">Phone Number</dt>
|
||||
<dd class="col-sm-7">{{ event.person.phone }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.organisation %}
|
||||
<div class="card mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">Organisation Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
@@ -29,9 +31,10 @@
|
||||
<dd class="col-sm-7">
|
||||
{{ event.organisation.name }}
|
||||
</dd>
|
||||
|
||||
{% if event.organisation.phone %}
|
||||
<dt class="col-sm-5">Phone Number</dt>
|
||||
<dd class="col-sm-7">{{ object.organisation.phone }}</dd>
|
||||
<dd class="col-sm-7">{{ event.organisation.phone }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,15 +46,12 @@
|
||||
<div class="card-header">Event Info</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
{% if event.venue %}
|
||||
<dt class="col-sm-5">Event Venue</dt>
|
||||
<dd class="col-sm-7">
|
||||
{% if object.venue %}
|
||||
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
|
||||
{{ object.venue }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ event.venue }}
|
||||
</dd>
|
||||
|
||||
{% endif %}
|
||||
<dt class="col-sm-5">Status</dt>
|
||||
<dd class="col-sm-7">{{ event.get_status_display }}</dd>
|
||||
|
||||
|
||||
47
RIGS/templates/partials/contact_details.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% load linkornone from filters %}
|
||||
{% load namewithnotes from filters %}
|
||||
|
||||
{% if object.person %}
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">Person Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Person</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% if object.person %}
|
||||
<a href="{% url 'person_detail' object.person.pk %}" class="modal-href">
|
||||
{{ object.person|namewithnotes:'person_detail' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt class="col-sm-6">Email</dt>
|
||||
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
|
||||
<dt class="col-sm-6">Phone Number</dt>
|
||||
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if object.organisation %}
|
||||
<div class="card card-default">
|
||||
<div class="card-header">Organisation Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Organisation</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% if object.organisation %}
|
||||
<a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href">
|
||||
{{ object.organisation|namewithnotes:'organisation_detail' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt class="col-sm-6">Email</dt>
|
||||
<dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd>
|
||||
<dt class="col-sm-6">Phone Number</dt>
|
||||
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
|
||||
<dt class="col-sm-6">Has SU Account</dt>
|
||||
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -9,7 +9,7 @@
|
||||
{% if event.internal %}
|
||||
<a class="btn item-add modal-href event-authorise-request
|
||||
{% if event.authorised %}
|
||||
btn-success
|
||||
btn-success active
|
||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||
btn-warning
|
||||
{% elif event.auth_request_to %}
|
||||
@@ -19,7 +19,7 @@
|
||||
{% endif %}
|
||||
"
|
||||
href="{% url 'event_authorise_request' object.pk %}">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
<span class="fas fa-paper-plane"></span>
|
||||
<span class="d-none d-sm-inline">
|
||||
{% if event.authorised %}
|
||||
Authorised
|
||||
@@ -47,5 +47,7 @@
|
||||
class="fas fa-pound-sign"></span>
|
||||
<span class="d-none d-sm-inline">Invoice</span></a>
|
||||
{% endif %}
|
||||
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load namewithnotes from filters %}
|
||||
{% load markdown_tags %}
|
||||
<div class="card card-info">
|
||||
<div class="card-header">Event Info</div>
|
||||
<div class="card-body">
|
||||
@@ -20,15 +21,7 @@
|
||||
|
||||
{% if event.is_rig %}
|
||||
<dt class="col-sm-6">Event MIC</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% if event.mic and perms.RIGS.view_profile %}
|
||||
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
|
||||
{{ event.mic.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ event.mic.name }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dd class="col-sm-6">{% include 'partials/linked_name.html' with profile=event.mic %}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-6">Status</dt>
|
||||
@@ -54,7 +47,7 @@
|
||||
<dd class="col-sm-12"> </dd>
|
||||
|
||||
<dt class="col-sm-6">Event Description</dt>
|
||||
<dd class="dont-break-out col-sm-12">{{ event.description|linebreaksbr }}</dd>
|
||||
<dd class="dont-break-out col-sm-12">{{ event.description|markdown }}</dd>
|
||||
|
||||
<dd class="col-sm-12"> </dd>
|
||||
|
||||
@@ -71,7 +64,7 @@
|
||||
|
||||
{% if event.dry_hire %}
|
||||
<dt class="col-sm-6">Checked In By</dt>
|
||||
<dd class="col-sm-6">{{ object.checked_in_by.name }}</dd>
|
||||
<dd class="col-sm-6">{% include 'partials/linked_name.html' with profile=event.checked_in_by %}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if event.is_rig %}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<h5>
|
||||
<div>
|
||||
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
|
||||
{% if event.is_rig %}
|
||||
{% if event.purchase_order %}
|
||||
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
|
||||
{% elif event.authorised %}
|
||||
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
|
||||
{% if event.sum_total > 0 %}
|
||||
{% if event.purchase_order %}
|
||||
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
|
||||
{% elif event.authorised %}
|
||||
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
|
||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||
<span class="badge badge-warning"> Authorisation: Issue <span class="fas fa-exclamation-circle"></span></span>
|
||||
{% elif event.auth_request_to %}
|
||||
<span class="badge badge-info"> Authorisation: Sent <span class="fas fa-paper-plane"></span></span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not event.dry_hire %}
|
||||
{% if event.riskassessment %}
|
||||
@@ -14,8 +20,6 @@
|
||||
{% else %}
|
||||
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">RA: N/A</span>
|
||||
{% endif %}
|
||||
{% if not event.dry_hire %}
|
||||
{% if event.hs_done %}
|
||||
@@ -24,8 +28,6 @@
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">Checklist: N/A</span>
|
||||
{% endif %}
|
||||
{% if perms.RIGS.view_invoice %}
|
||||
{% if event.invoice %}
|
||||
@@ -41,4 +43,4 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
@@ -25,30 +25,30 @@
|
||||
{% endif %}
|
||||
{% else %}
|
||||
table-warning
|
||||
{% endif %}" id="event_row">
|
||||
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||
<!---Number-->
|
||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||
<!--Dates & Times-->
|
||||
<td id="event_dates">
|
||||
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}</strong>
|
||||
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
|
||||
{% if event.has_start_time %}
|
||||
{{ event.start_time|date:"H:i" }}
|
||||
{% endif %}
|
||||
{% endif %}</strong>
|
||||
</span>
|
||||
{% if event.end_date %}
|
||||
<br>
|
||||
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}</strong>{% endif %}
|
||||
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
|
||||
{% if event.has_end_time %}
|
||||
{{ event.end_time|date:"H:i" }}
|
||||
{% endif %}
|
||||
{% endif %}</strong>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not event.cancelled %}
|
||||
{% if event.meet_at %}
|
||||
<br><span>Crew meet: <strong>{{ event.meet_at|date:"H:i" }}</strong> {{ event.meet_at|date:"(d/m/Y)" }}</span>
|
||||
<br><span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
|
||||
{% endif %}
|
||||
{% if event.access_at %}
|
||||
<br><span>Access at: <strong>{{ event.access_at|date:"H:i" }}</strong> {{ event.access_at|date:"(d/m/Y)" }}</span>
|
||||
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:" D d/m/Y H:i" }}</strong></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -67,9 +67,9 @@
|
||||
</h4>
|
||||
{% if event.is_rig and not event.cancelled %}
|
||||
<h5>
|
||||
{{ event.person.name }}
|
||||
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
|
||||
{% if event.organisation %}
|
||||
for {{ event.organisation.name }}
|
||||
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% endif %}
|
||||
@@ -90,7 +90,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif event.is_rig %}
|
||||
<span class="fas fa-exclamation"></span>
|
||||
<span class="fas fa-user-slash"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
id="item_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<div class="form-group form-row" data-toggle="tooltip" title="A detailed description of the kit. MD enabled.">
|
||||
<label for="item_description" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea type="text" placeholder="Description" class="form-control"
|
||||
<textarea type="text" placeholder="Description" class="form-control md-enabled"
|
||||
id="item_description" rows="8"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,16 +1,17 @@
|
||||
{% load markdown_tags %}
|
||||
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
|
||||
<th scope="row">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="item-description">
|
||||
<em class="description">{{item.description|linebreaksbr}}</em>
|
||||
<em class="description">{{item.description|markdown}}</em>
|
||||
</div>
|
||||
</th>
|
||||
{% if perms.RIGS.view_event %}
|
||||
<td>£ <span class="cost">{{item.cost|floatformat:2}}</span></td>
|
||||
<td>£<span class="cost">{{item.cost|floatformat:2}}</span></td>
|
||||
{% endif %}
|
||||
<td class="quantity">{{item.quantity}}</td>
|
||||
{% if perms.RIGS.view_event %}
|
||||
<td>£ <span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
|
||||
<td>£<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
|
||||
{% endif %}
|
||||
{% if edit %}
|
||||
<td class="vert-align text-right">
|
||||
@@ -23,16 +23,17 @@
|
||||
</thead>
|
||||
<tbody id="item-table-body">
|
||||
{% for item in object.items.all %}
|
||||
{% include 'item_row.html' %}
|
||||
{% include 'partials/item_row.html' %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% if auth or perms.RIGS.view_event %}
|
||||
<tfoot>
|
||||
<tfoot style="font-weight: bold">
|
||||
<tr>
|
||||
<td rowspan="3" colspan="2"></td>
|
||||
<td>Total (ex. VAT)</td>
|
||||
<td colspan="2">£ <span id="sumtotal">{{object.sum_total|default:0|floatformat:2}}</span></td>
|
||||
<td>Total {% if object.vat > 0 or not object.pk %}(ex. VAT){% endif %}</td>
|
||||
<td colspan="2">£<span id="sumtotal">{{object.sum_total|default:0|floatformat:2}}</span></td>
|
||||
</tr>
|
||||
{% if object.vat > 0 or not object.pk %}
|
||||
<tr>
|
||||
{% if not object.pk %}
|
||||
<td id="vat-rate" data-rate="{{currentVAT.rate}}">VAT @
|
||||
@@ -41,12 +42,13 @@
|
||||
<td id="vat-rate" data-rate="{{object.vat_rate.rate}}">VAT @
|
||||
{{object.vat_rate.as_percent|floatformat|default:"TBD"}}%</td>
|
||||
{% endif %}
|
||||
<td colspan="2">£ <span id="vat">{{object.vat|default:0|floatformat:2}}</span></td>
|
||||
<td colspan="2">£<span id="vat">{{object.vat|default:0|floatformat:2}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<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>
|
||||
{% endif %}
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
@@ -59,9 +61,9 @@
|
||||
<em class="description"></em>
|
||||
</div>
|
||||
</td>
|
||||
<td>£ <span class="cost"></span></td>
|
||||
<td>£<span class="cost"></span></td>
|
||||
<td class="quantity"></td>
|
||||
<td>£ <span class="sub-total"></span></td>
|
||||
<td>£<span class="sub-total"></span></td>
|
||||
{% if edit %}
|
||||
<td class="vert-align text-right">
|
||||
<div class="btn-group" role="group" aria-label="Action buttons">
|
||||
7
RIGS/templates/partials/linked_name.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% if profile and perms.RIGS.view_profile %}
|
||||
<a href="{% url 'profile_detail' profile.pk %}" class="modal-href">
|
||||
{{ profile.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ profile.name }}
|
||||
{% endif %}
|
||||
@@ -2,9 +2,9 @@
|
||||
{% load button from filters %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row align-items-center justify-content-between py-2">
|
||||
<div class="col-sm-12 col-md">
|
||||
Key: <span class="table-success mr-1 px-2">Ready</span><span class="table-warning mr-1 px-2">Action Required</span><span class="table-danger mr-1 px-2">Needs MIC</span><span class="table-secondary mr-1 px-2">Cancelled</span><span class="table-info px-2">Non-Rig</span>
|
||||
<div class="row align-items-center justify-content-between py-2 align-middle">
|
||||
<div class="col-sm-12 col-md align-middle">
|
||||
Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span class="table-warning mr-1 px-2 rounded">Action Required</span><span class="table-danger mr-1 px-2 rounded">Needs MIC</span><span class="table-secondary mr-1 px-2 rounded">Cancelled</span><span class="table-info px-2 rounded">Non-Rig</span>
|
||||
</div>
|
||||
{% if perms.RIGS.add_event %}
|
||||
<div class="col text-right">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% block title %}Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}{% endblock %}
|
||||
{% load help_text from filters %}
|
||||
{% load yesnoi from filters %}
|
||||
{% load linkornone from filters %}
|
||||
@@ -7,7 +6,6 @@
|
||||
{% block content %}
|
||||
<div class="row py-3">
|
||||
<div class="col-12">
|
||||
<h3>Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}</h3>
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">General</div>
|
||||
<div class="card-body">
|
||||
@@ -47,15 +45,15 @@
|
||||
<dd class="col-sm-6">
|
||||
{{ object.big_power|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'power_mic' }}</dt>
|
||||
<dt class="col-sm-6">{{ object|help_text:'power_mic'|safe }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.power_mic.name|default:'None' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'generators' }}</dt>
|
||||
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.outside|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
|
||||
<dt class="col-sm-6">{{ object|help_text:'generators' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.generators|yesnoi:'invert' }}
|
||||
</dd>
|
||||
@@ -97,58 +95,64 @@
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">Site Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">{{ object|help_text:'known_venue' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.known_venue|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'safe_loading'|safe }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.safe_loading|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'safe_storage' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.safe_storage|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'area_outside_of_control' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.area_outside_of_control|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'barrier_required' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.barrier_required|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
|
||||
</dd>
|
||||
</dl>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">Site Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-10">{{ object|help_text:'known_venue' }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.known_venue|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'safe_loading'|safe }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.safe_loading|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'safe_storage' }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.safe_storage|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'area_outside_of_control' }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.area_outside_of_control|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'barrier_required' }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.barrier_required|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">Structures</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">{{ object|help_text:'special_structures' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.special_structures|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'suspended_structures' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.suspended_structures|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'persons_responsible_structures' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
|
||||
</dd>
|
||||
<dt class="col-6">{{ object|help_text:'rigging_plan'|safe }}</dt>
|
||||
<dd class="col-6">
|
||||
{{ object.rigging_plan|linkornone }}
|
||||
</dd>
|
||||
</dl>
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">Structures</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-10">{{ object|help_text:'special_structures' }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.special_structures|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-10">{{ object|help_text:'suspended_structures' }}</dt>
|
||||
<dd class="col-2">
|
||||
{{ object.suspended_structures|yesnoi:'invert' }}
|
||||
</dd>
|
||||
<dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
|
||||
<dd class="col-12">
|
||||
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
|
||||
</dd>
|
||||
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
|
||||
<dd class="col-12">
|
||||
{{ object.rigging_plan|linkornone|default:'N/A' }}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,25 +5,16 @@
|
||||
{% load nice_errors from filters %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
|
||||
<script src="{% static 'js/selects.js' %}" async></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise--->
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
<script src="{% static 'js/modal.js' %}"></script>
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
|
||||
<script>
|
||||
function parseBool(str) {
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.forms.forms import NON_FIELD_ERRORS
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.template.defaultfilters import yesno, title, truncatewords
|
||||
from django.urls import reverse_lazy
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from django.utils.text import normalize_newlines
|
||||
@@ -173,7 +173,7 @@ def title_spaced(string):
|
||||
@register.filter(needs_autoescape=True)
|
||||
def namewithnotes(obj, url, autoescape=True):
|
||||
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
|
||||
return mark_safe(obj.name + " <a href='{}'><span class='far fa-sticky-note'></span></a>".format(reverse_lazy(url, kwargs={'pk': obj.pk})))
|
||||
return mark_safe(obj.name + " <a href='{}'><span class='fas fa-sticky-note'></span></a>".format(reverse(url, kwargs={'pk': obj.pk})))
|
||||
else:
|
||||
return obj.name
|
||||
|
||||
@@ -212,8 +212,25 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
||||
clazz += " btn-primary "
|
||||
icon = "fa-plus"
|
||||
text = "New"
|
||||
elif type == 'copy':
|
||||
return {'copy': True, 'id': id, 'style': style}
|
||||
elif type == 'search':
|
||||
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
|
||||
elif type == 'submit':
|
||||
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
|
||||
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def invoices_waiting():
|
||||
return len(models.Event.objects.waiting_invoices())
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def invoices_outstanding():
|
||||
return len(models.Invoice.objects.outstanding_invoices())
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def total_invoices_todo():
|
||||
return len(models.Event.objects.waiting_invoices()) + len(models.Invoice.objects.outstanding_invoices())
|
||||
|
||||
56
RIGS/templatetags/markdown_tags.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
import markdown
|
||||
|
||||
__author__ = 'ghost'
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="markdown")
|
||||
def markdown_filter(text, input_format='html'):
|
||||
# markdown library can't handle text=None
|
||||
if text is None:
|
||||
return text
|
||||
html = markdown.markdown(text, extensions=['markdown.extensions.nl2br'])
|
||||
# Convert format to RML
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
# Prevent code injection
|
||||
for script in soup('script'):
|
||||
script.string = "Your script shall not pass!"
|
||||
if input_format == 'html':
|
||||
return mark_safe('<div class="markdown">' + str(soup) + '</div>')
|
||||
elif input_format == 'rml':
|
||||
|
||||
# Image aren't supported so remove them
|
||||
for img in soup('img'):
|
||||
img.parent.extract()
|
||||
|
||||
# <code> should become <font>
|
||||
for c in soup('code'):
|
||||
c.name = 'font'
|
||||
c['face'] = "Courier"
|
||||
|
||||
# blockquotes don't exist but we can still do something to show
|
||||
for bq in soup('blockquote'):
|
||||
bq.name = 'pre'
|
||||
bq.string = bq.text
|
||||
|
||||
for alist in soup.find_all(['ul', 'ol']):
|
||||
alist['style'] = alist.name
|
||||
for li in alist.find_all('li', recursive=False):
|
||||
text = li.find(text=True)
|
||||
text.wrap(soup.new_tag('p'))
|
||||
|
||||
if alist.parent.name != 'li':
|
||||
indent = soup.new_tag('indent')
|
||||
indent['left'] = '0.6cm'
|
||||
|
||||
alist.wrap(indent)
|
||||
|
||||
# Paragraphs have a different tag
|
||||
for p in soup('p'):
|
||||
p.name = 'para'
|
||||
|
||||
return mark_safe(str(soup))
|
||||
108
RIGS/tests/conftest.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from RIGS import models
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_event(db):
|
||||
event = models.Event.objects.create(name="TE E1", start_date=timezone.now())
|
||||
yield event
|
||||
event.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ra(basic_event, admin_user):
|
||||
ra = models.RiskAssessment.objects.create(event=basic_event, nonstandard_equipment=False, nonstandard_use=False,
|
||||
contractors=False, other_companies=False, crew_fatigue=False,
|
||||
big_power=False, power_mic=admin_user, generators=False,
|
||||
other_companies_power=False, nonstandard_equipment_power=False,
|
||||
multiple_electrical_environments=False, noise_monitoring=False,
|
||||
known_venue=True, safe_loading=True, safe_storage=True,
|
||||
area_outside_of_control=True, barrier_required=True,
|
||||
nonstandard_emergency_procedure=True, special_structures=False,
|
||||
suspended_structures=False, outside=False)
|
||||
yield ra
|
||||
ra.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def medium_ra(ra):
|
||||
ra.big_power = True
|
||||
ra.save()
|
||||
yield ra
|
||||
ra.big_power = False
|
||||
ra.save()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def venue(db):
|
||||
venue = models.Venue.objects.create(name="Venue 1")
|
||||
yield venue
|
||||
venue.delete()
|
||||
|
||||
|
||||
@pytest.fixture # TODO parameterise with Event sizes
|
||||
def checklist(basic_event, venue, admin_user, ra):
|
||||
checklist = models.EventChecklist.objects.create(event=basic_event, power_mic=admin_user, safe_parking=False,
|
||||
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
||||
ear_plugs=False, hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
|
||||
date=timezone.now(), venue=venue)
|
||||
yield checklist
|
||||
checklist.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def many_events(db, admin_user, scope="class"):
|
||||
many_events = {
|
||||
# produce 7 normal events - 5 current
|
||||
1: models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6),
|
||||
description="start future no end"),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end"),
|
||||
3: models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(),
|
||||
description="start today with end today"),
|
||||
4: models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end"),
|
||||
5: models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21',
|
||||
description="start past with end past"),
|
||||
6: models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2),
|
||||
end_date=date.today() + timedelta(days=2),
|
||||
description="start past, end future"),
|
||||
7: models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2),
|
||||
end_date=date.today() + timedelta(days=2),
|
||||
description="start + end in future"),
|
||||
|
||||
# 2 cancelled - 1 current
|
||||
8: models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
|
||||
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
|
||||
description="cancelled in future"),
|
||||
9: models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
|
||||
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
|
||||
description="cancelled and started"),
|
||||
|
||||
# 5 dry hire - 3 current
|
||||
10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True,
|
||||
description="dryhire today"),
|
||||
11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True,
|
||||
checked_in_by=admin_user,
|
||||
description="dryhire today, checked in"),
|
||||
12: models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
|
||||
status=models.Event.BOOKED, description="dryhire past"),
|
||||
13: models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
|
||||
checked_in_by=admin_user, description="dryhire past checked in"),
|
||||
14: models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
|
||||
status=models.Event.CANCELLED, description="dryhire today cancelled"),
|
||||
|
||||
# 4 non rig - 3 current
|
||||
15: models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False,
|
||||
description="non rig today"),
|
||||
16: models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
|
||||
description="non rig tomorrow"),
|
||||
17: models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
|
||||
description="non rig yesterday"),
|
||||
18: models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False,
|
||||
status=models.Event.CANCELLED,
|
||||
description="non rig today cancelled"),
|
||||
}
|
||||
yield many_events
|
||||
for event in many_events:
|
||||
event.delete()
|
||||
@@ -52,8 +52,8 @@ class EventDetail(BasePage):
|
||||
URL_TEMPLATE = 'event/{event_id}'
|
||||
|
||||
# TODO Refactor into regions to match template fragmentation
|
||||
_event_name_selector = (By.XPATH, '//h1')
|
||||
_person_panel_selector = (By.XPATH, '//div[contains(text(), "Contact Details")]/..')
|
||||
_event_name_selector = (By.XPATH, '//h2')
|
||||
_person_panel_selector = (By.XPATH, '//div[contains(text(), "Person Details")]/..')
|
||||
_name_selector = (By.XPATH, '//dt[text()="Person"]/following-sibling::dd[1]')
|
||||
_email_selector = (By.XPATH, '//dt[text()="Email"]/following-sibling::dd[1]')
|
||||
_phone_selector = (By.XPATH, '//dt[text()="Phone Number"]/following-sibling::dd[1]')
|
||||
@@ -96,7 +96,7 @@ class CreateEvent(FormPage):
|
||||
_warning_selector = (By.XPATH, '/html/body/div[1]/div[1]')
|
||||
|
||||
form_items = {
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'description': (regions.SimpleMDETextArea, (By.ID, 'id_description')),
|
||||
|
||||
'name': (regions.TextBox, (By.ID, 'id_name')),
|
||||
'start_date': (regions.DatePicker, (By.ID, 'id_start_date')),
|
||||
@@ -110,7 +110,7 @@ class CreateEvent(FormPage):
|
||||
'collected_by': (regions.TextBox, (By.ID, 'id_collector')),
|
||||
'po': (regions.TextBox, (By.ID, 'id_purchase_order')),
|
||||
|
||||
'notes': (regions.TextBox, (By.ID, 'id_notes'))
|
||||
'notes': (regions.SimpleMDETextArea, (By.ID, 'id_notes'))
|
||||
}
|
||||
|
||||
def select_event_type(self, type_name):
|
||||
@@ -230,9 +230,11 @@ class CreateEventChecklist(FormPage):
|
||||
URL_TEMPLATE = 'event/{event_id}/checklist'
|
||||
|
||||
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
||||
_power_mic_selector = (By.XPATH, "//div[@id='id_power_mic-group']//div[contains(@class, 'bootstrap-select')]")
|
||||
_power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]")
|
||||
_add_vehicle_locator = (By.XPATH, "//button[contains(., 'Vehicle')]")
|
||||
_add_crew_locator = (By.XPATH, "//button[contains(., 'Crew')]")
|
||||
_vehicle_row_locator = ('xpath', "//tr[@id[starts-with(., 'vehicle') and not(contains(.,'new'))]]")
|
||||
_crew_row_locator = ('xpath', "//tr[@id[starts-with(., 'crew') and not(contains(.,'new'))]]")
|
||||
|
||||
form_items = {
|
||||
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
|
||||
@@ -271,11 +273,61 @@ class CreateEventChecklist(FormPage):
|
||||
def power_mic(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector))
|
||||
|
||||
@property
|
||||
def vehicles(self):
|
||||
return [self.VehicleRow(self, el) for el in self.find_elements(*self._vehicle_row_locator)]
|
||||
|
||||
class VehicleRow(Region):
|
||||
_name_locator = ('xpath', ".//input")
|
||||
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return regions.TextBox(self, self.root.find_element(*self._name_locator))
|
||||
|
||||
@property
|
||||
def vehicle(self):
|
||||
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
|
||||
|
||||
@property
|
||||
def crew(self):
|
||||
return [self.CrewRow(self, el) for el in self.find_elements(*self._crew_row_locator)]
|
||||
|
||||
class CrewRow(Region):
|
||||
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
|
||||
_start_time_locator = ('xpath', ".//input[@name[starts-with(., 'start') and not(contains(.,'new'))]]")
|
||||
_end_time_locator = ('xpath', ".//input[@name[starts-with(., 'end') and not(contains(.,'new'))]]")
|
||||
_role_locator = ('xpath', ".//input[@name[starts-with(., 'role') and not(contains(.,'new'))]]")
|
||||
|
||||
@property
|
||||
def crewmember(self):
|
||||
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
|
||||
|
||||
@property
|
||||
def start_time(self):
|
||||
return regions.DateTimePicker(self, self.root.find_element(*self._start_time_locator))
|
||||
|
||||
@property
|
||||
def end_time(self):
|
||||
return regions.DateTimePicker(self, self.root.find_element(*self._end_time_locator))
|
||||
|
||||
@property
|
||||
def role(self):
|
||||
return regions.TextBox(self, self.root.find_element(*self._role_locator))
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return '{event_id}' not in self.driver.current_url
|
||||
|
||||
|
||||
class EditEventChecklist(CreateEventChecklist):
|
||||
URL_TEMPLATE = '/event/checklist/{pk}/edit'
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return 'edit' not in self.driver.current_url
|
||||
|
||||
|
||||
class GenericList(BasePage):
|
||||
_search_selector = (By.CSS_SELECTOR, 'div.input-group:nth-child(2) > input:nth-child(1)')
|
||||
_search_go_selector = (By.ID, 'id_search')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pypom import Region
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from PyRIGS.tests.regions import TextBox, Modal
|
||||
from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
|
||||
|
||||
|
||||
class Header(Region):
|
||||
@@ -42,7 +42,7 @@ class ItemModal(Modal):
|
||||
|
||||
form_items = {
|
||||
'name': (TextBox, (By.ID, 'item_name')),
|
||||
'description': (TextBox, (By.ID, 'item_description')),
|
||||
'description': (SimpleMDETextArea, (By.ID, 'item_description')),
|
||||
'quantity': (TextBox, (By.ID, 'item_quantity')),
|
||||
'price': (TextBox, (By.ID, 'item_cost'))
|
||||
}
|
||||
|
||||
@@ -8,54 +8,12 @@ from django.http import HttpResponseBadRequest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
import PyRIGS.tests.base
|
||||
from RIGS import models
|
||||
from pytest_django.asserts import assertContains, assertNotContains
|
||||
|
||||
|
||||
class BaseCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
cls.profile = models.Profile.objects.get_or_create(
|
||||
first_name='Test',
|
||||
last_name='TEC User',
|
||||
username='eventauthtest',
|
||||
email='teccie@functional.test',
|
||||
is_superuser=True # lazily grant all permissions
|
||||
)[0]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
venue = models.Venue.objects.create(name='Authorisation Test Venue')
|
||||
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
|
||||
organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=True)
|
||||
self.event = models.Event.objects.create(
|
||||
name='Authorisation Test',
|
||||
start_date=date.today(),
|
||||
venue=venue,
|
||||
person=client,
|
||||
organisation=organisation,
|
||||
)
|
||||
|
||||
|
||||
class TestEventValidation(BaseCase):
|
||||
def test_create(self):
|
||||
url = reverse('event_create')
|
||||
# end time before start access after start
|
||||
response = self.client.post(url, {'start_date': datetime.date(2020, 1, 1), 'start_time': datetime.time(10, 00),
|
||||
'end_time': datetime.time(9, 00),
|
||||
'access_at': datetime.datetime(2020, 1, 5, 10)})
|
||||
self.assertFormError(response, 'form', 'end_time',
|
||||
"Unless you've invented time travel, the event can't finish before it has started.")
|
||||
self.assertFormError(response, 'form', 'access_at',
|
||||
"Regardless of what some clients might think, access time cannot be after the event has started.")
|
||||
from pytest_django.asserts import assertContains, assertNotContains, assertFormError
|
||||
|
||||
|
||||
def setup_event():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
venue = models.Venue.objects.create(name='Authorisation Test Venue')
|
||||
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
|
||||
organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=True)
|
||||
@@ -84,6 +42,18 @@ def setup_mail(event, profile):
|
||||
return auth_data, hmac, url
|
||||
|
||||
|
||||
def test_create(admin_client):
|
||||
url = reverse('event_create')
|
||||
# end time before start access after start
|
||||
response = admin_client.post(url, {'start_date': datetime.date(2020, 1, 1), 'start_time': datetime.time(10, 00),
|
||||
'end_time': datetime.time(9, 00),
|
||||
'access_at': datetime.datetime(2020, 1, 5, 10)})
|
||||
assertFormError(response, 'form', 'end_time',
|
||||
"Unless you've invented time travel, the event can't finish before it has started.")
|
||||
assertFormError(response, 'form', 'access_at',
|
||||
"Regardless of what some clients might think, access time cannot be after the event has started.")
|
||||
|
||||
|
||||
def test_requires_valid_hmac(client, admin_user):
|
||||
event = setup_event()
|
||||
auth_data, hmac, url = setup_mail(event, admin_user)
|
||||
@@ -138,7 +108,7 @@ def test_duplicate_warning(client, admin_user):
|
||||
assertContains(response, 'amount has changed')
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_email_sent(admin_client, admin_user, mailoutbox):
|
||||
event = setup_event()
|
||||
auth_data, hmac, url = setup_mail(event, admin_user)
|
||||
@@ -152,36 +122,36 @@ def test_email_sent(admin_client, admin_user, mailoutbox):
|
||||
assert mailoutbox[1].to == [settings.AUTHORISATION_NOTIFICATION_ADDRESS]
|
||||
|
||||
|
||||
class TECEventAuthorisationTest(BaseCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse('event_authorise_request', kwargs={'pk': self.event.pk})
|
||||
def test_email_check(admin_client, admin_user):
|
||||
event = setup_event()
|
||||
url = reverse('event_authorise_request', kwargs={'pk': event.pk})
|
||||
admin_user.email = 'teccie@someotherdomain.com'
|
||||
admin_user.save()
|
||||
|
||||
def test_email_check(self):
|
||||
self.profile.email = 'teccie@someotherdomain.com'
|
||||
self.profile.save()
|
||||
response = admin_client.post(url)
|
||||
|
||||
response = self.client.post(self.url)
|
||||
assertContains(response, 'must have an @nottinghamtec.co.uk email address')
|
||||
|
||||
self.assertContains(response, 'must have an @nottinghamtec.co.uk email address')
|
||||
|
||||
def test_request_send(self):
|
||||
self.profile.email = 'teccie@nottinghamtec.co.uk'
|
||||
self.profile.save()
|
||||
response = self.client.post(self.url)
|
||||
self.assertContains(response, 'This field is required.')
|
||||
def test_request_send(admin_client, admin_user):
|
||||
event = setup_event()
|
||||
url = reverse('event_authorise_request', kwargs={'pk': event.pk})
|
||||
admin_user.email = 'teccie@nottinghamtec.co.uk'
|
||||
admin_user.save()
|
||||
response = admin_client.post(url)
|
||||
assertContains(response, 'This field is required.')
|
||||
|
||||
mail.outbox = []
|
||||
mail.outbox = []
|
||||
|
||||
response = self.client.post(self.url, {'email': 'client@functional.test'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
email = mail.outbox[0]
|
||||
self.assertIn('client@functional.test', email.to)
|
||||
self.assertIn('/event/%d/' % (self.event.pk), email.body)
|
||||
response = admin_client.post(url, {'email': 'client@functional.test'})
|
||||
assert response.status_code == 302
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert 'client@functional.test' in email.to
|
||||
assert '/event/%d/' % event.pk in email.body
|
||||
|
||||
# Check sent by details are populated
|
||||
self.event.refresh_from_db()
|
||||
self.assertEqual(self.event.auth_request_by, self.profile)
|
||||
self.assertEqual(self.event.auth_request_to, 'client@functional.test')
|
||||
self.assertIsNotNone(self.event.auth_request_at)
|
||||
# Check sent by details are populated
|
||||
event.refresh_from_db()
|
||||
assert event.auth_request_by == admin_user
|
||||
assert event.auth_request_to == 'client@functional.test'
|
||||
assert event.auth_request_at is not None
|
||||
|
||||
@@ -15,6 +15,11 @@ from PyRIGS.tests.pages import animation_is_finished
|
||||
from RIGS import models
|
||||
from RIGS.tests import regions
|
||||
from . import pages
|
||||
import pytest
|
||||
import time as t
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db(transaction=True)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
@@ -307,13 +312,13 @@ class TestEventDuplicate(BaseRigboardTest):
|
||||
# TODO Rewrite when EventDetail page is implemented
|
||||
newEvent = models.Event.objects.latest('pk')
|
||||
|
||||
self.assertEqual(newEvent.auth_request_to, None)
|
||||
assert newEvent.auth_request_to == ''
|
||||
self.assertEqual(newEvent.auth_request_by, None)
|
||||
self.assertEqual(newEvent.auth_request_at, None)
|
||||
|
||||
self.assertFalse(newEvent.authorised)
|
||||
|
||||
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element_by_xpath('//h1').text)
|
||||
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element_by_xpath('//h2').text)
|
||||
self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible
|
||||
|
||||
# Check the new items are visible
|
||||
@@ -445,7 +450,7 @@ class TestEventDetail(BaseRigboardTest):
|
||||
self.assertIn("N%05d | %s" % (self.testEvent.pk, self.testEvent.name), self.page.event_name)
|
||||
self.assertEqual(self.client.name, self.page.name)
|
||||
self.assertEqual(self.client.email, self.page.email)
|
||||
self.assertEqual(self.client.phone, None)
|
||||
assert self.client.phone == ''
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
@@ -633,270 +638,190 @@ class TestCalendar(BaseRigboardTest):
|
||||
else:
|
||||
self.assertNotContains(response, "TE E" + str(test) + " ")
|
||||
|
||||
def test_calendar_buttons(self): # If FullCalendar fails to load for whatever reason, the buttons don't work
|
||||
self.page = pages.CalendarPage(self.driver, self.live_server_url).open()
|
||||
self.assertIn(timezone.now().strftime("%Y-%m"), self.driver.current_url)
|
||||
|
||||
target_date = datetime.date(2020, 1, 1)
|
||||
self.page.target_date.set_value(target_date)
|
||||
self.page.go()
|
||||
self.assertIn(self.page.target_date.value.strftime("%Y-%m"), self.driver.current_url)
|
||||
def test_calendar_buttons(logged_in_browser, live_server): # If FullCalendar fails to load for whatever reason, the buttons don't work
|
||||
page = pages.CalendarPage(logged_in_browser.driver, live_server.url).open()
|
||||
assert timezone.now().strftime("%Y-%m") in logged_in_browser.url
|
||||
|
||||
self.page.next()
|
||||
target_date += datetime.timedelta(days=32)
|
||||
self.assertIn(target_date.strftime("%m"), self.driver.current_url)
|
||||
target_date = datetime.date(2020, 1, 1)
|
||||
page.target_date.set_value(target_date)
|
||||
page.go()
|
||||
assert page.target_date.value.strftime("%Y-%m") in logged_in_browser.url
|
||||
|
||||
page.next()
|
||||
target_date += datetime.timedelta(days=32)
|
||||
assert target_date.strftime("%m") in logged_in_browser.url
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestHealthAndSafety(BaseRigboardTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.profile = models.Profile.objects.get_or_create(
|
||||
first_name='Test',
|
||||
last_name='TEC User',
|
||||
username='eventtest',
|
||||
email='teccie@functional.test',
|
||||
is_superuser=True # lazily grant all permissions
|
||||
)[0]
|
||||
self.venue = models.Venue.objects.create(name="Venue 1")
|
||||
def test_ra_edit(logged_in_browser, live_server, ra):
|
||||
page = pages.EditRiskAssessment(logged_in_browser.driver, live_server.url, pk=ra.pk).open()
|
||||
page.nonstandard_equipment = nse = True
|
||||
page.general_notes = gn = "There are some notes, but I've not written them here as that would be helpful"
|
||||
page.submit()
|
||||
assert not page.success
|
||||
page.supervisor_consulted = True
|
||||
page.submit()
|
||||
assert page.success
|
||||
# Check that data is right
|
||||
ra = models.RiskAssessment.objects.get(pk=ra.pk)
|
||||
assert ra.general_notes == gn
|
||||
assert ra.nonstandard_equipment == nse
|
||||
|
||||
self.testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
|
||||
start_date=date.today() + timedelta(days=6),
|
||||
description="start future no end",
|
||||
purchase_order='TESTPO',
|
||||
person=self.client,
|
||||
venue=self.venue)
|
||||
self.testEvent2 = models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL,
|
||||
start_date=date.today() + timedelta(days=6),
|
||||
description="start future no end",
|
||||
purchase_order='TESTPO',
|
||||
person=self.client,
|
||||
venue=self.venue)
|
||||
self.testEvent3 = models.Event.objects.create(name="TE E3", status=models.Event.PROVISIONAL,
|
||||
start_date=date.today() + timedelta(days=6),
|
||||
description="start future no end",
|
||||
purchase_order='TESTPO',
|
||||
person=self.client,
|
||||
venue=self.venue)
|
||||
self.testRA = models.RiskAssessment.objects.create(event=self.testEvent2, supervisor_consulted=False, nonstandard_equipment=False,
|
||||
nonstandard_use=False,
|
||||
contractors=False,
|
||||
other_companies=False,
|
||||
crew_fatigue=False,
|
||||
big_power=False,
|
||||
generators=False,
|
||||
other_companies_power=False,
|
||||
nonstandard_equipment_power=False,
|
||||
multiple_electrical_environments=False,
|
||||
noise_monitoring=False,
|
||||
known_venue=True,
|
||||
safe_loading=True,
|
||||
safe_storage=True,
|
||||
area_outside_of_control=False,
|
||||
barrier_required=False,
|
||||
nonstandard_emergency_procedure=False,
|
||||
special_structures=False,
|
||||
suspended_structures=False,
|
||||
outside=False)
|
||||
self.testRA2 = models.RiskAssessment.objects.create(event=self.testEvent3, supervisor_consulted=False, nonstandard_equipment=False,
|
||||
nonstandard_use=False,
|
||||
contractors=False,
|
||||
other_companies=False,
|
||||
crew_fatigue=False,
|
||||
big_power=True,
|
||||
generators=False,
|
||||
other_companies_power=False,
|
||||
nonstandard_equipment_power=False,
|
||||
multiple_electrical_environments=False,
|
||||
noise_monitoring=False,
|
||||
known_venue=True,
|
||||
safe_loading=True,
|
||||
safe_storage=True,
|
||||
area_outside_of_control=False,
|
||||
barrier_required=False,
|
||||
nonstandard_emergency_procedure=False,
|
||||
special_structures=False,
|
||||
suspended_structures=False,
|
||||
outside=False)
|
||||
self.page = pages.EventDetail(self.driver, self.live_server_url, event_id=self.testEvent.pk).open()
|
||||
|
||||
# TODO Can I loop through all the boolean fields and test them at once?
|
||||
def test_ra_creation(self):
|
||||
self.page = pages.CreateRiskAssessment(self.driver, self.live_server_url, event_id=self.testEvent.pk).open()
|
||||
def small_ec(page, admin_user):
|
||||
page.safe_parking = True
|
||||
page.safe_packing = True
|
||||
page.exits = True
|
||||
page.trip_hazard = True
|
||||
page.warning_signs = True
|
||||
page.ear_plugs = True
|
||||
page.hs_location = "The Moon"
|
||||
page.extinguishers_location = "With the rest of the fire"
|
||||
# If we do this first the search fails, for ... reasons
|
||||
page.power_mic.search(admin_user.name)
|
||||
page.power_mic.toggle()
|
||||
assert not page.power_mic.is_open
|
||||
page.earthing = True
|
||||
page.rcds = True
|
||||
page.supply_test = True
|
||||
page.pat = True
|
||||
|
||||
# Check there are no defaults
|
||||
self.assertIsNone(self.page.nonstandard_equipment)
|
||||
|
||||
# No database side validation, only HTML5.
|
||||
def test_ec_create_small(logged_in_browser, live_server, admin_user, ra):
|
||||
page = pages.CreateEventChecklist(logged_in_browser.driver, live_server.url, event_id=ra.event.pk).open()
|
||||
small_ec(page, admin_user)
|
||||
page.submit()
|
||||
assert page.success
|
||||
|
||||
self.page.nonstandard_equipment = False
|
||||
self.page.nonstandard_use = False
|
||||
self.page.contractors = False
|
||||
self.page.other_companies = False
|
||||
self.page.crew_fatigue = False
|
||||
self.page.general_notes = "There are no notes."
|
||||
self.page.big_power = False
|
||||
self.page.outside = False
|
||||
self.page.power_mic.search(self.profile.name)
|
||||
self.page.power_mic.set_option(self.profile.name, True)
|
||||
# TODO This should not be necessary, normally closes automatically
|
||||
self.page.power_mic.toggle()
|
||||
self.assertFalse(self.page.power_mic.is_open)
|
||||
self.page.generators = False
|
||||
self.page.other_companies_power = False
|
||||
self.page.nonstandard_equipment_power = False
|
||||
self.page.multiple_electrical_environments = False
|
||||
self.page.power_notes = "Remember to bring some power"
|
||||
self.page.noise_monitoring = False
|
||||
self.page.sound_notes = "Loud, but not too loud"
|
||||
self.page.known_venue = False
|
||||
self.page.safe_loading = False
|
||||
self.page.safe_storage = False
|
||||
self.page.area_outside_of_control = False
|
||||
self.page.barrier_required = False
|
||||
self.page.nonstandard_emergency_procedure = False
|
||||
self.page.special_structures = False
|
||||
# self.page.persons_responsible_structures = "Nobody and her cat, She"
|
||||
|
||||
self.page.suspended_structures = True
|
||||
# TODO Test for this proper
|
||||
self.page.rigging_plan = "https://nottinghamtec.sharepoint.com/test/"
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra):
|
||||
page = pages.CreateEventChecklist(logged_in_browser.driver, live_server.url, event_id=medium_ra.event.pk).open()
|
||||
|
||||
self.page.suspended_structures = False
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
page.safe_parking = True
|
||||
page.safe_packing = True
|
||||
page.exits = True
|
||||
page.trip_hazard = True
|
||||
page.warning_signs = True
|
||||
page.ear_plugs = True
|
||||
page.hs_location = "Death Valley"
|
||||
page.extinguishers_location = "With the rest of the fire"
|
||||
# If we do this first the search fails, for ... reasons
|
||||
page.power_mic.search(admin_user.name)
|
||||
page.power_mic.toggle()
|
||||
assert not page.power_mic.is_open
|
||||
|
||||
# Test that we can't make another one
|
||||
self.page = pages.CreateRiskAssessment(self.driver, self.live_server_url, event_id=self.testEvent.pk).open()
|
||||
self.assertIn('edit', self.driver.current_url)
|
||||
# Gotta scroll to make the button clickable
|
||||
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||
|
||||
def test_ra_edit(self):
|
||||
self.page = pages.EditRiskAssessment(self.driver, self.live_server_url, pk=self.testRA.pk).open()
|
||||
self.page.nonstandard_equipment = nse = True
|
||||
self.page.general_notes = gn = "There are some notes, but I've not written them here as that would be helpful"
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.page.supervisor_consulted = True
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
# Check that data is right
|
||||
ra = models.RiskAssessment.objects.get(pk=self.testRA.pk)
|
||||
self.assertEqual(ra.general_notes, gn)
|
||||
self.assertEqual(ra.nonstandard_equipment, nse)
|
||||
page.earthing = True
|
||||
page.pat = True
|
||||
page.source_rcd = True
|
||||
page.labelling = True
|
||||
page.fd_voltage_l1 = 240
|
||||
page.fd_voltage_l2 = 235
|
||||
page.fd_voltage_l3 = 0
|
||||
page.fd_phase_rotation = True
|
||||
page.fd_earth_fault = "1.21"
|
||||
page.fd_pssc = 1984
|
||||
page.w1_description = "In the carpark, by the bins"
|
||||
page.w1_polarity = True
|
||||
page.w1_voltage = 240
|
||||
page.w1_earth_fault = "0.42"
|
||||
|
||||
def test_ec_create_small(self):
|
||||
self.page = pages.CreateEventChecklist(self.driver, self.live_server_url, event_id=self.testEvent2.pk).open()
|
||||
page.submit()
|
||||
assert page.success
|
||||
|
||||
self.page.safe_parking = True
|
||||
self.page.safe_packing = True
|
||||
self.page.exits = True
|
||||
self.page.trip_hazard = True
|
||||
self.page.warning_signs = True
|
||||
self.page.ear_plugs = True
|
||||
self.page.hs_location = "The Moon"
|
||||
self.page.extinguishers_location = "With the rest of the fire"
|
||||
# If we do this first the search fails, for ... reasons
|
||||
self.page.power_mic.search(self.profile.name)
|
||||
self.page.power_mic.toggle()
|
||||
self.assertFalse(self.page.power_mic.is_open)
|
||||
|
||||
# Gotta scroll to make the button clickable
|
||||
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||
def test_ec_create_vehicle(logged_in_browser, live_server, admin_user, checklist):
|
||||
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
|
||||
small_ec(page, admin_user)
|
||||
page.add_vehicle()
|
||||
assert len(page.vehicles) == 1
|
||||
vehicle_name = 'Brian'
|
||||
page.vehicles[0].name.set_value(vehicle_name)
|
||||
# Appears we're moving too fast for javascript...
|
||||
t.sleep(1)
|
||||
page.vehicles[0].vehicle.search(admin_user.first_name)
|
||||
t.sleep(1)
|
||||
page.submit()
|
||||
assert page.success
|
||||
# Check data is correct
|
||||
checklist.refresh_from_db()
|
||||
vehicle = models.EventChecklistVehicle.objects.get(checklist=checklist.pk)
|
||||
assert vehicle_name == vehicle.vehicle
|
||||
|
||||
self.page.earthing = True
|
||||
self.page.rcds = True
|
||||
self.page.supply_test = True
|
||||
self.page.pat = True
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
# TODO Test validation of end before start
|
||||
def test_ec_create_crew(logged_in_browser, live_server, admin_user, checklist):
|
||||
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
|
||||
small_ec(page, admin_user)
|
||||
page.add_crew()
|
||||
assert len(page.crew) == 1
|
||||
role = "MIC"
|
||||
start_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 9, 0))
|
||||
end_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 10, 30))
|
||||
crew = page.crew[0]
|
||||
t.sleep(2)
|
||||
crew.crewmember.search(admin_user.first_name)
|
||||
t.sleep(2)
|
||||
crew.role.set_value(role)
|
||||
crew.start_time.set_value(start_time)
|
||||
crew.end_time.set_value(end_time)
|
||||
page.submit()
|
||||
assert page.success
|
||||
# Check data is correct
|
||||
crew_obj = models.EventChecklistCrew.objects.get(checklist=checklist.pk)
|
||||
assert admin_user.pk == crew_obj.crewmember.pk
|
||||
assert role == crew_obj.role
|
||||
assert start_time == crew_obj.start
|
||||
assert end_time == crew_obj.end
|
||||
|
||||
def test_ec_create_medium(self):
|
||||
self.page = pages.CreateEventChecklist(self.driver, self.live_server_url, event_id=self.testEvent3.pk).open()
|
||||
|
||||
self.page.safe_parking = True
|
||||
self.page.safe_packing = True
|
||||
self.page.exits = True
|
||||
self.page.trip_hazard = True
|
||||
self.page.warning_signs = True
|
||||
self.page.ear_plugs = True
|
||||
self.page.hs_location = "Death Valley"
|
||||
self.page.extinguishers_location = "With the rest of the fire"
|
||||
# If we do this first the search fails, for ... reasons
|
||||
self.page.power_mic.search(self.profile.name)
|
||||
self.page.power_mic.toggle()
|
||||
self.assertFalse(self.page.power_mic.is_open)
|
||||
# TODO Can I loop through all the boolean fields and test them at once?
|
||||
def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event):
|
||||
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open()
|
||||
|
||||
# Gotta scroll to make the button clickable
|
||||
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||
# Check there are no defaults
|
||||
assert page.nonstandard_equipment is None
|
||||
|
||||
self.page.earthing = True
|
||||
self.page.pat = True
|
||||
self.page.source_rcd = True
|
||||
self.page.labelling = True
|
||||
self.page.fd_voltage_l1 = 240
|
||||
self.page.fd_voltage_l2 = 235
|
||||
self.page.fd_voltage_l3 = 0
|
||||
self.page.fd_phase_rotation = True
|
||||
self.page.fd_earth_fault = 666
|
||||
self.page.fd_pssc = 1984
|
||||
self.page.w1_description = "In the carpark, by the bins"
|
||||
self.page.w1_polarity = True
|
||||
self.page.w1_voltage = 240
|
||||
self.page.w1_earth_fault = 333
|
||||
# No database side validation, only HTML5.
|
||||
page.nonstandard_equipment = False
|
||||
page.nonstandard_use = False
|
||||
page.contractors = False
|
||||
page.other_companies = False
|
||||
page.crew_fatigue = False
|
||||
page.general_notes = "There are no notes."
|
||||
page.big_power = False
|
||||
page.outside = False
|
||||
page.power_mic.search(admin_user.first_name)
|
||||
page.generators = False
|
||||
page.other_companies_power = False
|
||||
page.nonstandard_equipment_power = False
|
||||
page.multiple_electrical_environments = False
|
||||
page.power_notes = "Remember to bring some power"
|
||||
page.noise_monitoring = False
|
||||
page.sound_notes = "Loud, but not too loud"
|
||||
page.known_venue = False
|
||||
page.safe_loading = False
|
||||
page.safe_storage = False
|
||||
page.area_outside_of_control = False
|
||||
page.barrier_required = False
|
||||
page.nonstandard_emergency_procedure = False
|
||||
page.special_structures = False
|
||||
# self.page.persons_responsible_structures = "Nobody and her cat, She"
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
page.suspended_structures = True
|
||||
# TODO Test for this proper
|
||||
page.rigging_plan = "https://nottinghamtec.sharepoint.com/test/"
|
||||
page.submit()
|
||||
assert not page.success
|
||||
|
||||
def test_ec_create_extras(self):
|
||||
eid = self.testEvent2.pk
|
||||
self.page = pages.CreateEventChecklist(self.driver, self.live_server_url, event_id=eid).open()
|
||||
self.page.add_vehicle()
|
||||
self.page.add_crew()
|
||||
page.suspended_structures = False
|
||||
page.submit()
|
||||
assert page.success
|
||||
|
||||
self.page.safe_parking = True
|
||||
self.page.safe_packing = True
|
||||
self.page.exits = True
|
||||
self.page.trip_hazard = True
|
||||
self.page.warning_signs = True
|
||||
self.page.ear_plugs = True
|
||||
self.page.hs_location = "The Moon"
|
||||
self.page.extinguishers_location = "With the rest of the fire"
|
||||
# If we do this first the search fails, for ... reasons
|
||||
self.page.power_mic.search(self.profile.name)
|
||||
self.page.power_mic.toggle()
|
||||
self.assertFalse(self.page.power_mic.is_open)
|
||||
|
||||
vehicle_name = 'Brian'
|
||||
self.driver.find_element(By.XPATH, '//*[@name="vehicle_-1"]').send_keys(vehicle_name)
|
||||
driver = base_regions.BootstrapSelectElement(self.page, self.driver.find_element(By.XPATH, '//tr[@id="vehicles_-1"]//div[contains(@class, "bootstrap-select")]'))
|
||||
driver.search(self.profile.name)
|
||||
|
||||
crew = self.profile
|
||||
role = "MIC"
|
||||
crew_select = base_regions.BootstrapSelectElement(self.page, self.driver.find_element(By.XPATH, '//tr[@id="crew_-1"]//div[contains(@class, "bootstrap-select")]'))
|
||||
start_time = base_regions.DateTimePicker(self.page, self.driver.find_element(By.XPATH, '//*[@name="start_-1"]'))
|
||||
end_time = base_regions.DateTimePicker(self.page, self.driver.find_element(By.XPATH, '//*[@name="end_-1"]'))
|
||||
|
||||
start_time.set_value(timezone.make_aware(datetime.datetime(2015, 1, 1, 9, 0)))
|
||||
# TODO Test validation of end before start
|
||||
end_time.set_value(timezone.make_aware(datetime.datetime(2015, 1, 1, 10, 30)))
|
||||
crew_select.search(crew.name)
|
||||
self.driver.find_element(By.XPATH, '//*[@name="role_-1"]').send_keys(role)
|
||||
|
||||
self.page.earthing = True
|
||||
self.page.rcds = True
|
||||
self.page.supply_test = True
|
||||
self.page.pat = True
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
checklist = models.EventChecklist.objects.get(event=eid)
|
||||
vehicle = models.EventChecklistVehicle.objects.get(checklist=checklist.pk)
|
||||
self.assertEqual(vehicle_name, vehicle.vehicle)
|
||||
crew_obj = models.EventChecklistCrew.objects.get(checklist=checklist.pk)
|
||||
self.assertEqual(crew.pk, crew_obj.crewmember.pk)
|
||||
self.assertEqual(role, crew_obj.role)
|
||||
def test_ra_no_duplicates(logged_in_browser, live_server, ra):
|
||||
# Test that we can't make another one
|
||||
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=ra.event.pk).open()
|
||||
assert 'edit' in logged_in_browser.url
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import date, timedelta, datetime, time
|
||||
from decimal import *
|
||||
|
||||
import pytz
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from reversion import revisions as reversion
|
||||
@@ -9,110 +10,56 @@ from reversion import revisions as reversion
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class ProfileTestCase(TestCase):
|
||||
def test_str(self):
|
||||
profile = models.Profile(first_name='Test', last_name='Case')
|
||||
self.assertEqual(str(profile), 'Test Case')
|
||||
profile.initials = 'TC'
|
||||
self.assertEqual(str(profile), 'Test Case "TC"')
|
||||
def assert_decimal_equality(d1, d2):
|
||||
assert float(d1) == pytest.approx(float(d2))
|
||||
|
||||
|
||||
class VatRateTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.rates = {
|
||||
0: models.VatRate.objects.create(start_at='2014-03-01', rate=0.20, comment='test1'),
|
||||
1: models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2'),
|
||||
}
|
||||
|
||||
def test_find_correct(self):
|
||||
r = models.VatRate.objects.find_rate('2015-03-01')
|
||||
self.assertEqual(r, self.rates[0])
|
||||
r = models.VatRate.objects.find_rate('2016-03-01')
|
||||
self.assertEqual(r, self.rates[1])
|
||||
|
||||
def test_percent_correct(self):
|
||||
self.assertEqual(self.rates[0].as_percent, 20)
|
||||
def test_str():
|
||||
profile = models.Profile(first_name='Test', last_name='Case')
|
||||
assert str(profile) == 'Test Case'
|
||||
profile.initials = 'TC'
|
||||
assert str(profile) == 'Test Case "TC"'
|
||||
|
||||
|
||||
class EventTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.all_events = set(range(1, 18))
|
||||
cls.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
|
||||
cls.not_current_events = set(cls.all_events) - set(cls.current_events)
|
||||
@pytest.mark.django_db
|
||||
def test_find_correct(vat_rate):
|
||||
new_rate = models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2')
|
||||
r = models.VatRate.objects.find_rate('2015-03-01')
|
||||
assert_decimal_equality(r.rate, vat_rate.rate)
|
||||
r = models.VatRate.objects.find_rate('2016-03-01')
|
||||
assert_decimal_equality(r.rate, new_rate.rate)
|
||||
|
||||
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com")
|
||||
|
||||
cls.events = {
|
||||
# produce 7 normal events - 5 current
|
||||
1: models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6),
|
||||
description="start future no end"),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end"),
|
||||
3: models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(),
|
||||
description="start today with end today"),
|
||||
4: models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end"),
|
||||
5: models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21',
|
||||
description="start past with end past"),
|
||||
6: models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2),
|
||||
end_date=date.today() + timedelta(days=2),
|
||||
description="start past, end future"),
|
||||
7: models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2),
|
||||
end_date=date.today() + timedelta(days=2),
|
||||
description="start + end in future"),
|
||||
def test_percent_correct(vat_rate):
|
||||
assert vat_rate.as_percent == 20
|
||||
|
||||
# 2 cancelled - 1 current
|
||||
8: models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
|
||||
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
|
||||
description="cancelled in future"),
|
||||
9: models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
|
||||
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
|
||||
description="cancelled and started"),
|
||||
|
||||
# 5 dry hire - 3 current
|
||||
10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True,
|
||||
description="dryhire today"),
|
||||
11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True,
|
||||
checked_in_by=cls.profile,
|
||||
description="dryhire today, checked in"),
|
||||
12: models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
|
||||
status=models.Event.BOOKED, description="dryhire past"),
|
||||
13: models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
|
||||
checked_in_by=cls.profile, description="dryhire past checked in"),
|
||||
14: models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
|
||||
status=models.Event.CANCELLED, description="dryhire today cancelled"),
|
||||
def test_related_vatrate(basic_event, vat_rate):
|
||||
assert_decimal_equality(vat_rate.rate, basic_event.vat_rate.rate)
|
||||
|
||||
# 4 non rig - 3 current
|
||||
15: models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False,
|
||||
description="non rig today"),
|
||||
16: models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
|
||||
description="non rig tomorrow"),
|
||||
17: models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
|
||||
description="non rig yesterday"),
|
||||
18: models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False,
|
||||
status=models.Event.CANCELLED,
|
||||
description="non rig today cancelled"),
|
||||
}
|
||||
|
||||
def test_count(self):
|
||||
# Santiy check we have the expected events created
|
||||
self.assertEqual(models.Event.objects.count(), 18, "Incorrect number of events, check setup")
|
||||
class EventTest():
|
||||
def test_count(many_events):
|
||||
# Sanity check we have the expected events created
|
||||
assert models.Event.objects.count() == 18
|
||||
|
||||
def test_rig_count(self):
|
||||
def test_rig_count(many_events):
|
||||
# Changed to not include unreturned dry hires in rig count
|
||||
self.assertEqual(models.Event.objects.rig_count(), 7)
|
||||
assert models.Event.objects.rig_count() == 7
|
||||
|
||||
def test_current_events(self):
|
||||
def test_current_events(many_events):
|
||||
all_events = set(range(1, 18))
|
||||
current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
|
||||
not_current_events = set(cls.all_events) - set(cls.current_events)
|
||||
current_events = models.Event.objects.current_events()
|
||||
self.assertEqual(len(current_events), len(self.current_events))
|
||||
for eid in self.current_events:
|
||||
self.assertIn(models.Event.objects.get(name="TE E%d" % eid), current_events)
|
||||
assert len(current_events) == len(self.current_events)
|
||||
for eid in current_events:
|
||||
assert models.Event.objects.get(name="TE E%d" % eid) in current_events
|
||||
|
||||
for eid in self.not_current_events:
|
||||
self.assertNotIn(models.Event.objects.get(name="TE E%d" % eid), current_events)
|
||||
for eid in not_current_events:
|
||||
assert models.Event.objects.get(name="TE E%d" % eid) not in current_events
|
||||
|
||||
def test_related_venue(self):
|
||||
def test_related(many_events):
|
||||
v1 = models.Venue.objects.create(name="TE V1")
|
||||
v2 = models.Venue.objects.create(name="TE V2")
|
||||
|
||||
@@ -127,16 +74,13 @@ class EventTestCase(TestCase):
|
||||
e2.append(event)
|
||||
event.save()
|
||||
|
||||
self.assertCountEqual(e1, v1.latest_events)
|
||||
self.assertCountEqual(e2, v2.latest_events)
|
||||
assert set(e1) == set(v1.latest_events)
|
||||
assert set(e2) == set(v2.latest_events)
|
||||
# Cleanup
|
||||
v1.delete()
|
||||
v2.delete()
|
||||
|
||||
for (key, event) in self.events.items():
|
||||
event.venue = None
|
||||
|
||||
def test_related_vatrate(self):
|
||||
self.assertEqual(self.vatrate, models.Event.objects.all()[0].vat_rate)
|
||||
|
||||
def test_related_person(self):
|
||||
def test_related_person(many_events):
|
||||
p1 = models.Person.objects.create(name="TE P1")
|
||||
p2 = models.Person.objects.create(name="TE P2")
|
||||
|
||||
@@ -151,13 +95,13 @@ class EventTestCase(TestCase):
|
||||
e2.append(event)
|
||||
event.save()
|
||||
|
||||
self.assertCountEqual(e1, p1.latest_events)
|
||||
self.assertCountEqual(e2, p2.latest_events)
|
||||
assert set(e1) == set(p1.latest_events)
|
||||
assert set(e2) == set(p2.latest_events)
|
||||
|
||||
for (key, event) in self.events.items():
|
||||
event.person = None
|
||||
p1.delete()
|
||||
p2.delete()
|
||||
|
||||
def test_related_organisation(self):
|
||||
def test_related_organisation(many_events):
|
||||
o1 = models.Organisation.objects.create(name="TE O1")
|
||||
o2 = models.Organisation.objects.create(name="TE O2")
|
||||
|
||||
@@ -172,13 +116,13 @@ class EventTestCase(TestCase):
|
||||
e2.append(event)
|
||||
event.save()
|
||||
|
||||
self.assertCountEqual(e1, o1.latest_events)
|
||||
self.assertCountEqual(e2, o2.latest_events)
|
||||
assert set(e1) == set(o1.latest_events)
|
||||
assert set(e1) == set(o2.latest_events)
|
||||
|
||||
for (key, event) in self.events.items():
|
||||
event.organisation = None
|
||||
|
||||
def test_organisation_person_join(self):
|
||||
def test_organisation_person_join(many_events):
|
||||
p1 = models.Person.objects.create(name="TE P1")
|
||||
p2 = models.Person.objects.create(name="TE P2")
|
||||
o1 = models.Organisation.objects.create(name="TE O1")
|
||||
@@ -202,105 +146,109 @@ class EventTestCase(TestCase):
|
||||
events = models.Event.objects.all()
|
||||
|
||||
# Check person's organisations
|
||||
self.assertIn((o1, 2), p1.organisations)
|
||||
self.assertIn((o2, 1), p1.organisations)
|
||||
self.assertIn((o1, 2), p2.organisations)
|
||||
self.assertEqual(len(p2.organisations), 1)
|
||||
assert (o1, 2) in p1.organisations
|
||||
assert (o2, 1) in p1.organisations
|
||||
assert (o1, 2) in p2.organisations
|
||||
assert len(p2.organisations) == 1
|
||||
|
||||
# Check organisation's persons
|
||||
self.assertIn((p1, 2), o1.persons)
|
||||
self.assertIn((p2, 2), o1.persons)
|
||||
self.assertIn((p1, 1), o2.persons)
|
||||
self.assertEqual(len(o2.persons), 1)
|
||||
assert (p1, 2) in o1.persons
|
||||
assert (p2, 2) in o1.persons
|
||||
assert (p1, 1) in o2.persons
|
||||
assert len(o2.persons) == 1
|
||||
|
||||
def test_cancelled_property(self):
|
||||
edit = self.events[1]
|
||||
def test_cancelled_property(many_events):
|
||||
edit = many_events[1]
|
||||
edit.status = models.Event.CANCELLED
|
||||
edit.save()
|
||||
event = models.Event.objects.get(pk=edit.pk)
|
||||
self.assertEqual(event.status, models.Event.CANCELLED)
|
||||
self.assertTrue(event.cancelled)
|
||||
assert event.status == models.Event.CANCELLED
|
||||
assert event.cancelled
|
||||
event.status = models.Event.PROVISIONAL
|
||||
event.save()
|
||||
|
||||
def test_confirmed_property(self):
|
||||
edit = self.events[1]
|
||||
def test_confirmed_property(many_events):
|
||||
edit = many_events[1]
|
||||
edit.status = models.Event.CONFIRMED
|
||||
edit.save()
|
||||
event = models.Event.objects.get(pk=edit.pk)
|
||||
self.assertEqual(event.status, models.Event.CONFIRMED)
|
||||
self.assertTrue(event.confirmed)
|
||||
assert event.status == models.Event.CONFIRMED
|
||||
assert event.confirmed
|
||||
event.status = models.Event.PROVISIONAL
|
||||
event.save()
|
||||
|
||||
def test_earliest_time(self):
|
||||
event = models.Event(name="TE ET", start_date=date(2016, 0o1, 0o1))
|
||||
|
||||
# Just a start date
|
||||
self.assertEqual(event.earliest_time, date(2016, 0o1, 0o1))
|
||||
def test_earliest_time():
|
||||
event = models.Event(name="TE ET", start_date=date(2016, 0o1, 0o1))
|
||||
|
||||
# With start time
|
||||
event.start_time = time(9, 00)
|
||||
self.assertEqual(event.earliest_time, self.create_datetime(2016, 1, 1, 9, 00))
|
||||
# Just a start date
|
||||
assert event.earliest_time == date(2016, 0o1, 0o1)
|
||||
|
||||
# With access time
|
||||
event.access_at = self.create_datetime(2015, 12, 0o3, 9, 57)
|
||||
self.assertEqual(event.earliest_time, event.access_at)
|
||||
# With start time
|
||||
event.start_time = time(9, 00)
|
||||
assert event.earliest_time == create_datetime(2016, 1, 1, 9, 00)
|
||||
|
||||
# With meet time
|
||||
event.meet_at = self.create_datetime(2015, 12, 0o3, 9, 55)
|
||||
self.assertEqual(event.earliest_time, event.meet_at)
|
||||
# With access time
|
||||
event.access_at = create_datetime(2015, 12, 0o3, 9, 57)
|
||||
assert event.earliest_time == event.access_at
|
||||
|
||||
# Check order isn't important
|
||||
event.start_date = date(2015, 12, 0o3)
|
||||
self.assertEqual(event.earliest_time, self.create_datetime(2015, 12, 0o3, 9, 00))
|
||||
# With meet time
|
||||
event.meet_at = create_datetime(2015, 12, 0o3, 9, 55)
|
||||
assert event.earliest_time == event.meet_at
|
||||
|
||||
def test_latest_time(self):
|
||||
event = models.Event(name="TE LT", start_date=date(2016, 0o1, 0o1))
|
||||
# Check order isn't important
|
||||
event.start_date = date(2015, 12, 0o3)
|
||||
assert event.earliest_time == create_datetime(2015, 12, 0o3, 9, 00)
|
||||
|
||||
# Just start date
|
||||
self.assertEqual(event.latest_time, event.start_date)
|
||||
|
||||
# Just end date
|
||||
event.end_date = date(2016, 1, 2)
|
||||
self.assertEqual(event.latest_time, event.end_date)
|
||||
def test_latest_time():
|
||||
event = models.Event(name="TE LT", start_date=date(2016, 0o1, 0o1))
|
||||
|
||||
# With end time
|
||||
event.end_time = time(23, 00)
|
||||
self.assertEqual(event.latest_time, self.create_datetime(2016, 1, 2, 23, 00))
|
||||
# Just start date
|
||||
assert event.latest_time == event.start_date
|
||||
|
||||
def test_in_bounds(self):
|
||||
manager = models.Event.objects
|
||||
events = [
|
||||
manager.create(name="TE IB0", start_date='2016-01-02'), # yes no
|
||||
manager.create(name="TE IB1", start_date='2015-12-31', end_date='2016-01-04'),
|
||||
# Just end date
|
||||
event.end_date = date(2016, 1, 2)
|
||||
assert event.latest_time == event.end_date
|
||||
|
||||
# basic checks
|
||||
manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'),
|
||||
manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'),
|
||||
manager.create(name='TE IB4', start_date='2016-01-04',
|
||||
access_at=self.create_datetime(2016, 0o1, 0o3, 00, 00)),
|
||||
manager.create(name='TE IB5', start_date='2016-01-04',
|
||||
meet_at=self.create_datetime(2016, 0o1, 0o2, 00, 00)),
|
||||
# With end time
|
||||
event.end_time = time(23, 00)
|
||||
assert event.latest_time == create_datetime(2016, 1, 2, 23, 00)
|
||||
|
||||
# negative check
|
||||
manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'),
|
||||
]
|
||||
|
||||
in_bounds = manager.events_in_bounds(self.create_datetime(2016, 1, 2, 0, 0),
|
||||
self.create_datetime(2016, 1, 3, 0, 0))
|
||||
self.assertIn(events[0], in_bounds)
|
||||
self.assertIn(events[1], in_bounds)
|
||||
self.assertIn(events[2], in_bounds)
|
||||
self.assertIn(events[3], in_bounds)
|
||||
self.assertIn(events[4], in_bounds)
|
||||
self.assertIn(events[5], in_bounds)
|
||||
def test_in_bounds():
|
||||
manager = models.Event.objects
|
||||
events = [
|
||||
manager.create(name="TE IB0", start_date='2016-01-02'), # yes no
|
||||
manager.create(name="TE IB1", start_date='2015-12-31', end_date='2016-01-04'),
|
||||
|
||||
self.assertNotIn(events[6], in_bounds)
|
||||
# basic checks
|
||||
manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'),
|
||||
manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'),
|
||||
manager.create(name='TE IB4', start_date='2016-01-04',
|
||||
access_at=create_datetime(2016, 0o1, 0o3, 00, 00)),
|
||||
manager.create(name='TE IB5', start_date='2016-01-04',
|
||||
meet_at=create_datetime(2016, 0o1, 0o2, 00, 00)),
|
||||
|
||||
def create_datetime(self, year, month, day, hour, min):
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
return tz.localize(datetime(year, month, day, hour, min))
|
||||
# negative check
|
||||
manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'),
|
||||
]
|
||||
|
||||
in_bounds = manager.events_in_bounds(create_datetime(2016, 1, 2, 0, 0),
|
||||
create_datetime(2016, 1, 3, 0, 0))
|
||||
assert events[0] in in_bounds
|
||||
assert events[1], in_bounds
|
||||
assert events[2], in_bounds
|
||||
assert events[3], in_bounds
|
||||
assert events[4], in_bounds
|
||||
assert events[5], in_bounds
|
||||
|
||||
assert events[6] not in in_bounds
|
||||
|
||||
|
||||
def create_datetime(year, month, day, hour, minute):
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
return tz.localize(datetime(year, month, day, hour, minute))
|
||||
|
||||
|
||||
class EventItemTestCase(TestCase):
|
||||
@@ -331,7 +279,6 @@ class EventItemTestCase(TestCase):
|
||||
|
||||
class EventPricingTestCase(TestCase):
|
||||
def setUp(self):
|
||||
models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
|
||||
models.VatRate.objects.create(rate=0.10, comment="TP V2", start_at=date.today() - timedelta(days=1))
|
||||
self.e1 = models.Event.objects.create(name="TP E1", start_date=date.today() - timedelta(days=2))
|
||||
self.e2 = models.Event.objects.create(name="TP E2", start_date=date.today())
|
||||
@@ -364,7 +311,6 @@ class EventPricingTestCase(TestCase):
|
||||
|
||||
class EventAuthorisationTestCase(TestCase):
|
||||
def setUp(self):
|
||||
models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
|
||||
self.profile = models.Profile.objects.get_or_create(
|
||||
first_name='Test',
|
||||
last_name='TEC User',
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
from datetime import date
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.safestring import SafeText
|
||||
from RIGS.templatetags.markdown_tags import markdown_filter
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
|
||||
|
||||
from PyRIGS.tests.base import assert_times_equal
|
||||
from PyRIGS.tests.base import assert_times_almost_equal, assert_oembed, login
|
||||
from RIGS import models
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
class TestAdminMergeObjects(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="Person 1"),
|
||||
2: models.Person.objects.create(name="Person 2"),
|
||||
@@ -168,9 +172,7 @@ class TestInvoiceDelete(TestCase):
|
||||
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.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
|
||||
cls.events = {
|
||||
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today())
|
||||
@@ -201,7 +203,7 @@ class TestInvoiceDelete(TestCase):
|
||||
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[2].pk))
|
||||
|
||||
# Actually delete it
|
||||
response = self.client.post(request_url, follow=True)
|
||||
self.client.post(request_url, follow=True)
|
||||
|
||||
# Check the invoice is deleted
|
||||
self.assertRaises(ObjectDoesNotExist, models.Invoice.objects.get, pk=self.invoices[2].pk)
|
||||
@@ -216,7 +218,7 @@ class TestInvoiceDelete(TestCase):
|
||||
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
|
||||
|
||||
# Try to actually delete it
|
||||
response = self.client.post(request_url, follow=True)
|
||||
self.client.post(request_url, follow=True)
|
||||
|
||||
# Check this didn't work
|
||||
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
|
||||
@@ -227,9 +229,6 @@ class TestPrintPaperwork(TestCase):
|
||||
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.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
|
||||
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."),
|
||||
@@ -257,102 +256,50 @@ class TestPrintPaperwork(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestEmbeddedViews(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)
|
||||
def test_login_redirect(client, django_user_model):
|
||||
request_url = reverse('event_embed', kwargs={'pk': 1})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
|
||||
cls.events = {
|
||||
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today())
|
||||
}
|
||||
# Request the page and check it redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
cls.invoices = {
|
||||
1: models.Invoice.objects.create(event=cls.events[1]),
|
||||
2: models.Invoice.objects.create(event=cls.events[2])
|
||||
}
|
||||
# Now login
|
||||
login(client, django_user_model)
|
||||
|
||||
cls.payments = {
|
||||
1: models.Payment.objects.create(invoice=cls.invoices[1], date=date.today(), amount=12.34,
|
||||
method=models.Payment.CASH)
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
|
||||
def testLoginRedirect(self):
|
||||
request_url = reverse('event_embed', kwargs={'pk': 1})
|
||||
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):
|
||||
event_url = reverse('event_embed', kwargs={'pk': 1})
|
||||
login_url = reverse('login_embed')
|
||||
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
response = self.client.get(event_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):
|
||||
event_url = reverse('event_detail', kwargs={'pk': 1})
|
||||
event_embed_url = reverse('event_embed', kwargs={'pk': 1})
|
||||
oembed_url = reverse('event_oembed', kwargs={'pk': 1})
|
||||
|
||||
alt_oembed_url = reverse('event_oembed', kwargs={'pk': 999})
|
||||
alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999})
|
||||
|
||||
# Test the meta tag is in place
|
||||
response = self.client.get(event_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, event_embed_url)
|
||||
|
||||
# Should also work for non-existant events
|
||||
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, alt_event_embed_url)
|
||||
# And check that it no longer redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
assert len(response.redirect_chain) == 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('generateSampleRIGSData')
|
||||
def test_login_cookie_warning(client):
|
||||
login_url = reverse('login_embed')
|
||||
response = client.post(login_url, follow=True)
|
||||
assertContains(response, "Cookies do not seem to be enabled")
|
||||
|
||||
# Check there are lots of events
|
||||
self.assertTrue(models.Event.objects.all().count() > 100)
|
||||
|
||||
def test_production_exception(self):
|
||||
from django.core.management.base import CommandError
|
||||
def test_xframe_headers(admin_client, basic_event):
|
||||
event_url = reverse('event_embed', kwargs={'pk': basic_event.pk})
|
||||
login_url = reverse('login_embed')
|
||||
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
|
||||
response = admin_client.get(event_url, follow=True)
|
||||
with pytest.raises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
response = admin_client.get(login_url, follow=True)
|
||||
with pytest.raises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
|
||||
def test_oembed(client, basic_event):
|
||||
event_url = reverse('event_detail', kwargs={'pk': basic_event.pk})
|
||||
event_embed_url = reverse('event_embed', kwargs={'pk': basic_event.pk})
|
||||
oembed_url = reverse('event_oembed', kwargs={'pk': basic_event.pk})
|
||||
|
||||
alt_oembed_url = reverse('event_oembed', kwargs={'pk': 999})
|
||||
alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999})
|
||||
|
||||
assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url)
|
||||
|
||||
|
||||
def search(client, url, found, notfound, arguments):
|
||||
@@ -391,45 +338,11 @@ def test_search(admin_client):
|
||||
['name', 'id', 'address'])
|
||||
|
||||
|
||||
def setup_for_hs():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
venue = models.Venue.objects.create(name="Venue 1")
|
||||
return venue, {
|
||||
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.",
|
||||
venue=venue),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today()),
|
||||
}
|
||||
|
||||
|
||||
def create_ra(usr):
|
||||
venue, events = setup_for_hs()
|
||||
return models.RiskAssessment.objects.create(event=events[1], nonstandard_equipment=False, nonstandard_use=False,
|
||||
contractors=False, other_companies=False, crew_fatigue=False,
|
||||
big_power=False, power_mic=usr, generators=False,
|
||||
other_companies_power=False, nonstandard_equipment_power=False,
|
||||
multiple_electrical_environments=False, noise_monitoring=False,
|
||||
known_venue=True, safe_loading=True, safe_storage=True,
|
||||
area_outside_of_control=True, barrier_required=True,
|
||||
nonstandard_emergency_procedure=True, special_structures=False,
|
||||
suspended_structures=False, outside=False)
|
||||
|
||||
|
||||
def create_checklist(usr):
|
||||
venue, events = setup_for_hs()
|
||||
return models.EventChecklist.objects.create(event=events[1], power_mic=usr, safe_parking=False,
|
||||
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
||||
ear_plugs=False, hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
|
||||
date=timezone.now(), venue=venue)
|
||||
|
||||
|
||||
def test_list(admin_client):
|
||||
venue, events = setup_for_hs()
|
||||
def test_hs_list(admin_client, basic_event):
|
||||
request_url = reverse('hs_list')
|
||||
response = admin_client.get(request_url, follow=True)
|
||||
assertContains(response, events[1].name)
|
||||
assertContains(response, events[2].name)
|
||||
assertContains(response, basic_event.name)
|
||||
# assertContains(response, events[2].name)
|
||||
assertContains(response, 'Create')
|
||||
|
||||
|
||||
@@ -439,21 +352,229 @@ def review(client, profile, obj, request_url):
|
||||
obj.refresh_from_db()
|
||||
assertContains(response, 'Reviewed by')
|
||||
assertContains(response, profile.name)
|
||||
assert_times_equal(time, obj.reviewed_at)
|
||||
assert_times_almost_equal(time, obj.reviewed_at)
|
||||
|
||||
|
||||
def test_ra_review(admin_client, admin_user):
|
||||
review(admin_client, admin_user, create_ra(admin_user), 'ra_review')
|
||||
def test_ra_review(admin_client, admin_user, ra):
|
||||
review(admin_client, admin_user, ra, 'ra_review')
|
||||
|
||||
|
||||
def test_checklist_review(admin_client, admin_user):
|
||||
review(admin_client, admin_user, create_checklist(admin_user), 'ec_review')
|
||||
def test_checklist_review(admin_client, admin_user, checklist):
|
||||
review(admin_client, admin_user, checklist, 'ec_review')
|
||||
|
||||
|
||||
def test_ra_redirect(admin_client, admin_user):
|
||||
ra = create_ra(admin_user)
|
||||
def test_ra_redirect(admin_client, admin_user, ra):
|
||||
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
|
||||
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
|
||||
|
||||
response = admin_client.get(request_url, follow=True)
|
||||
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
|
||||
class TestMarkdownTemplateTags(TestCase):
|
||||
markdown = """
|
||||
An h1 header
|
||||
============
|
||||
|
||||
Paragraphs are separated by a blank line.
|
||||
|
||||
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
|
||||
look like:
|
||||
|
||||
* this one
|
||||
* that one
|
||||
* the other one
|
||||
|
||||
Note that --- not considering the asterisk --- the actual text
|
||||
content starts at 4-columns in.
|
||||
|
||||
> Block quotes are
|
||||
> written like so.
|
||||
>
|
||||
> They can span multiple paragraphs,
|
||||
> if you like.
|
||||
|
||||
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
|
||||
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
|
||||
Unicode is supported.
|
||||
|
||||
|
||||
|
||||
An h2 header
|
||||
------------
|
||||
|
||||
Here's a numbered list:
|
||||
|
||||
1. first item
|
||||
2. second item
|
||||
3. third item
|
||||
|
||||
Note again how the actual text starts at 4 columns in (4 characters
|
||||
from the left side). Here's a code sample:
|
||||
|
||||
# Let me re-iterate ...
|
||||
for i in 1 .. 10 { do-something(i) }
|
||||
|
||||
As you probably guessed, indented 4 spaces. By the way, instead of
|
||||
indenting the block, you can use delimited blocks, if you like:
|
||||
|
||||
~~~
|
||||
define foobar() {
|
||||
print "Welcome to flavor country!";
|
||||
}
|
||||
~~~
|
||||
|
||||
(which makes copying & pasting easier). You can optionally mark the
|
||||
delimited block for Pandoc to syntax highlight it:
|
||||
|
||||
~~~python
|
||||
import time
|
||||
# Quick, count to ten!
|
||||
for i in range(10):
|
||||
# (but not *too* quick)
|
||||
time.sleep(0.5)
|
||||
print i
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
### An h3 header ###
|
||||
|
||||
Now a nested list:
|
||||
|
||||
1. First, get these ingredients:
|
||||
|
||||
* carrots
|
||||
* celery
|
||||
* lentils
|
||||
|
||||
2. Boil some water.
|
||||
|
||||
3. Dump everything in the pot and follow
|
||||
this algorithm:
|
||||
|
||||
find wooden spoon
|
||||
uncover pot
|
||||
stir
|
||||
cover pot
|
||||
balance wooden spoon precariously on pot handle
|
||||
wait 10 minutes
|
||||
goto first step (or shut off burner when done)
|
||||
|
||||
Do not bump wooden spoon or it will fall.
|
||||
|
||||
Notice again how text always lines up on 4-space indents (including
|
||||
that last line which continues item 3 above).
|
||||
|
||||
Here's a link to [a website](http://foo.bar). Here's a footnote [^1].
|
||||
|
||||
[^1]: Footnote text goes here.
|
||||
|
||||
Tables can look like this:
|
||||
|
||||
size material color
|
||||
---- ------------ ------------
|
||||
9 leather brown
|
||||
10 hemp canvas natural
|
||||
11 glass transparent
|
||||
|
||||
Table: Shoes, their sizes, and what they're made of
|
||||
|
||||
(The above is the caption for the table.) Pandoc also supports
|
||||
multi-line tables:
|
||||
|
||||
-------- -----------------------
|
||||
keyword text
|
||||
-------- -----------------------
|
||||
red Sunsets, apples, and
|
||||
other red or reddish
|
||||
things.
|
||||
|
||||
green Leaves, grass, frogs
|
||||
and other things it's
|
||||
not easy being.
|
||||
-------- -----------------------
|
||||
|
||||
A horizontal rule follows.
|
||||
|
||||
***
|
||||
|
||||
Here's a definition list:
|
||||
|
||||
apples
|
||||
: Good for making applesauce.
|
||||
oranges
|
||||
: Citrus!
|
||||
tomatoes
|
||||
: There's no "e" in tomatoe.
|
||||
|
||||
Again, text is indented 4 spaces. (Put a blank line between each
|
||||
term/definition pair to spread things out more.)
|
||||
|
||||
Here's a "line block":
|
||||
|
||||
| Line one
|
||||
| Line too
|
||||
| Line tree
|
||||
|
||||
and images can be specified like so:
|
||||
|
||||

|
||||
|
||||
Inline math equations go in like so: $\\omega = d\\phi / dt$. Display
|
||||
math should get its own line and be put in in double-dollarsigns:
|
||||
|
||||
$$I = \\int \rho R^{2} dV$$
|
||||
|
||||
And note that you can backslash-escape any punctuation characters
|
||||
which you wish to be displayed literally, ex.: \\`foo\\`, \\*bar\\*, etc.
|
||||
"""
|
||||
|
||||
def test_html_safe(self):
|
||||
html = markdown_filter(self.markdown)
|
||||
self.assertIsInstance(html, SafeText)
|
||||
|
||||
def test_img_strip(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertNotIn("<img", rml)
|
||||
|
||||
def test_code(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertIn('<font face="Courier">monospace</font>', rml)
|
||||
|
||||
def test_blockquote(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertIn("<pre>\nBlock quotes", rml)
|
||||
|
||||
def test_lists(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertIn("<li><para>second item</para></li>", rml) # <ol>
|
||||
self.assertIn("<li><para>that one</para></li>", rml) # <ul>
|
||||
|
||||
def test_in_print(self):
|
||||
event = models.Event.objects.create(
|
||||
name="MD Print Test",
|
||||
description=self.markdown,
|
||||
start_date='2016-01-01',
|
||||
)
|
||||
user = models.Profile.objects.create(
|
||||
username='RML test',
|
||||
is_superuser=True, # Don't care about permissions
|
||||
is_active=True,
|
||||
)
|
||||
user.set_password('rmltester')
|
||||
user.save()
|
||||
|
||||
self.assertTrue(self.client.login(username=user.username, password='rmltester'))
|
||||
response = self.client.get(reverse('event_print', kwargs={'pk': event.pk}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# By the time we have a PDF it should be larger than the original by some margin
|
||||
# RML hard fails if something doesn't work
|
||||
self.assertGreater(len(response.content), len(self.markdown))
|
||||
|
||||
def test_nonetype(self):
|
||||
html = markdown_filter(None)
|
||||
self.assertIsNone(html)
|
||||
|
||||
def test_linebreaks(self):
|
||||
html = markdown_filter(self.markdown)
|
||||
self.assertIn("Itemized lists<br/>\nlook like", html)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path, re_path
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import RedirectView
|
||||
@@ -62,7 +61,7 @@ urlpatterns = [
|
||||
path('event/<int:pk>/embed/',
|
||||
xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
|
||||
name='event_embed'),
|
||||
path('event/<int:pk>/oembed_json/', rigboard.EventOembed.as_view(),
|
||||
path('event/<int:pk>/oembed_json/', rigboard.EventOEmbed.as_view(),
|
||||
name='event_oembed'),
|
||||
path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
|
||||
name='event_print'),
|
||||
@@ -133,6 +132,8 @@ urlpatterns = [
|
||||
name='event_authorise_preview'),
|
||||
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
|
||||
name='event_authorise'),
|
||||
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/preview/$', rigboard.EventAuthorise.as_view(preview=True),
|
||||
name='event_authorise_form_preview'),
|
||||
|
||||
# ICS Calendar - API key authentication
|
||||
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
|
||||
|
||||
2
app.json
@@ -51,7 +51,7 @@
|
||||
"url": "heroku/nodejs"
|
||||
},
|
||||
{
|
||||
"url": "heroku/python"
|
||||
"url": "https://github.com/nottinghamtec/heroku-buildpack-python"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = 'assets.apps.AssetsAppConfig'
|
||||
|
||||
8
assets/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AssetsAppConfig(AppConfig):
|
||||
name = 'assets'
|
||||
|
||||
def ready(self):
|
||||
import assets.signals
|
||||
8
assets/converters.py
Normal file
@@ -0,0 +1,8 @@
|
||||
class AssetIDConverter: # Forces lowercase to uppercase
|
||||
regex = '[^/]+'
|
||||
|
||||
def to_python(self, value):
|
||||
return str(value).upper()
|
||||
|
||||
def to_url(self, value):
|
||||
return str(value).upper()
|
||||
@@ -45,7 +45,7 @@ class CableTypeForm(forms.ModelForm):
|
||||
model = models.CableType
|
||||
fields = '__all__'
|
||||
|
||||
def clean(self):
|
||||
def clean(self): # TODO Does unique_together work better than this?
|
||||
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...
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from assets import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deletes testing sample data'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG):
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
self.delete_objects(models.AssetCategory)
|
||||
self.delete_objects(models.AssetStatus)
|
||||
self.delete_objects(models.Supplier)
|
||||
self.delete_objects(models.Connector)
|
||||
self.delete_objects(models.Asset)
|
||||
|
||||
def delete_objects(self, model):
|
||||
for object in model.objects.all():
|
||||
object.delete()
|
||||
@@ -1,17 +1,26 @@
|
||||
import random
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models as rigsmodels
|
||||
from assets import models
|
||||
from assets.models import get_available_asset_id
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Creates some sample data for testing'
|
||||
|
||||
categories = []
|
||||
statuses = []
|
||||
suppliers = []
|
||||
connectors = []
|
||||
assets = []
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
print("Generating sample assets data")
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG or settings.STAGING):
|
||||
@@ -19,57 +28,43 @@ class Command(BaseCommand):
|
||||
|
||||
random.seed('Some object to see the random number generator')
|
||||
|
||||
self.create_profile()
|
||||
self.create_categories()
|
||||
self.create_statuses()
|
||||
self.create_suppliers()
|
||||
self.create_assets()
|
||||
self.create_connectors()
|
||||
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()]))
|
||||
with transaction.atomic():
|
||||
self.create_categories()
|
||||
self.create_statuses()
|
||||
self.create_suppliers()
|
||||
self.create_assets()
|
||||
self.create_connectors()
|
||||
self.create_cables()
|
||||
print("Done generating sample assets data")
|
||||
|
||||
def create_categories(self):
|
||||
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
|
||||
|
||||
for cat in categories:
|
||||
models.AssetCategory.objects.create(name=cat)
|
||||
choices = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
|
||||
for cat in choices:
|
||||
self.categories.append(models.AssetCategory.objects.create(name=cat))
|
||||
|
||||
def create_statuses(self):
|
||||
statuses = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')]
|
||||
|
||||
for stat in statuses:
|
||||
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2])
|
||||
choices = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')]
|
||||
for stat in choices:
|
||||
self.statuses.append(models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2]))
|
||||
|
||||
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
|
||||
choices = ["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
|
||||
|
||||
with reversion.create_revision():
|
||||
for supplier in suppliers:
|
||||
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
|
||||
models.Supplier.objects.create(name=supplier)
|
||||
for supplier in choices:
|
||||
self.suppliers.append(models.Supplier.objects.create(name=supplier))
|
||||
|
||||
def create_assets(self):
|
||||
asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink']
|
||||
|
||||
categories = models.AssetCategory.objects.all()
|
||||
statuses = models.AssetStatus.objects.all()
|
||||
suppliers = models.Supplier.objects.all()
|
||||
|
||||
with reversion.create_revision():
|
||||
for i in range(100):
|
||||
for i in range(100):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
|
||||
asset_id = str(get_available_asset_id())
|
||||
asset = models.Asset(
|
||||
asset_id='{}'.format(models.Asset.get_available_asset_id()),
|
||||
asset_id=asset_id,
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(categories),
|
||||
status=random.choice(statuses),
|
||||
category=random.choice(self.categories),
|
||||
status=random.choice(self.statuses),
|
||||
date_acquired=timezone.now().date()
|
||||
)
|
||||
|
||||
@@ -77,53 +72,11 @@ class Command(BaseCommand):
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(suppliers)
|
||||
asset.purchased_from = random.choice(self.suppliers)
|
||||
|
||||
asset.clean()
|
||||
asset.save()
|
||||
|
||||
def create_cables(self):
|
||||
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
|
||||
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
|
||||
|
||||
csas = [0.75, 1.00, 1.25, 2.5, 4]
|
||||
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
|
||||
cores = [3, 5]
|
||||
circuits = [1, 2, 3, 6]
|
||||
categories = models.AssetCategory.objects.all()
|
||||
statuses = models.AssetStatus.objects.all()
|
||||
suppliers = models.Supplier.objects.all()
|
||||
connectors = models.Connector.objects.all()
|
||||
|
||||
for i in range(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):
|
||||
asset = models.Asset(
|
||||
asset_id='{}'.format(models.Asset.get_available_asset_id()),
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(categories),
|
||||
status=random.choice(statuses),
|
||||
date_acquired=timezone.now().date(),
|
||||
|
||||
is_cable=True,
|
||||
cable_type=random.choice(models.CableType.objects.all()),
|
||||
csa=random.choice(csas),
|
||||
length=random.choice(lengths),
|
||||
)
|
||||
|
||||
if i % 5 == 0:
|
||||
prefix = random.choice(asset_prefixes)
|
||||
asset.asset_id = prefix + str(models.Asset.get_available_asset_id(wanted_prefix=prefix))
|
||||
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(suppliers)
|
||||
|
||||
asset.clean()
|
||||
asset.save()
|
||||
|
||||
def create_connectors(self):
|
||||
connectors = [
|
||||
{"description": "13A UK", "current_rating": 13, "voltage_rating": 230, "num_pins": 3},
|
||||
@@ -134,3 +87,43 @@ class Command(BaseCommand):
|
||||
for connector in connectors:
|
||||
conn = models.Connector.objects.create(** connector)
|
||||
conn.save()
|
||||
self.connectors.append(conn)
|
||||
|
||||
def create_cables(self):
|
||||
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
|
||||
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
|
||||
|
||||
csas = [0.75, 1.00, 1.25, 2.5, 4]
|
||||
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
|
||||
cores = [3, 5]
|
||||
circuits = [1, 2, 3, 6]
|
||||
types = []
|
||||
|
||||
for i in range(len(self.connectors)):
|
||||
types.append(models.CableType.objects.create(plug=random.choice(self.connectors), socket=random.choice(self.connectors), circuits=random.choice(circuits), cores=random.choice(cores)))
|
||||
|
||||
for i in range(100):
|
||||
prefix = random.choice(asset_prefixes)
|
||||
asset_id = str(get_available_asset_id(wanted_prefix=prefix))
|
||||
asset_id = prefix + asset_id
|
||||
asset = models.Asset(
|
||||
asset_id=asset_id,
|
||||
description=random.choice(asset_description),
|
||||
category=random.choice(self.categories),
|
||||
status=random.choice(self.statuses),
|
||||
date_acquired=timezone.now().date(),
|
||||
|
||||
is_cable=True,
|
||||
cable_type=random.choice(types),
|
||||
csa=random.choice(csas),
|
||||
length=random.choice(lengths),
|
||||
)
|
||||
|
||||
if i % 4 == 0:
|
||||
asset.parent = models.Asset.objects.order_by('?').first()
|
||||
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(self.suppliers)
|
||||
|
||||
asset.clean()
|
||||
asset.save()
|
||||
|
||||
25
assets/migrations/0019_fix_cabletype.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.5 on 2021-02-08 16:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_default(apps, schema_editor):
|
||||
CableType = apps.get_model('assets', 'CableType')
|
||||
Connector = apps.get_model('assets', 'Connector')
|
||||
for cable_type in CableType.objects.all():
|
||||
if cable_type.plug is None:
|
||||
cable_type.plug = Connector.objects.first()
|
||||
if cable_type.socket is None:
|
||||
cable_type.socket = Connector.objects.first()
|
||||
cable_type.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0018_auto_20200415_1940'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default, migrations.RunPython.noop)
|
||||
]
|
||||
23
assets/migrations/0020_auto_20210302_1201.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 12:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def postgres_migration_prep(apps, schema_editor):
|
||||
model = apps.get_model("assets", "Supplier")
|
||||
fields = ["address", "email", "notes", "phone"]
|
||||
for field in fields:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0019_fix_cabletype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
|
||||
]
|
||||
50
assets/migrations/0021_auto_20210302_1204.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 12:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0020_auto_20210302_1201'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assetstatus',
|
||||
name='display_class',
|
||||
field=models.CharField(blank=True, default='', help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='plug',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plug', to='assets.connector'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cabletype',
|
||||
name='socket',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='socket', to='assets.connector'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supplier',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
]
|
||||
112
assets/models.py
@@ -2,8 +2,6 @@ import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, connection
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
from django.urls import reverse
|
||||
from reversion import revisions as reversion
|
||||
from reversion.models import Version
|
||||
@@ -12,46 +10,46 @@ from RIGS.models import RevisionMixin, Profile
|
||||
|
||||
|
||||
class AssetCategory(models.Model):
|
||||
name = models.CharField(max_length=80)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Asset Category'
|
||||
verbose_name_plural = 'Asset Categories'
|
||||
ordering = ['name']
|
||||
|
||||
name = models.CharField(max_length=80)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AssetStatus(models.Model):
|
||||
name = models.CharField(max_length=80)
|
||||
should_show = models.BooleanField(
|
||||
default=True, help_text="Should this be shown by default in the asset list.")
|
||||
display_class = models.CharField(max_length=80, blank=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Asset Status'
|
||||
verbose_name_plural = 'Asset Statuses'
|
||||
ordering = ['name']
|
||||
|
||||
name = models.CharField(max_length=80)
|
||||
should_show = models.BooleanField(
|
||||
default=True, help_text="Should this be shown by default in the asset list.")
|
||||
display_class = models.CharField(max_length=80, blank=True, null=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Supplier(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=80)
|
||||
phone = models.CharField(max_length=15, blank=True, default="")
|
||||
email = models.EmailField(blank=True, default="")
|
||||
address = models.TextField(blank=True, default="")
|
||||
|
||||
notes = models.TextField(blank=True, default="")
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
name = models.CharField(max_length=80)
|
||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||
email = models.EmailField(blank=True, null=True)
|
||||
address = models.TextField(blank=True, null=True)
|
||||
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('supplier_list')
|
||||
return reverse('supplier_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -67,17 +65,16 @@ class Connector(models.Model):
|
||||
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)
|
||||
related_name='plug')
|
||||
socket = models.ForeignKey(Connector, on_delete=models.CASCADE,
|
||||
related_name='socket', null=True)
|
||||
related_name='socket')
|
||||
|
||||
class Meta:
|
||||
ordering = ['plug', 'socket', '-circuits']
|
||||
|
||||
def __str__(self):
|
||||
if self.plug and self.socket:
|
||||
@@ -85,15 +82,31 @@ class CableType(models.Model):
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('cable_type_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
def get_available_asset_id(wanted_prefix=""):
|
||||
sql = """
|
||||
SELECT a.asset_id_number+1
|
||||
FROM assets_asset a
|
||||
LEFT OUTER JOIN assets_asset b ON
|
||||
(a.asset_id_number + 1 = b.asset_id_number AND
|
||||
a.asset_id_prefix = b.asset_id_prefix)
|
||||
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, [9000, wanted_prefix])
|
||||
row = cursor.fetchone()
|
||||
if row is None or row[0] is None:
|
||||
return 9000
|
||||
else:
|
||||
return row[0]
|
||||
cursor.close()
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Asset(models.Model, RevisionMixin):
|
||||
class Meta:
|
||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||
permissions = [
|
||||
('asset_finance', 'Can see financial data for assets')
|
||||
]
|
||||
|
||||
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
||||
blank=True, null=True, on_delete=models.SET_NULL)
|
||||
asset_id = models.CharField(max_length=15, unique=True)
|
||||
@@ -127,32 +140,18 @@ class Asset(models.Model, RevisionMixin):
|
||||
|
||||
reversion_perm = 'assets.asset_finance'
|
||||
|
||||
def get_available_asset_id(wanted_prefix=""):
|
||||
sql = """
|
||||
SELECT a.asset_id_number+1
|
||||
FROM assets_asset a
|
||||
LEFT OUTER JOIN assets_asset b ON
|
||||
(a.asset_id_number + 1 = b.asset_id_number AND
|
||||
a.asset_id_prefix = b.asset_id_prefix)
|
||||
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, [9000, wanted_prefix])
|
||||
row = cursor.fetchone()
|
||||
if row is None or row[0] is None:
|
||||
return 9000
|
||||
else:
|
||||
return row[0]
|
||||
class Meta:
|
||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||
permissions = [
|
||||
('asset_finance', 'Can see financial data for assets')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "{} | {}".format(self.asset_id, self.description)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('asset_detail', kwargs={'pk': self.asset_id})
|
||||
|
||||
def __str__(self):
|
||||
out = str(self.asset_id) + ' - ' + self.description
|
||||
if self.is_cable and self.cable_type is not None:
|
||||
out += '{} - {}m - {}'.format(self.cable_type.plug, self.length, self.cable_type.socket)
|
||||
return out
|
||||
|
||||
def clean(self):
|
||||
errdict = {}
|
||||
if self.date_sold and self.date_acquired > self.date_sold:
|
||||
@@ -188,14 +187,3 @@ class Asset(models.Model, RevisionMixin):
|
||||
@property
|
||||
def display_id(self):
|
||||
return str(self.asset_id)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Asset)
|
||||
def pre_save_asset(sender, instance, **kwargs):
|
||||
"""Automatically fills in hidden members on database access"""
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
if asset_search is None:
|
||||
instance.asset_id += "1"
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
instance.asset_id_prefix = asset_search.group(1)
|
||||
instance.asset_id_number = int(asset_search.group(2))
|
||||
|
||||
15
assets/signals.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import re
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
from .models import Asset
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Asset)
|
||||
def pre_save_asset(sender, instance, **kwargs):
|
||||
"""Automatically fills in hidden members on database access"""
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
if asset_search is None:
|
||||
instance.asset_id += "1"
|
||||
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
|
||||
instance.asset_id_prefix = asset_search.group(1)
|
||||
instance.asset_id_number = int(asset_search.group(2))
|
||||
@@ -64,16 +64,16 @@
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1">5{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1">10{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1">20{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1">1.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1">2.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'js/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 () {
|
||||
@@ -15,9 +12,7 @@
|
||||
});
|
||||
$('#searchButton').click(function (e) {
|
||||
e.preventDefault();
|
||||
var url = "{% url 'asset_audit' None %}";
|
||||
var id = $("#{{form.q.id_for_label}}").val();
|
||||
url = url.replace('None', id);
|
||||
var url = "{% url 'asset_audit' None %}".replace('None', $("#{{form.q.id_for_label}}").val();
|
||||
$.ajax({
|
||||
url: url,
|
||||
success: function(){
|
||||
@@ -49,7 +44,7 @@
|
||||
<span>Asset with that ID does not exist!</span>
|
||||
</div>
|
||||
|
||||
<form id="asset-search-form" class="mb-3" method="POST">
|
||||
<form id="asset-search-form" class="mb-3" method="GET">
|
||||
<div class="form-group form-row">
|
||||
<h3>Audit Asset:</h3>
|
||||
<div class="input-group input-group-lg">
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-end">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
{% include 'partials/asset_detail_form.html' %}
|
||||
|
||||