Compare commits

...

45 Commits

Author SHA1 Message Date
b31d53a3c5 Minor fixes to contact detail stuff on event auth form 2021-09-27 20:44:20 +01:00
62a891c6ec Fallback for when there is no person 2021-09-27 20:27:15 +01:00
8c0c0941c2 Update event authorisation status chip with more statusi
Closes #446
2021-09-23 11:39:54 +01:00
abb0e35690 Uncomment headless flag for old style tests 2021-09-18 10:04:34 +01:00
bec0d4aee5 Aronafail 2021-09-18 09:53:23 +01:00
f1e43b707e Bypass hCaptcha in automated testing 2021-09-18 09:47:21 +01:00
796f5b44b0 Navbar works properly again 2021-09-13 16:14:18 +01:00
6458f016f0 Switch to hCapatcha 2021-09-13 16:14:05 +01:00
9ca953423f Revamp registration form 2021-09-13 16:13:47 +01:00
dependabot[bot]
4c5d958c6d Build(deps): Bump sqlparse from 0.4.1 to 0.4.2 (#442)
Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.1...0.4.2)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-11 10:56:42 +01:00
85ca7b0880 Fix event button on invoice detail page
Was very confusing at first when I got a random event from several years ago!
2021-09-11 10:40:53 +01:00
44f9509eda Make very long asset children lists scroll 2021-09-08 15:41:14 +01:00
a2be4cbe5e Add some missing links to asset detail 2021-09-08 15:31:30 +01:00
dependabot[bot]
bb2f369ab5 Build(deps): Bump pillow from 8.2.0 to 8.3.2 (#441)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.2.0 to 8.3.2.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.2.0...8.3.2)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-08 12:48:44 +01:00
2fdb2f260f FEAT: Add ability to generate label images for cable assets
To come SoonTM: ability to generate a A4 page of labels at once
2021-09-07 14:54:01 +01:00
6de3cb5d8c Allow filling out of electrical checks for large events 2021-09-02 12:11:05 +01:00
7c38af66f6 May fix windows/chrome RA name chooser issue
No idea tbh
2021-08-31 19:47:49 +01:00
f1a624ec8f Improve RA detail layout slightly 2021-08-31 19:39:24 +01:00
ab01beb2cd Add title links to ra/ec detail 2021-08-31 19:33:03 +01:00
11636809ca Add link to subhire insurance form on event detail 2021-08-19 15:48:56 +01:00
d7458f6366 Account for null power MICs in event checklist detail 2021-08-16 20:28:30 +01:00
febf9cf3ed curses! 2021-08-05 12:07:23 +01:00
3322a5ddf8 Add badge to nav for number of waiting invoices
Might slightly help us stop leaving them waiting for far too long...
2021-08-05 11:37:10 +01:00
dependabot[bot]
673bee4215 Build(deps): Bump django from 3.1.8 to 3.1.12 (#436)
Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.12.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.8...3.1.12)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-29 17:17:19 +01:00
dependabot[bot]
bab31107f7 Build(deps): Bump pillow from 8.1.2 to 8.2.0 (#434)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.2 to 8.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.2...8.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-29 17:04:03 +01:00
dependabot[bot]
2d8473b698 Build(deps): Bump urllib3 from 1.26.4 to 1.26.5 (#432)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-29 16:48:50 +01:00
d81ecd9015 Minor fixes 2021-06-01 15:43:09 +01:00
b42c583897 FIX: Update detail test to match template change 2021-05-29 21:34:30 +01:00
57e966826e Redesign invoice detail page
Closes #431
2021-05-29 21:11:42 +01:00
6a5de4a9d6 Format all dates in event table the same way
Why did I think the old way was a good idea!
2021-05-19 11:17:51 +01:00
56bbf4c17c FIX #426: Override autofill styles in dark mode
Not super pretty, but you can at least read it!
2021-04-19 16:09:55 +01:00
dependabot[bot]
698f0be281 Build(deps): Bump django-debug-toolbar from 3.2 to 3.2.1 (#430)
Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.2 to 3.2.1.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.2...3.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 16:09:40 +01:00
dependabot[bot]
483f06e96f Build(deps): Bump django from 3.1.7 to 3.1.8 (#429)
Bumps [django](https://github.com/django/django) from 3.1.7 to 3.1.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.7...3.1.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 15:42:51 +01:00
dependabot[bot]
22193f3c39 Build(deps): Bump urllib3 from 1.26.3 to 1.26.4 (#428)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-15 16:45:31 +01:00
dependabot[bot]
59b63fe7aa Build(deps): Bump lxml from 4.6.2 to 4.6.3 (#427)
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.2 to 4.6.3.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.2...lxml-4.6.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 20:11:26 +01:00
5976ce9ea2 FIX Event form not displaying properly on creation error
Turns out that bit of code was needed ya goof
2021-04-08 19:32:28 +01:00
780d05e27c FIX: Rewrite event form hide/show logic.
Should be much more readable now. Closes #421
2021-03-31 18:46:16 +01:00
8cfa4bd79d Oh No (#425) 2021-03-25 14:27:14 +00:00
36f83ee59b Might fix CI 2021-03-15 12:18:30 +00:00
6d768832f4 Navbar layout wrangling 2021-03-15 11:59:37 +00:00
38da8642fa Add a bit of left/right padding to icons by default 2021-03-08 11:28:39 +00:00
f75e1d5bfc Use user-slash instead of (badly kerned) exclamation in Rigboard 2021-03-03 13:24:41 +00:00
3f959f8d56 Fix fix cabletype migration 2021-03-02 12:36:17 +00:00
b63a01120b Fix migrations 2021-03-02 12:15:56 +00:00
911336ceec More optimisation and cleanup (#420) 2021-03-02 11:29:57 +00:00
128 changed files with 6947 additions and 2750 deletions

View File

@@ -1,3 +1,5 @@
[run] [run]
plugins = django_coverage_plugin omit = */migrations/*
omit = */migrations/*, */tests/* */tests/*
*/site-packages/*
*/distutils/*

View File

@@ -10,38 +10,44 @@ jobs:
build: build:
if: "!contains(github.event.head_commit.message, '[ci skip]')" if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest runs-on: ubuntu-latest
# strategy:
# matrix:
# browser: ['chrome']
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# BROWSER: ${{ matrix.browser }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: bahmutov/npm-install@v1
- run: node node_modules/gulp/bin/gulp build
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: 3.9.1
- name: Cache python deps - uses: actions/cache@v2
uses: actions/cache@v2 id: pcache
with: with:
path: ${{ env.pythonLocation }} path: ~/.local/share/virtualenvs
key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }} key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install Dependencies - name: Install Dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip pipenv
pip install pycodestyle coveralls django_coverage_plugin pytest-cov pipenv install -d
pip install --upgrade --upgrade-strategy eager -r requirements.txt # if: steps.pcache.outputs.cache-hit != 'true'
python manage.py collectstatic --noinput - 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 - name: Basic Checks
run: | run: |
pycodestyle . --exclude=migrations,node_modules pipenv run pycodestyle . --exclude=migrations,node_modules
python manage.py check pipenv run python manage.py check
python manage.py makemigrations --check --dry-run pipenv run python manage.py makemigrations --check --dry-run
pipenv run python manage.py collectstatic --noinput
- name: Run Tests - name: Run Tests
run: pytest --cov -n 8 run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure() if: failure()
with: with:
@@ -49,4 +55,4 @@ jobs:
path: screenshots/ path: screenshots/
retention-days: 5 retention-days: 5
- name: Coveralls - name: Coveralls
run: coveralls --service=github run: pipenv run coveralls --service=github

102
Pipfile Normal file
View File

@@ -0,0 +1,102 @@
[[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.1.12"
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 = "*"
[requires]
python_version = "3.9"
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

1526
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

View File

View File

@@ -9,27 +9,21 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
""" """
import datetime import datetime
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) from pathlib import Path
import os
import secrets import secrets
import raven import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from envparse import env from envparse import env
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # 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') 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', cast=bool, default=True) DEBUG = env('DEBUG', cast=bool, default=True)
STAGING = env('STAGING', cast=bool, default=False) STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False) CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
@@ -55,6 +49,7 @@ if DEBUG:
# Application definition # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
'whitenoise.runserver_nostatic',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -70,13 +65,11 @@ INSTALLED_APPS = (
'debug_toolbar', 'debug_toolbar',
'registration', 'registration',
'reversion', 'reversion',
'captcha',
'widget_tweaks', 'widget_tweaks',
'raven.contrib.django.raven_compat', 'hcaptcha',
) )
MIDDLEWARE = ( MIDDLEWARE = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
@@ -87,19 +80,19 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'htmlmin.middleware.HtmlMinifyMiddleware',
'htmlmin.middleware.MarkRequestMiddleware',
) )
ROOT_URLCONF = 'PyRIGS.urls' ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application' WSGI_APPLICATION = 'PyRIGS.wsgi.application'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'NAME': str(BASE_DIR / 'db.sqlite3'),
} }
} }
@@ -177,9 +170,12 @@ else:
} }
} }
RAVEN_CONFIG = { # Error/performance monitoring
'dsn': env('RAVEN_DSN', default=""), sentry_sdk.init(
} dsn=env('SENTRY_DSN', default=""),
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
# User system # User system
AUTH_USER_MODEL = 'RIGS.Profile' AUTH_USER_MODEL = 'RIGS.Profile'
@@ -190,12 +186,13 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
# reCAPTCHA settings # CAPTCHA settings
RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key if DEBUG or CI:
RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key HCAPTCHA_SITEKEY = '10000000-ffff-ffff-ffff-000000000001'
NOCAPTCHA = True HCAPTCHA_SECRET = '0x0000000000000000000000000000000000000000'
else:
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error'] HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET')
# Email # Email
EMAILER_TEST = False EMAILER_TEST = False
@@ -232,21 +229,18 @@ USE_TZ = True
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S') DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/') STATIC_ROOT = str(BASE_DIR / 'static/')
STATIC_DIRS = (
os.path.join(BASE_DIR, 'static/')
)
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'pipeline/built_assets/'), str(BASE_DIR / 'pipeline/built_assets'),
] ]
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ 'DIRS': [
os.path.join(BASE_DIR, 'templates') BASE_DIR / 'templates'
], ],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {

View File

@@ -11,24 +11,16 @@ from selenium.webdriver.support.wait import WebDriverWait
from RIGS import models as rigsmodels from RIGS import models as rigsmodels
from . import pages 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) 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(): 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 = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080") options.add_argument("--window-size=1920,1080")
options.add_argument("--headless") options.add_argument("--headless")
@@ -60,6 +52,7 @@ class AutoLoginTest(BaseTest):
login_page.login("EventTest", "EventTestPassword") login_page.login("EventTest", "EventTestPassword")
# FIXME Refactor as a pytest fixture
def screenshot_failure(func): def screenshot_failure(func):
def wrapper_func(self, *args, **kwargs): def wrapper_func(self, *args, **kwargs):
try: try:
@@ -83,5 +76,30 @@ def screenshot_failure_cls(cls):
return 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) 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

View File

@@ -71,6 +71,7 @@ class BootstrapSelectElement(Region):
self.find_element(*self._deselect_all_locator).click() self.find_element(*self._deselect_all_locator).click()
def search(self, query): def search(self, query):
# self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
search_box = self.find_element(*self._search_locator) search_box = self.find_element(*self._search_locator)
self.open() self.open()
search_box.clear() search_box.clear()

View File

@@ -1,11 +1,26 @@
from PyRIGS import urls
from assets.tests.test_unit import create_asset_one
import pytest 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 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): def find_urls_recursive(patterns):
@@ -14,7 +29,7 @@ def find_urls_recursive(patterns):
if isinstance(url, URLResolver): if isinstance(url, URLResolver):
urls_to_check += find_urls_recursive(url.url_patterns) urls_to_check += find_urls_recursive(url.url_patterns)
elif isinstance(url, URLPattern): 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): if url.name is not None and url.name != "closemodal" and "json" not in str(url):
urls_to_check.append(url) urls_to_check.append(url)
return urls_to_check return urls_to_check
@@ -22,7 +37,6 @@ def find_urls_recursive(patterns):
def get_request_url(url): def get_request_url(url):
pattern = str(url.pattern) pattern = str(url.pattern)
request_url = ""
try: try:
kwargz = {} kwargz = {}
if ":pk>" in pattern: if ":pk>" in pattern:
@@ -34,12 +48,35 @@ def get_request_url(url):
print("Couldn't test url " + pattern) print("Couldn't test url " + pattern)
def test_unauthenticated(client): # Nothing should be available to the unauthenticated @pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
create_asset_one() '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): for url in find_urls_recursive(urls.urlpatterns):
request_url = get_request_url(url) request_url = get_request_url(url)
if request_url and 'user' not in request_url: # User module is full of edge cases 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') response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'Login') assertContains(response, 'Login')
if 'application/json+oembed' in response.content.decode(): if 'application/json+oembed' in response.content.decode():
assertTemplateUsed(response, 'login_redirect.html') assertTemplateUsed(response, 'login_redirect.html')
@@ -50,16 +87,59 @@ def test_unauthenticated(client): # Nothing should be available to the unauthen
expected_url = "{0}?next={1}".format(reverse('login'), request_url) expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url) assertRedirects(response, expected_url)
def test_page_titles(self):
def test_page_titles(admin_client): assert self.client.login(username='superuser', password='superuser')
create_asset_one()
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)): for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
request_url = get_request_url(url) request_url = get_request_url(url)
response = admin_client.get(request_url) response = self.client.get(request_url)
if hasattr(response, "context_data") and "page_title" in response.context_data: if hasattr(response, "context_data") and "page_title" in response.context_data:
expected_title = response.context_data["page_title"] expected_title = striptags(response.context_data["page_title"])
# try: assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title),
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title), response.content.decode()) response.content.decode())
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where! print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
# except: self.client.logout()
# 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()

View File

@@ -27,13 +27,14 @@ urlpatterns = [
path('', include('users.urls')), path('', include('users.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
import debug_toolbar import debug_toolbar
urlpatterns = [ urlpatterns += [
path('__debug__/', include(debug_toolbar.urls)), path('__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")), path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
] + urlpatterns ]

View File

@@ -3,6 +3,7 @@ import operator
from functools import reduce from functools import reduce
import simplejson import simplejson
from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.core import serializers from django.core import serializers
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@@ -11,6 +12,7 @@ from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse, NoReverseMatch from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic from django.views import generic
from django.views.decorators.clickjacking import xframe_options_exempt
from RIGS import models from RIGS import models
from assets import models as asset_models from assets import models as asset_models
@@ -19,10 +21,8 @@ from assets import models as asset_models
def is_ajax(request): def is_ajax(request):
return request.headers.get('x-requested-with') == 'XMLHttpRequest' 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): # Displays the current rig count along with a few other bits and pieces
class Index(generic.TemplateView):
template_name = 'index.html' template_name = 'index.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -230,15 +230,29 @@ class SearchHelp(generic.TemplateView):
template_name = 'search_help.html' 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): 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' template_name = 'closemodal.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return {'messages': messages.get_messages(self.request)} 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")

View File

@@ -10,7 +10,7 @@ from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse_lazy from django.urls import reverse
from django.views import generic from django.views import generic
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
@@ -55,7 +55,13 @@ class InvoiceDetail(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceDetail, self).get_context_data(**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 return context
@@ -67,12 +73,6 @@ class InvoicePrint(generic.View):
context = { context = {
'object': object, 'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'invoice': invoice, 'invoice': invoice,
'current_user': request.user, '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)) 'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
@@ -98,8 +98,8 @@ class InvoiceVoid(generic.View):
object.save() object.save()
if object.void: if object.void:
return HttpResponseRedirect(reverse_lazy('invoice_list')) return HttpResponseRedirect(reverse('invoice_list'))
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk})) return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceDelete(generic.DeleteView): class InvoiceDelete(generic.DeleteView):
@@ -110,14 +110,14 @@ class InvoiceDelete(generic.DeleteView):
obj = self.get_object() obj = self.get_object()
if obj.payment_set.all().count() > 0: if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.') 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) return super(InvoiceDelete, self).get(pk)
def post(self, request, pk): def post(self, request, pk):
obj = self.get_object() obj = self.get_object()
if obj.payment_set.all().count() > 0: if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.') 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) return super(InvoiceDelete, self).post(pk)
def get_success_url(self): def get_success_url(self):
@@ -172,30 +172,14 @@ class InvoiceWaiting(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs) context = super(InvoiceWaiting, self).get_context_data(**kwargs)
total = 0 total = 0
for obj in self.get_objects(): objects = self.get_queryset()
for obj in objects:
total += obj.sum_total 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 return context
def get_queryset(self): def get_queryset(self):
return self.get_objects() return self.model.objects.waiting_invoices()
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
class InvoiceEvent(generic.View): class InvoiceEvent(generic.View):
@@ -216,7 +200,7 @@ class InvoiceEvent(generic.View):
invoice.save() invoice.save()
messages.warning(self.request, 'Invoice voided') 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): class PaymentCreate(generic.CreateView):
@@ -242,7 +226,7 @@ class PaymentCreate(generic.CreateView):
def get_success_url(self): def get_success_url(self):
messages.info(self.request, "location.reload()") messages.info(self.request, "location.reload()")
return reverse_lazy('closemodal') return reverse('closemodal')
class PaymentDelete(generic.DeleteView): class PaymentDelete(generic.DeleteView):

View File

@@ -70,12 +70,20 @@ class EventRiskAssessmentDetail(generic.DetailView):
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'risk_assessment_detail.html' 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): class EventRiskAssessmentList(generic.ListView):
paginate_by = 20 paginate_by = 20
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'hs_object_list.html' template_name = 'hs_object_list.html'
def get_queryset(self):
return self.model.objects.order_by('reviewed_at').select_related('event')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs) context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
context['title'] = 'Risk Assessment' context['title'] = 'Risk Assessment'
@@ -83,7 +91,6 @@ class EventRiskAssessmentList(generic.ListView):
context['edit'] = 'ra_edit' context['edit'] = 'ra_edit'
context['review'] = 'ra_review' context['review'] = 'ra_review'
context['perm'] = 'perms.RIGS.review_riskassessment' 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 return context
@@ -105,7 +112,7 @@ class EventChecklistDetail(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventChecklistDetail, self).get_context_data(**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 return context
@@ -187,7 +194,6 @@ class EventChecklistList(generic.ListView):
context['edit'] = 'ec_edit' context['edit'] = 'ec_edit'
context['review'] = 'ec_review' context['review'] = 'ec_review'
context['perm'] = 'perms.RIGS.review_eventchecklist' 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 return context
@@ -209,7 +215,7 @@ class HSList(generic.ListView):
template_name = 'hs_list.html' template_name = 'hs_list.html'
def get_queryset(self): def get_queryset(self):
return models.Event.objects.all().order_by('-start_date') return models.Event.objects.all().order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(HSList, self).get_context_data(**kwargs) context = super(HSList, self).get_context_data(**kwargs)

View File

@@ -0,0 +1,37 @@
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
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)
def delete_objects(self, model):
for obj in model.objects.all():
obj.delete()

View File

@@ -1,11 +1,14 @@
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from RIGS import models
class Command(BaseCommand): class Command(BaseCommand):
help = 'Adds sample data to use for testing' help = 'Adds sample data to use for testing'
can_import_settings = True can_import_settings = True
def handle(self, *args, **options): def handle(self, *args, **options):
call_command('generateSampleUserData')
call_command('generateSampleRIGSData') call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData') call_command('generateSampleAssetsData')

View File

@@ -17,11 +17,8 @@ class Command(BaseCommand):
people = [] people = []
organisations = [] organisations = []
venues = [] venues = []
profiles = [] events = []
profiles = models.Profile.objects.all()
keyholder_group = None
finance_group = None
hs_group = None
def handle(self, *args, **options): def handle(self, *args, **options):
from django.conf import settings from django.conf import settings
@@ -34,20 +31,12 @@ class Command(BaseCommand):
with transaction.atomic(): with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') 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()
self.setupGenericProfiles() def setup_people(self):
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
@@ -62,25 +51,25 @@ class Command(BaseCommand):
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names): for i, name in enumerate(names):
with reversion.create_revision(): 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: if i % 3 == 0:
newPerson.email = "address@person.com" person.email = "address@person.com"
if i % 5 == 0: 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: 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: if i % 9 == 0:
newPerson.phone = "01234 567894" person.phone = "01234 567894"
newPerson.save() person.save()
self.people.append(newPerson) 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", 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", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
@@ -109,27 +98,28 @@ class Command(BaseCommand):
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
for i, name in enumerate(names): for i, name in enumerate(names):
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(self.profiles)) reversion.set_user(random.choice(models.Profile.objects.all()))
newOrganisation = models.Organisation.objects.create(name=name) new_organisation = models.Organisation.objects.create(name=name)
if i % 2 == 0: if i % 2 == 0:
newOrganisation.has_su_account = True new_organisation.has_su_account = True
if i % 3 == 0: if i % 3 == 0:
newOrganisation.email = "address@organisation.com" new_organisation.email = "address@organisation.com"
if i % 5 == 0: 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: 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: if i % 9 == 0:
newOrganisation.phone = "01234 567894" new_organisation.phone = "01234 567894"
newOrganisation.save() new_organisation.save()
self.organisations.append(newOrganisation) 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", names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
@@ -145,108 +135,27 @@ class Command(BaseCommand):
for i, name in enumerate(names): for i, name in enumerate(names):
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(self.profiles)) 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: if i % 2 == 0:
newVenue.three_phase_available = True new_venue.three_phase_available = True
if i % 3 == 0: if i % 3 == 0:
newVenue.email = "address@venue.com" new_venue.email = "address@venue.com"
if i % 5 == 0: 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: 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: if i % 9 == 0:
newVenue.phone = "01234 567894" new_venue.phone = "01234 567894"
newVenue.save() new_venue.save()
self.venues.append(newVenue) self.venues.append(new_venue)
def setupGroups(self): def setup_events(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):
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", 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", "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", "End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
@@ -257,7 +166,7 @@ class Command(BaseCommand):
notes = ["The client came into the office at some point", "Who knows if this will happen", 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!"] "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, {'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
'cost': 200.00}, 'cost': 200.00},
{'name': 'Projector', {'name': 'Projector',
@@ -274,7 +183,7 @@ class Command(BaseCommand):
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00}, {'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}] {'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 for i in range(150): # Let's add 100 events
with reversion.create_revision(): with reversion.create_revision():
@@ -282,70 +191,71 @@ class Command(BaseCommand):
name = names[i % len(names)] name = names[i % len(names)]
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta) start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
dayDelta = dayDelta + random.randint(0, 3) 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 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 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 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) new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
newEvent.end_time = datetime.time(random.randint(0, 5)) 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 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 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 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 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 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 # 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]) [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 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 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 # Now add some items
for j in range(random.randint(1, 5)): for j in range(random.randint(1, 5)):
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)] item_data = item_options[random.randint(0, len(item_options) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData) new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
newItem.save() new_item.save()
while newEvent.sum_total < 0: while new_event.sum_total < 0:
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)] item_data = item_options[random.randint(0, len(item_options) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData) new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
newItem.save() new_item.save()
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(self.profiles)) 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 if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
newInvoice = models.Invoice.objects.create(event=newEvent) new_invoice = models.Invoice.objects.create(event=new_event)
if newEvent.status is models.Event.CANCELLED: # void cancelled events if new_event.status is models.Event.CANCELLED: # void cancelled events
newInvoice.void = True new_invoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid 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()) date=datetime.date.today())
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA 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)), nonstandard_use=bool(random.getrandbits(1)),
contractors=bool(random.getrandbits(1)), contractors=bool(random.getrandbits(1)),
other_companies=bool(random.getrandbits(1)), other_companies=bool(random.getrandbits(1)),
@@ -366,8 +276,15 @@ class Command(BaseCommand):
suspended_structures=bool(random.getrandbits(1)), suspended_structures=bool(random.getrandbits(1)),
outside=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 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)), models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles),
safe_packing=bool(random.getrandbits(1)), exits=bool(random.getrandbits(1)), trip_hazard=bool(random.getrandbits(1)), warning_signs=bool(random.getrandbits(1)), safe_parking=bool(random.getrandbits(1)),
ear_plugs=bool(random.getrandbits(1)), hs_location="Locked away safely", safe_packing=bool(random.getrandbits(1)),
extinguishers_location="Somewhere, I forgot", earthing=bool(random.getrandbits(1)), pat=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)) date=timezone.now(), venue=random.choice(self.venues))

View File

@@ -15,5 +15,5 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(approve_legacy) migrations.RunPython(approve_legacy, migrations.RunPython.noop)
] ]

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

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

View File

@@ -12,7 +12,7 @@ from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse_lazy from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from reversion import revisions as reversion from reversion import revisions as reversion
@@ -21,11 +21,12 @@ from reversion.models import Version
class Profile(AbstractUser): class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False) initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True) phone = models.CharField(max_length=13, blank=True, default='')
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True) api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
is_approved = models.BooleanField(default=False) is_approved = models.BooleanField(default=False)
last_emailed = models.DateTimeField(blank=True, # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
null=True) # 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)
@classmethod @classmethod
def make_api_key(cls): def make_api_key(cls):
@@ -51,7 +52,7 @@ class Profile(AbstractUser):
@property @property
def latest_events(self): def latest_events(self):
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
@classmethod @classmethod
def admins(cls): def admins(cls):
@@ -102,12 +103,12 @@ class RevisionMixin(object):
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True) phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, null=True) 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): def __str__(self):
string = self.name 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') return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self): 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): class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True) phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, null=True) 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) union_account = models.BooleanField(default=False)
def __str__(self): 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') return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self): 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): class VatManager(models.Manager):
@@ -178,7 +179,6 @@ class VatManager(models.Manager):
return self.find_rate(timezone.now()) return self.find_rate(timezone.now())
def find_rate(self, date): def find_rate(self, date):
# return self.filter(startAt__lte=date).latest()
try: try:
return self.filter(start_at__lte=date).latest() return self.filter(start_at__lte=date).latest()
except VatRate.DoesNotExist: except VatRate.DoesNotExist:
@@ -211,12 +211,12 @@ class VatRate(models.Model, RevisionMixin):
class Venue(models.Model, RevisionMixin): class Venue(models.Model, RevisionMixin):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
phone = models.CharField(max_length=15, blank=True, null=True) phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, null=True) email = models.EmailField(blank=True, default='')
three_phase_available = models.BooleanField(default=False) 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): def __str__(self):
string = self.name 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') return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self): 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): class EventManager(models.Manager):
def current_events(self): def current_events(self):
events = self.filter( 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 status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q( (models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Ends after 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 status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & ( (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.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 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', ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
'organisation',
'venue', 'mic')
return events return events
def events_in_bounds(self, start, end): def events_in_bounds(self, start, end):
@@ -269,16 +268,29 @@ class EventManager(models.Manager):
def rig_count(self): def rig_count(self):
event_count = self.filter( 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( is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end 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 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 status=Event.CANCELLED)) # Active dry hire
).count() ).count()
return event_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']) @reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin): 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) person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
organisation = models.ForeignKey('Organisation', blank=True, null=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) venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, null=True) notes = models.TextField(blank=True, default='')
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL) status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False) dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True) is_rig = models.BooleanField(default=True)
@@ -313,7 +325,6 @@ class Event(models.Model, RevisionMixin):
end_time = models.TimeField(blank=True, null=True) end_time = models.TimeField(blank=True, null=True)
access_at = models.DateTimeField(blank=True, null=True) access_at = models.DateTimeField(blank=True, null=True)
meet_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 # Crew management
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True, checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
@@ -322,15 +333,13 @@ class Event(models.Model, RevisionMixin):
verbose_name="MIC", on_delete=models.CASCADE) verbose_name="MIC", on_delete=models.CASCADE)
# Monies # Monies
payment_method = models.CharField(max_length=255, blank=True, null=True) purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
payment_received = models.CharField(max_length=255, blank=True, null=True) collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
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')
# Authorisation request details # Authorisation request details
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE) 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_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 @property
def display_id(self): def display_id(self):
@@ -346,7 +355,7 @@ class Event(models.Model, RevisionMixin):
@property @property
def sum_total(self): 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'), sum_total=models.Sum(models.F('cost') * models.F('quantity'),
output_field=models.DecimalField(max_digits=10, decimal_places=2)) output_field=models.DecimalField(max_digits=10, decimal_places=2))
)['sum_total'] )['sum_total']
@@ -456,7 +465,7 @@ class Event(models.Model, RevisionMixin):
objects = EventManager() objects = EventManager()
def get_absolute_url(self): 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): def __str__(self):
return "{}: {}".format(self.display_id, self.name) return "{}: {}".format(self.display_id, self.name)
@@ -490,7 +499,7 @@ class Event(models.Model, RevisionMixin):
class EventItem(models.Model, RevisionMixin): class EventItem(models.Model, RevisionMixin):
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE) event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, default='')
quantity = models.IntegerField() quantity = models.IntegerField()
cost = models.DecimalField(max_digits=10, decimal_places=2) cost = models.DecimalField(max_digits=10, decimal_places=2)
order = models.IntegerField() order = models.IntegerField()
@@ -505,7 +514,7 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order'] ordering = ['order']
def __str__(self): 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 @property
def activity_feed_string(self): def activity_feed_string(self):
@@ -517,13 +526,13 @@ class EventAuthorisation(models.Model, RevisionMixin):
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE) event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
email = models.EmailField() email = models.EmailField()
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID") uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
account_code = models.CharField(max_length=50, blank=True, null=True) account_code = models.CharField(max_length=50, default='', blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount") amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE) sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk}) return reverse('event_detail', kwargs={'pk': self.event_id})
@property @property
def activity_feed_string(self): def activity_feed_string(self):
@@ -562,11 +571,11 @@ class Invoice(models.Model, RevisionMixin):
return self.balance == 0 or self.void return self.balance == 0 or self.void
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('invoice_detail', kwargs={'pk': self.pk}) return reverse('invoice_detail', kwargs={'pk': self.pk})
@property @property
def activity_feed_string(self): 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): def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
@@ -597,7 +606,7 @@ class Payment(models.Model, RevisionMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE) invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
date = models.DateField() date = models.DateField()
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT') 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 reversion_hide = True
@@ -632,10 +641,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>") 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>") 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?") 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 # 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?") 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... # 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, power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
@@ -645,12 +653,12 @@ class RiskAssessment(models.Model, RevisionMixin):
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?") 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?") 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?") 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_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, 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_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 # 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?") 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 # Site
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?") known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
@@ -663,8 +671,8 @@ class RiskAssessment(models.Model, RevisionMixin):
# Structures # Structures
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?") 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?") 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?") 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, 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]) 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 # Blimey that was a lot of options
@@ -708,6 +716,10 @@ class RiskAssessment(models.Model, RevisionMixin):
('review_riskassessment', 'Can review Risk Assessments') ('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 @property
def event_size(self): def event_size(self):
# Confirm event size. Check all except generators, since generators entails outside # Confirm event size. Check all except generators, since generators entails outside
@@ -723,7 +735,7 @@ class RiskAssessment(models.Model, RevisionMixin):
return str(self.event) return str(self.event)
def get_absolute_url(self): 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): def __str__(self):
return "%i - %s" % (self.pk, self.event) return "%i - %s" % (self.pk, self.event)
@@ -746,8 +758,8 @@ class EventChecklist(models.Model, RevisionMixin):
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?") 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>") 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?") 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") hs_location = models.CharField(blank=True, default='', 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") extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
# Small Electrical Checks # Small Electrical Checks
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?") rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
@@ -768,15 +780,15 @@ class EventChecklist(models.Model, RevisionMixin):
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.IntegerField(blank=True, null=True, 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") fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
# Worst case points # 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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") 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>)") 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") 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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") 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>)") 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") 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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") 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.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
@@ -796,12 +808,16 @@ class EventChecklist(models.Model, RevisionMixin):
('review_eventchecklist', 'Can review Event Checklists') ('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 @property
def activity_feed_string(self): def activity_feed_string(self):
return str(self.event) return str(self.event)
def get_absolute_url(self): 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): def __str__(self):
return "%i - %s" % (self.pk, self.event) return "%i - %s" % (self.pk, self.event)

View File

@@ -11,7 +11,7 @@ import simplejson
from PyPDF2 import PdfFileMerger, PdfFileReader from PyPDF2 import PdfFileMerger, PdfFileReader
from django.conf import settings from django.conf import settings
from django.contrib import messages 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 import signing
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
@@ -27,6 +27,7 @@ from django.views import generic
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from PyRIGS import decorators from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax
from RIGS import models, forms from RIGS import models, forms
__author__ = 'ghost' __author__ = 'ghost'
@@ -40,7 +41,7 @@ class RigboardIndex(generic.TemplateView):
context = super(RigboardIndex, self).get_context_data(**kwargs) context = super(RigboardIndex, self).get_context_data(**kwargs)
# call out method to get current events # 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" context['page_title'] = "Rigboard"
return context return context
@@ -59,29 +60,24 @@ class EventDetail(generic.DetailView):
template_name = 'event_detail.html' template_name = 'event_detail.html'
model = models.Event model = models.Event
def get_context_data(self, **kwargs):
class EventOembed(generic.View): context = super(EventDetail, self).get_context_data(**kwargs)
model = models.Event title = "{} | {}".format(self.object.display_id, self.object.name)
if self.object.dry_hire:
def get(self, request, pk=None): title += " <span class='badge badge-secondary'>Dry Hire</span>"
embed_url = reverse('event_embed', args=[pk]) context['page_title'] = title
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url) return context
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
class EventEmbed(EventDetail): class EventEmbed(EventDetail):
template_name = 'event_embed.html' template_name = 'event_embed.html'
class EventOEmbed(OEmbedView):
model = models.Event
url_name = 'event_embed'
class EventCreate(generic.CreateView): class EventCreate(generic.CreateView):
model = models.Event model = models.Event
form_class = forms.EventForm form_class = forms.EventForm
@@ -157,7 +153,7 @@ class EventDuplicate(EventUpdate):
new.checked_in_by = None new.checked_in_by = None
# Remove all the authorisation information from the new event # 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_by = None
new.auth_request_at = None new.auth_request_at = None
@@ -185,15 +181,9 @@ class EventPrint(generic.View):
context = { context = {
'object': object, 'object': object,
'fonts': {
'opensans': {
'regular': 'static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'static/fonts/OPENSANS-BOLD.TTF',
}
},
'quote': True, 'quote': True,
'current_user': request.user, '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) rml = template.render(context)
@@ -284,6 +274,7 @@ class EventArchive(generic.ListView):
class EventAuthorise(generic.UpdateView): class EventAuthorise(generic.UpdateView):
template_name = 'eventauthorisation_form.html' template_name = 'eventauthorisation_form.html'
success_template = 'eventauthorisation_success.html' success_template = 'eventauthorisation_success.html'
preview = False
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
@@ -311,6 +302,7 @@ class EventAuthorise(generic.UpdateView):
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name) context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
if self.event.dry_hire: if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>' context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -359,7 +351,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
return self.get_object() return self.get_object()
def get_success_url(self): def get_success_url(self):
if self.request.is_ajax(): if is_ajax(self.request):
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
messages.info(self.request, "location.reload()") messages.info(self.request, "location.reload()")
else: else:
@@ -397,7 +389,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
to=[email], to=[email],
reply_to=[self.request.user.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), html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
external_styles=css).transform() external_styles=css).transform()
msg.attach_alternative(html, 'text/html') msg.attach_alternative(html, 'text/html')
@@ -412,8 +404,7 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
model = models.Event model = models.Event
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
from django.contrib.staticfiles.storage import staticfiles_storage css = finders.find('css/email.css')
css = staticfiles_storage.path('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs) response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse) assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform() response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
@@ -427,4 +418,5 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
'sent_by': self.request.user.pk, 'sent_by': self.request.user.pk,
}) })
context['to_name'] = self.request.GET.get('to_name', None) context['to_name'] = self.request.GET.get('to_name', None)
context['target'] = 'event_authorise_form_preview'
return context return context

View File

@@ -6,7 +6,7 @@ from io import BytesIO
from PyPDF2 import PdfFileReader, PdfFileMerger from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings 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.cache import cache
from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.db.models.signals import post_save 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 # Generate PDF first to prevent context conflicts
context = { context = {
'object': instance.event, 'object': instance.event,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'receipt': True, 'receipt': True,
'current_user': False, 'current_user': False,
} }
@@ -69,7 +63,7 @@ def send_eventauthorisation_success_email(instance):
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS], 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), html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
external_styles=css).transform() external_styles=css).transform()
client_email.attach_alternative(html, 'text/html') client_email.attach_alternative(html, 'text/html')
@@ -127,7 +121,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
to=[admin.email], to=[admin.email],
reply_to=[user.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), html = Premailer(get_template("admin_awaiting_approval.html").render(context),
external_styles=css).transform() external_styles=css).transform()
email.attach_alternative(html, 'text/html') email.attach_alternative(html, 'text/html')

BIN
RIGS/static/imgs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
RIGS/static/imgs/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -1,9 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% load invoices_waiting from filters %}
{% block titleheader %} {% block titleheader %}
<a class="navbar-brand" href="/">RIGS</a> <a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
{% endblock %} {% endblock %}
{% block titleelements %} {% block titleelements %}
@@ -45,11 +46,11 @@
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <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 badge-danger badge-pill">{% invoices_waiting %}</span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices"> <div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
{% if perms.RIGS.add_invoice %} {% 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 badge-danger badge-pill">{% invoices_waiting %}</span></a>
{% endif %} {% 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</a>
<a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a> <a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a>
@@ -74,6 +75,7 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }}
<script src="{% static 'js/tooltip.js' %}"></script> <script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script> <script src="{% static 'js/popover.js' %}"></script>
<script> <script>

View File

@@ -4,12 +4,12 @@
{% block title %}Calendar{% endblock %} {% block title %}Calendar{% endblock %}
{% block css %} {% block css %}
<link href="{% static 'css/main.min.css' %}" rel='stylesheet' /> <link href="{% static 'css/main.css' %}" rel='stylesheet' />
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'js/moment.js' %}"></script> <script src="{% static 'js/moment.js' %}"></script>
<script src="{% static 'js/main.min.js' %}"></script> <script src="{% static 'js/main.js' %}"></script>
<script> <script>
viewToUrl = { viewToUrl = {
'timeGridWeek':'week', 'timeGridWeek':'week',

View File

@@ -5,11 +5,13 @@
{% load static %} {% load static %}
{% block css %} {% block css %}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/> {{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
<script src="{% static 'js/bootstrap-select.js' %}"></script> {{ block.super }}
<script src="{% static 'js/selects.js' %}" async></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@@ -32,7 +32,11 @@
</dd> </dd>
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt> <dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
<dd class="col-6"> <dd class="col-6">
{% if object.power_mic %}
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a> <a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
{% else %}
None
{% endif %}
</dd> </dd>
</dl> </dl>
<p>List vehicles and their drivers</p> <p>List vehicles and their drivers</p>

View File

@@ -7,24 +7,18 @@
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/> <link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/bootstrap-select.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }} {{ 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/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %} {% include 'partials/datetime-fix.html' %}
@@ -134,14 +128,14 @@
<tbody id="vehiclest" data-pk="-1"> <tbody id="vehiclest" data-pk="-1">
<tr id="vehicles_new" style="display: none;"> <tr id="vehicles_new" style="display: none;">
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td> <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> <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> </tr>
{% for i in object.vehicles.all %} {% for i in object.vehicles.all %}
<tr id="vehicles_{{i.pk}}"> <tr id="vehicles_{{i.pk}}">
<td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td> <td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td>
<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 != '' %} {% if i.driver != '' %}
<option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option> <option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option>
{% endif %} {% endif %}
@@ -202,7 +196,7 @@
<tbody id="crewmemberst" data-pk="-1"> <tbody id="crewmemberst" data-pk="-1">
<tr id="crew_new" style="display: none;"> <tr id="crew_new" style="display: none;">
<td> <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>
<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="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> <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 %} {% for crew in object.crew.all %}
<tr id="crew_{{crew.pk}}"> <tr id="crew_{{crew.pk}}">
<td> <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 != '' %} {% if crew.crewmember != '' %}
<option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option> <option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option>
{% endif %} {% endif %}
@@ -250,12 +244,19 @@
</div> </div>
</div> </div>
</div> </div>
{% elif event.riskassessment.event_size == 1 %} {% else %}
<div class="row my-3" id="size-1"> <div class="row my-3" id="size-1">
<div class="col-12"> <div class="col-12">
{% if event.riskassessment.event_size == 1 %}
<div class="card border-warning"> <div class="card border-warning">
<div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div> <div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div>
<div class="card-body"> <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.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %} {% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %} {% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
@@ -345,17 +346,6 @@
</div> </div>
</div> </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 %} {% endif %}
<div class="row mt-3"> <div class="row mt-3">
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">

View File

@@ -1,71 +1,18 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% 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 %}
{% block content %} {% block content %}
<div class="row my-3 py-3"> <div class="row my-3 py-3">
{% if not request.is_ajax %} {% 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 %} {% if perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'event_detail_buttons.html' %} {% include 'partials/event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if object.is_rig and perms.RIGS.view_event %} {% if object.is_rig and perms.RIGS.view_event %}
{# only need contact details for a rig #} {# only need contact details for a rig #}
<div class="col-md-6"> <div class="col-md-6">
{% if event.person %} {% include 'partials/contact_details.html' %}
<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 %}
</div> </div>
{% endif %} {% endif %}
<div class="col-md-6"> <div class="col-md-6">
@@ -85,7 +32,7 @@
{% endif %} {% endif %}
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'event_detail_buttons.html' %} {% include 'partials/event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% if event.is_rig %} {% if event.is_rig %}
@@ -105,7 +52,7 @@
</div> </div>
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'event_detail_buttons.html' %} {% include 'partials/event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -1,16 +1,13 @@
{% extends 'base_embed.html' %} {% extends 'base_embed.html' %}
{% load static %} {% load static %}
{% block content %} {% block extra-head %}
<div class="row"> <link href="{% static 'fontawesome_free/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<div class="col-sm-12"> <link href="{% static 'fontawesome_free/css/solid.css' %}" rel="stylesheet" type="text/css">
<a href="/"> {% endblock %}
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
</a>
</div>
<div class="col-sm-12"> {% block content %}
<span class="pull-right"> <span class="float-right">
{% if object.mic %} {% if object.mic %}
<div class="text-center"> <div class="text-center">
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/> <img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/>
@@ -21,9 +18,7 @@
</span> </span>
<h3> <h3>
<a href="{% url 'event_detail' object.pk %}"> <a href="{% url 'event_detail' object.pk %}">{{ object.display_id }} | {{ object.name }}</a>
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
| {{ object.name }} </a>
{% if object.venue %} {% if object.venue %}
<small>at {{ object.venue }}</small> <small>at {{ object.venue }}</small>
{% endif %} {% endif %}
@@ -43,13 +38,9 @@
{% endif %} {% endif %}
</small> </small>
</h3> </h3>
{% include 'partials/event_status.html' %}
<div class="row"> <div class="row ml-2">
<div class="col-xs-6"> <div class="col-xs-6 pr-2">
<p>
<strong>Status:</strong>
{{ object.get_status_display }}
</p>
<p> <p>
{% if object.is_rig %} {% if object.is_rig %}
<strong>Client:</strong> {{ object.person.name }} <strong>Client:</strong> {{ object.person.name }}
@@ -70,7 +61,7 @@
{% endif %} {% endif %}
</p> </p>
</div> </div>
<div class="col-xs-6"> <div class="col-xs-6 px-2">
{% if object.meet_at %} {% if object.meet_at %}
<p> <p>
<strong>Crew meet:</strong> <strong>Crew meet:</strong>
@@ -95,6 +86,4 @@
{{ object.description|linebreaksbr }} {{ object.description|linebreaksbr }}
</p> </p>
{% endif %} {% endif %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -7,25 +7,19 @@
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/> <link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
<link rel="stylesheet" href="{% static 'css/flatpickr.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/bootstrap-select.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }} {{ 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/autocompleter.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %} {% include 'partials/datetime-fix.html' %}
@@ -33,18 +27,26 @@
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches; const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
$(document).ready(function () { $(document).ready(function () {
dur = matches ? 0 : 500; dur = matches ? 0 : 500;
{% if not object.pk and not form.errors %} {% if object.pk %}
$('.form-hws').slideUp(dur, function () { // Editing
$('.form-is_rig').slideUp(dur); {% if not object.is_rig %}
}); $('.form-is_rig').hide();
{% elif not object.pk and form.errors %} {% 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') { if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
$('.form-is_rig').hide(); $('.form-is_rig').hide();
} }
{% else %}
//Initial hide
$('.form-hws').slideUp(dur);
{% endif %} {% endif %}
{% if not object.pk %} //Button handling
$('#is_rig-selector button').on('click', function () { $('#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) { if ($(this).data('is_rig') === 1) {
$('#{{form.is_rig.auto_id}}').prop('checked', true); $('#{{form.is_rig.auto_id}}').prop('checked', true);
if ($('.form-non_rig').is(':hidden')) { if ($('.form-non_rig').is(':hidden')) {
@@ -54,7 +56,6 @@
} }
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible'); $('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
} else { } else {
$('#{{form.is_rig.auto_id}}').prop('checked', false); $('#{{form.is_rig.auto_id}}').prop('checked', false);
$('.form-is_rig').slideUp(dur); $('.form-is_rig').slideUp(dur);
} }
@@ -68,18 +69,11 @@
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}) })
</script> </script>
<noscript>
<style>
.form-hws {
display: inherit !important;
}
</style>
</noscript>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'item_modal.html' %} {% include 'item_modal.html' %}
<form class=" itemised_form" role="form" method="POST"> <form class="itemised_form" role="form" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
{% load multiply from filters %}
{% load static %}
<!DOCTYPE document SYSTEM "rml.dtd"> <!DOCTYPE document SYSTEM "rml.dtd">
<document filename="{{filename}}"> <document filename="{{filename}}">
<docinit> <docinit>
<registerTTFont faceName="OpenSans" fileName="{{ fonts.opensans.regular }}"/> <registerTTFont faceName="OpenSans" fileName="static/fonts/OpenSans-Regular.tff"/>
<registerTTFont faceName="OpenSans-Bold" fileName="{{ fonts.opensans.bold }}"/> <registerTTFont faceName="OpenSans-Bold" fileName="static/fonts/OpenSans-Bold.tff"/>
<registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/> <registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/>
</docinit> </docinit>
@@ -82,11 +79,11 @@
<template > {# Note: page is 595x842 points (1 point=1/72in) #} <template > {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" > <pageTemplate id="Headed" >
<pageGraphics> <pageGraphics>
<image file="RIGS/static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/> <image file="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-bl.jpg" x="0" y="0" height="200" width="200"/>
{# logo positioned 42 from left, 33 from top #} {# 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"/> <setFont name="OpenSans-Bold" size="22.5" leading="10"/>
<drawString x="137" y="780">TEC PA &amp; Lighting</drawString> <drawString x="137" y="780">TEC PA &amp; Lighting</drawString>
@@ -110,8 +107,8 @@
<pageTemplate id="Main"> <pageTemplate id="Main">
<pageGraphics> <pageGraphics>
<image file="RIGS/static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/> <image file="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-bl.jpg" x="0" y="0" height="200" width="200"/>
<setFont name="OpenSans" size="10"/> <setFont name="OpenSans" size="10"/>
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString> <drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>

View File

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

View File

@@ -4,16 +4,14 @@
<p>Hi {{ to_name|default:"there" }},</p> <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" }} <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>{{ object.person.name }}</b>{% endif %}.</p> | {{ 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> <p>
Please find the link below to complete the event booking process. 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 Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
this this
email on. email on.
{% endif %}
</p> </p>
@@ -23,7 +21,7 @@
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td class="button" align="center"> <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 Complete Authorisation Form
</a> </a>
</td> </td>

View File

@@ -1,6 +1,6 @@
Hi {{ to_name|default:"there" }}, 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. Please find the link below to complete the event booking process.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #} {% if object.event.organisation and object.event.organisation.union_account %}{# internal #}

View File

@@ -1,9 +1,6 @@
{% extends 'eventauthorisation.html' %} {% extends 'eventauthorisation.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block js %}
{% endblock %}
{% block authorisation %} {% block authorisation %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
@@ -86,7 +83,7 @@
<div class="text-right"> <div class="text-right">
<div class="btn-group"> <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> </div>
</div> </div>

View File

@@ -30,14 +30,6 @@
{% render_field form.email type="email" class+="form-control" %} {% render_field form.email type="email" class+="form-control" %}
</div> </div>
</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> </form>
</div> </div>
</div> </div>
@@ -48,3 +40,14 @@
}); });
</script> </script>
{% endblock %} {% 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 %}

View File

@@ -21,7 +21,7 @@
<th scope="col">Event</th> <th scope="col">Event</th>
{# mmm hax #} {# mmm hax #}
{% if object_list.0 != None %} {% 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> <th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@@ -33,7 +33,7 @@
<tr class="{% if object.reviewed_by %}table-success{%endif%}"> <tr class="{% if object.reviewed_by %}table-success{%endif%}">
{# General #} {# General #}
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a></th> <th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a></th>
{% for field in fields %} {% for field in object_list.0.fieldz %}
<td>{{ object|get_field:field }}</td> <td>{{ object|get_field:field }}</td>
{% endfor %} {% endfor %}
{# Buttons #} {# Buttons #}

View File

@@ -2,10 +2,10 @@
{% load button from filters %} {% load button from filters %}
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="row py-4">
<div class="row justify-content-end py-3"> <div class="col-sm-12 text-right px-0">
<div class="col-sm-4 text-right"> <div class="btn-group">
<div class="btn-group btn-page"> <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"> <a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
<span class="fas fa-times"></span> <span <span class="fas fa-times"></span> <span
class="d-none d-sm-inline">Delete</span> class="d-none d-sm-inline">Delete</span>
@@ -17,24 +17,11 @@
{% button 'print' url='invoice_print' pk=object.pk %} {% button 'print' url='invoice_print' pk=object.pk %}
</div> </div>
</div> </div>
</div> <div>
<div class="row py-4">
<div class="row"> {% with object.event as object %}
<div class="col-sm-6"> <div class="col-sm-6">
<div class="card card-default"> {% include 'partials/contact_details.html' %}
<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>
<div class="col-sm-6"> <div class="col-sm-6">
{% include 'partials/event_details.html' %} {% include 'partials/event_details.html' %}
@@ -44,9 +31,9 @@
{% include 'partials/auth_details.html' %} {% include 'partials/auth_details.html' %}
</div> </div>
{% endif %} {% endif %}
</div> {% endwith %}
</div>
<div class="row py-4"> <div class="row py-4">
<div class="col-sm-6"> <div class="col-sm-6">
<div class="card card-default"> <div class="card card-default">
<div class="card-body"> <div class="card-body">
@@ -95,9 +82,8 @@
{% endwith %} {% endwith %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="invoice_history" %} {% include 'partials/last_edited.html' with target="invoice_history" %}
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -42,7 +42,7 @@
<td>{{ invoice.event.start_date }}</td> <td>{{ invoice.event.start_date }}</td>
<td>{{ invoice.invoice_date }}</td> <td>{{ invoice.invoice_date }}</td>
<td> <td>
{{ invoice.balance|floatformat:2 }} £{{ invoice.balance|floatformat:2 }}
{% if not invoice.event.internal %} {% if not invoice.event.internal %}
<br /> <br />
<span class="text-muted">{{ invoice.event.purchase_order }}</span> <span class="text-muted">{{ invoice.event.purchase_order }}</span>

View File

@@ -53,7 +53,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ event.sum_total|floatformat:2 }} £{{ event.sum_total|floatformat:2 }}
<br /> <br />
<span class="text-muted">{% if not event.internal %}{{ event.purchase_order }}{% endif %}</span> <span class="text-muted">{% if not event.internal %}{{ event.purchase_order }}{% endif %}</span>
</td> </td>

View File

@@ -1,15 +1,15 @@
<div class="card card-default <div class="card card-default
{% if object.authorised %} {% if event.authorised %}
card-success border-success
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %} {% 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 %} {% elif event.auth_request_to %}
card-info border-info
{% endif %} {% endif %}
"> ">
<div class="card-header">Client Authorisation</div> <div class="card-header">Client Authorisation</div>
<div class="card-body row"> <div class="card-body row">
<dl class="col-md-6"> <dl class="col-sm-6">
<dt>Authorisation Request</dt> <dt>Authorisation Request</dt>
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd> <dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
@@ -22,8 +22,8 @@
<dt>To</dt> <dt>To</dt>
<dd>{{ object.auth_request_to }}</dd> <dd>{{ object.auth_request_to }}</dd>
</dl> </dl>
<dd class="d-block d-sm-none">&nbsp;</dd> <dl class="col-sm-6">
<dl class="col-md-6"> <hr class="d-block d-sm-none">
<dt>Authorised</dt> <dt>Authorised</dt>
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd> <dd>{{ object.authorised|yesno:"Yes,No" }}</dd>

View File

@@ -1,27 +1,29 @@
<div class="col-sm-6"> <div class="col-sm-6">
{% if event.person %}
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header">Contact Details</div> <div class="card-header">Contact Details</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-5">Person</dt> <dt class="col-sm-5">Person</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{% if event.person %}
{{ event.person.name }} {{ event.person.name }}
{% endif %}
</dd> </dd>
{% if event.person.email %}
<dt class="col-sm-5">Email</dt> <dt class="col-sm-5">Email</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span class="overflow-ellipsis">{{ event.person.email }}</span> <span class="overflow-ellipsis">{{ event.person.email }}</span>
</dd> </dd>
{% endif %}
{% if event.person.phone %}
<dt class="col-sm-5">Phone Number</dt> <dt class="col-sm-5">Phone Number</dt>
<dd class="col-sm-7">{{ event.person.phone }}</dd> <dd class="col-sm-7">{{ event.person.phone }}</dd>
{% endif %}
</dl> </dl>
</div> </div>
</div> </div>
{% endif %}
{% if event.organisation %} {% if event.organisation %}
<div class="card mt-3"> <div class="card">
<div class="card-header">Organisation Details</div> <div class="card-header">Organisation Details</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
@@ -29,9 +31,10 @@
<dd class="col-sm-7"> <dd class="col-sm-7">
{{ event.organisation.name }} {{ event.organisation.name }}
</dd> </dd>
{% if event.organisation.phone %}
<dt class="col-sm-5">Phone Number</dt> <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> </dl>
</div> </div>
</div> </div>

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

View File

@@ -9,7 +9,7 @@
{% if event.internal %} {% if event.internal %}
<a class="btn item-add modal-href event-authorise-request <a class="btn item-add modal-href event-authorise-request
{% if event.authorised %} {% 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 %} {% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
btn-warning btn-warning
{% elif event.auth_request_to %} {% elif event.auth_request_to %}
@@ -19,7 +19,7 @@
{% endif %} {% endif %}
" "
href="{% url 'event_authorise_request' object.pk %}"> 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"> <span class="d-none d-sm-inline">
{% if event.authorised %} {% if event.authorised %}
Authorised Authorised
@@ -47,5 +47,7 @@
class="fas fa-pound-sign"></span> class="fas fa-pound-sign"></span>
<span class="d-none d-sm-inline">Invoice</span></a> <span class="d-none d-sm-inline">Invoice</span></a>
{% endif %} {% 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 %} {% endif %}
</div> </div>

View File

@@ -20,15 +20,7 @@
{% if event.is_rig %} {% if event.is_rig %}
<dt class="col-sm-6">Event MIC</dt> <dt class="col-sm-6">Event MIC</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">{% include 'partials/linked_name.html' with profile=event.mic %}</dd>
{% 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>
{% endif %} {% endif %}
<dt class="col-sm-6">Status</dt> <dt class="col-sm-6">Status</dt>
@@ -71,7 +63,7 @@
{% if event.dry_hire %} {% if event.dry_hire %}
<dt class="col-sm-6">Checked In By</dt> <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 %} {% endif %}
{% if event.is_rig %} {% if event.is_rig %}

View File

@@ -1,21 +1,25 @@
<h5> <div>
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span> <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.is_rig %}
{% if event.sum_total > 0 %}
{% if event.purchase_order %} {% if event.purchase_order %}
<span class="badge badge-success">PO: {{ event.purchase_order }}</span> <span class="badge badge-success">PO: {{ event.purchase_order }}</span>
{% elif event.authorised %} {% elif event.authorised %}
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span> <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 %} {% else %}
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span> <span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
{% endif %} {% endif %}
{% endif %}
{% if not event.dry_hire %} {% if not event.dry_hire %}
{% if event.riskassessment %} {% if event.riskassessment %}
<span class="badge badge-success">RA: <span class="fas fa-check"></span>{%if event.riskassessment.reviewed_by%}<span class="fas fa-check"></span>{%endif%}</span> <span class="badge badge-success">RA: <span class="fas fa-check"></span>{%if event.riskassessment.reviewed_by%}<span class="fas fa-check"></span>{%endif%}</span>
{% else %} {% else %}
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span> <span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
{% endif %} {% endif %}
{% else %}
<span class="badge badge-secondary">RA: N/A</span>
{% endif %} {% endif %}
{% if not event.dry_hire %} {% if not event.dry_hire %}
{% if event.hs_done %} {% if event.hs_done %}
@@ -24,8 +28,6 @@
{% else %} {% else %}
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span> <span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
{% endif %} {% endif %}
{% else %}
<span class="badge badge-secondary">Checklist: N/A</span>
{% endif %} {% endif %}
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
{% if event.invoice %} {% if event.invoice %}
@@ -41,4 +43,4 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</h5> </div>

View File

@@ -25,30 +25,30 @@
{% endif %} {% endif %}
{% else %} {% else %}
table-warning table-warning
{% endif %}" id="event_row"> {% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number--> <!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th> <th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times--> <!--Dates & Times-->
<td id="event_dates"> <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 %} {% if event.has_start_time %}
{{ event.start_time|date:"H:i" }} {{ event.start_time|date:"H:i" }}
{% endif %} {% endif %}</strong>
</span> </span>
{% if event.end_date %} {% if event.end_date %}
<br> <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 %} {% if event.has_end_time %}
{{ event.end_time|date:"H:i" }} {{ event.end_time|date:"H:i" }}
{% endif %} {% endif %}</strong>
</span> </span>
{% endif %} {% endif %}
{% if not event.cancelled %} {% if not event.cancelled %}
{% if event.meet_at %} {% 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 %} {% endif %}
{% if event.access_at %} {% 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 %}
{% endif %} {% endif %}
</td> </td>
@@ -67,9 +67,9 @@
</h4> </h4>
{% if event.is_rig and not event.cancelled %} {% if event.is_rig and not event.cancelled %}
<h5> <h5>
{{ event.person.name }} <a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %} {% if event.organisation %}
for {{ event.organisation.name }} for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %} {% endif %}
</h5> </h5>
{% endif %} {% endif %}
@@ -90,7 +90,7 @@
</a> </a>
{% endif %} {% endif %}
{% elif event.is_rig %} {% elif event.is_rig %}
<span class="fas fa-exclamation"></span> <span class="fas fa-user-slash"></span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

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

View File

@@ -2,9 +2,9 @@
{% load button from filters %} {% load button from filters %}
{% block content %} {% block content %}
<div class="row align-items-center justify-content-between py-2"> <div class="row align-items-center justify-content-between py-2 align-middle">
<div class="col-sm-12 col-md"> <div class="col-sm-12 col-md align-middle">
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> 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> </div>
{% if perms.RIGS.add_event %} {% if perms.RIGS.add_event %}
<div class="col text-right"> <div class="col text-right">

View File

@@ -1,5 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% 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 help_text from filters %}
{% load yesnoi from filters %} {% load yesnoi from filters %}
{% load linkornone from filters %} {% load linkornone from filters %}
@@ -7,7 +6,6 @@
{% block content %} {% block content %}
<div class="row py-3"> <div class="row py-3">
<div class="col-12"> <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 card-default mb-3">
<div class="card-header">General</div> <div class="card-header">General</div>
<div class="card-body"> <div class="card-body">
@@ -47,15 +45,15 @@
<dd class="col-sm-6"> <dd class="col-sm-6">
{{ object.big_power|yesnoi:'invert' }} {{ object.big_power|yesnoi:'invert' }}
</dd> </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"> <dd class="col-sm-6">
{{ object.power_mic.name|default:'None' }} {{ object.power_mic.name|default:'None' }}
</dd> </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"> <dd class="col-sm-6">
{{ object.outside|yesnoi:'invert' }} {{ object.outside|yesnoi:'invert' }}
</dd> </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"> <dd class="col-sm-6">
{{ object.generators|yesnoi:'invert' }} {{ object.generators|yesnoi:'invert' }}
</dd> </dd>
@@ -97,61 +95,67 @@
</dl> </dl>
</div> </div>
</div> </div>
<div class="row">
<div class="col-lg-6 col-12">
<div class="card card-default mb-3"> <div class="card card-default mb-3">
<div class="card-header">Site Details</div> <div class="card-header">Site Details</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-6">{{ object|help_text:'known_venue' }}</dt> <dt class="col-10">{{ object|help_text:'known_venue' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.known_venue|yesnoi:'invert' }} {{ object.known_venue|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'safe_loading'|safe }}</dt> <dt class="col-10">{{ object|help_text:'safe_loading'|safe }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.safe_loading|yesnoi:'invert' }} {{ object.safe_loading|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'safe_storage' }}</dt> <dt class="col-10">{{ object|help_text:'safe_storage' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.safe_storage|yesnoi:'invert' }} {{ object.safe_storage|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'area_outside_of_control' }}</dt> <dt class="col-10">{{ object|help_text:'area_outside_of_control' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.area_outside_of_control|yesnoi:'invert' }} {{ object.area_outside_of_control|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'barrier_required' }}</dt> <dt class="col-10">{{ object|help_text:'barrier_required' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.barrier_required|yesnoi:'invert' }} {{ object.barrier_required|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt> <dt class="col-10">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }} {{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
</dd> </dd>
</dl> </dl>
</div> </div>
</div> </div>
</div>
<div class="col-lg-6 col-12">
<div class="card card-default mb-3"> <div class="card card-default mb-3">
<div class="card-header">Structures</div> <div class="card-header">Structures</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-6">{{ object|help_text:'special_structures' }}</dt> <dt class="col-10">{{ object|help_text:'special_structures' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.special_structures|yesnoi:'invert' }} {{ object.special_structures|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'suspended_structures' }}</dt> <dt class="col-10">{{ object|help_text:'suspended_structures' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.suspended_structures|yesnoi:'invert' }} {{ object.suspended_structures|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'persons_responsible_structures' }}</dt> <dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
<dd class="col-sm-6"> <dd class="col-12">
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }} {{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
</dd> </dd>
<dt class="col-6">{{ object|help_text:'rigging_plan'|safe }}</dt> <dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
<dd class="col-6"> <dd class="col-12">
{{ object.rigging_plan|linkornone }} {{ object.rigging_plan|linkornone|default:'N/A' }}
</dd> </dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<div class="col-12 text-right"> <div class="col-12 text-right">
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span <a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a> class="d-none d-sm-inline">Edit</span></a>

View File

@@ -6,23 +6,16 @@
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/> <link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/bootstrap-select.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }} {{ 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/autocompleter.js' %}"></script>
<script> <script>

View File

@@ -4,7 +4,7 @@ from django.forms.forms import NON_FIELD_ERRORS
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django.template.defaultfilters import yesno, title, truncatewords 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.html import escape
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
from django.utils.text import normalize_newlines from django.utils.text import normalize_newlines
@@ -173,7 +173,7 @@ def title_spaced(string):
@register.filter(needs_autoescape=True) @register.filter(needs_autoescape=True)
def namewithnotes(obj, url, autoescape=True): def namewithnotes(obj, url, autoescape=True):
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0: 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: else:
return obj.name return obj.name
@@ -217,3 +217,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
elif type == 'submit': elif type == 'submit':
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style} 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} 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())

108
RIGS/tests/conftest.py Normal file
View 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()

View File

@@ -52,8 +52,8 @@ class EventDetail(BasePage):
URL_TEMPLATE = 'event/{event_id}' URL_TEMPLATE = 'event/{event_id}'
# TODO Refactor into regions to match template fragmentation # TODO Refactor into regions to match template fragmentation
_event_name_selector = (By.XPATH, '//h1') _event_name_selector = (By.XPATH, '//h2')
_person_panel_selector = (By.XPATH, '//div[contains(text(), "Contact Details")]/..') _person_panel_selector = (By.XPATH, '//div[contains(text(), "Person Details")]/..')
_name_selector = (By.XPATH, '//dt[text()="Person"]/following-sibling::dd[1]') _name_selector = (By.XPATH, '//dt[text()="Person"]/following-sibling::dd[1]')
_email_selector = (By.XPATH, '//dt[text()="Email"]/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]') _phone_selector = (By.XPATH, '//dt[text()="Phone Number"]/following-sibling::dd[1]')
@@ -230,9 +230,11 @@ class CreateEventChecklist(FormPage):
URL_TEMPLATE = 'event/{event_id}/checklist' URL_TEMPLATE = 'event/{event_id}/checklist'
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]") _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_vehicle_locator = (By.XPATH, "//button[contains(., 'Vehicle')]")
_add_crew_locator = (By.XPATH, "//button[contains(., 'Crew')]") _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 = { form_items = {
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')), 'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
@@ -271,11 +273,61 @@ class CreateEventChecklist(FormPage):
def power_mic(self): def power_mic(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector)) 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 @property
def success(self): def success(self):
return '{event_id}' not in self.driver.current_url 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): class GenericList(BasePage):
_search_selector = (By.CSS_SELECTOR, 'div.input-group:nth-child(2) > input:nth-child(1)') _search_selector = (By.CSS_SELECTOR, 'div.input-group:nth-child(2) > input:nth-child(1)')
_search_go_selector = (By.ID, 'id_search') _search_go_selector = (By.ID, 'id_search')

View File

@@ -8,54 +8,12 @@ from django.http import HttpResponseBadRequest
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
import PyRIGS.tests.base
from RIGS import models from RIGS import models
from pytest_django.asserts import assertContains, assertNotContains from pytest_django.asserts import assertContains, assertNotContains, assertFormError
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.")
def setup_event(): 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') venue = models.Venue.objects.create(name='Authorisation Test Venue')
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test') 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) 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 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): def test_requires_valid_hmac(client, admin_user):
event = setup_event() event = setup_event()
auth_data, hmac, url = setup_mail(event, admin_user) 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') assertContains(response, 'amount has changed')
@pytest.mark.django_db(transaction=True) @pytest.mark.django_db
def test_email_sent(admin_client, admin_user, mailoutbox): def test_email_sent(admin_client, admin_user, mailoutbox):
event = setup_event() event = setup_event()
auth_data, hmac, url = setup_mail(event, admin_user) 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] assert mailoutbox[1].to == [settings.AUTHORISATION_NOTIFICATION_ADDRESS]
class TECEventAuthorisationTest(BaseCase): def test_email_check(admin_client, admin_user):
def setUp(self): event = setup_event()
super().setUp() url = reverse('event_authorise_request', kwargs={'pk': event.pk})
self.url = reverse('event_authorise_request', kwargs={'pk': self.event.pk}) admin_user.email = 'teccie@someotherdomain.com'
admin_user.save()
def test_email_check(self): response = admin_client.post(url)
self.profile.email = 'teccie@someotherdomain.com'
self.profile.save()
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): def test_request_send(admin_client, admin_user):
self.profile.email = 'teccie@nottinghamtec.co.uk' event = setup_event()
self.profile.save() url = reverse('event_authorise_request', kwargs={'pk': event.pk})
response = self.client.post(self.url) admin_user.email = 'teccie@nottinghamtec.co.uk'
self.assertContains(response, 'This field is required.') 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'}) response = admin_client.post(url, {'email': 'client@functional.test'})
self.assertEqual(response.status_code, 302) assert response.status_code == 302
self.assertEqual(len(mail.outbox), 1) assert len(mail.outbox) == 1
email = mail.outbox[0] email = mail.outbox[0]
self.assertIn('client@functional.test', email.to) assert 'client@functional.test' in email.to
self.assertIn('/event/%d/' % (self.event.pk), email.body) assert '/event/%d/' % event.pk in email.body
# Check sent by details are populated # Check sent by details are populated
self.event.refresh_from_db() event.refresh_from_db()
self.assertEqual(self.event.auth_request_by, self.profile) assert event.auth_request_by == admin_user
self.assertEqual(self.event.auth_request_to, 'client@functional.test') assert event.auth_request_to == 'client@functional.test'
self.assertIsNotNone(self.event.auth_request_at) assert event.auth_request_at is not None

View File

@@ -15,6 +15,11 @@ from PyRIGS.tests.pages import animation_is_finished
from RIGS import models from RIGS import models
from RIGS.tests import regions from RIGS.tests import regions
from . import pages from . import pages
import pytest
import time as t
pytestmark = pytest.mark.django_db(transaction=True)
@screenshot_failure_cls @screenshot_failure_cls
@@ -307,13 +312,13 @@ class TestEventDuplicate(BaseRigboardTest):
# TODO Rewrite when EventDetail page is implemented # TODO Rewrite when EventDetail page is implemented
newEvent = models.Event.objects.latest('pk') 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_by, None)
self.assertEqual(newEvent.auth_request_at, None) self.assertEqual(newEvent.auth_request_at, None)
self.assertFalse(newEvent.authorised) 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 self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible
# Check the new items are 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.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.name, self.page.name)
self.assertEqual(self.client.email, self.page.email) self.assertEqual(self.client.email, self.page.email)
self.assertEqual(self.client.phone, None) assert self.client.phone == ''
@screenshot_failure_cls @screenshot_failure_cls
@@ -633,270 +638,190 @@ class TestCalendar(BaseRigboardTest):
else: else:
self.assertNotContains(response, "TE E" + str(test) + " ") 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() def test_calendar_buttons(logged_in_browser, live_server): # If FullCalendar fails to load for whatever reason, the buttons don't work
self.assertIn(timezone.now().strftime("%Y-%m"), self.driver.current_url) page = pages.CalendarPage(logged_in_browser.driver, live_server.url).open()
assert timezone.now().strftime("%Y-%m") in logged_in_browser.url
target_date = datetime.date(2020, 1, 1) target_date = datetime.date(2020, 1, 1)
self.page.target_date.set_value(target_date) page.target_date.set_value(target_date)
self.page.go() page.go()
self.assertIn(self.page.target_date.value.strftime("%Y-%m"), self.driver.current_url) assert page.target_date.value.strftime("%Y-%m") in logged_in_browser.url
self.page.next() page.next()
target_date += datetime.timedelta(days=32) target_date += datetime.timedelta(days=32)
self.assertIn(target_date.strftime("%m"), self.driver.current_url) assert target_date.strftime("%m") in logged_in_browser.url
@screenshot_failure_cls def test_ra_edit(logged_in_browser, live_server, ra):
class TestHealthAndSafety(BaseRigboardTest): page = pages.EditRiskAssessment(logged_in_browser.driver, live_server.url, pk=ra.pk).open()
def setUp(self): page.nonstandard_equipment = nse = True
super().setUp() page.general_notes = gn = "There are some notes, but I've not written them here as that would be helpful"
self.profile = models.Profile.objects.get_or_create( page.submit()
first_name='Test', assert not page.success
last_name='TEC User', page.supervisor_consulted = True
username='eventtest', page.submit()
email='teccie@functional.test', assert page.success
is_superuser=True # lazily grant all permissions # Check that data is right
)[0] ra = models.RiskAssessment.objects.get(pk=ra.pk)
self.venue = models.Venue.objects.create(name="Venue 1") 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 small_ec(page, admin_user):
def test_ra_creation(self): page.safe_parking = True
self.page = pages.CreateRiskAssessment(self.driver, self.live_server_url, event_id=self.testEvent.pk).open() 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
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
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()
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
# Gotta scroll to make the button clickable
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
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 = 666
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 = 333
page.submit()
assert page.success
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
# 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
# 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()
# Check there are no defaults # Check there are no defaults
self.assertIsNone(self.page.nonstandard_equipment) assert page.nonstandard_equipment is None
# No database side validation, only HTML5. # No database side validation, only HTML5.
page.nonstandard_equipment = False
self.page.nonstandard_equipment = False page.nonstandard_use = False
self.page.nonstandard_use = False page.contractors = False
self.page.contractors = False page.other_companies = False
self.page.other_companies = False page.crew_fatigue = False
self.page.crew_fatigue = False page.general_notes = "There are no notes."
self.page.general_notes = "There are no notes." page.big_power = False
self.page.big_power = False page.outside = False
self.page.outside = False page.power_mic.search(admin_user.first_name)
self.page.power_mic.search(self.profile.name) page.generators = False
self.page.power_mic.set_option(self.profile.name, True) page.other_companies_power = False
# TODO This should not be necessary, normally closes automatically page.nonstandard_equipment_power = False
self.page.power_mic.toggle() page.multiple_electrical_environments = False
self.assertFalse(self.page.power_mic.is_open) page.power_notes = "Remember to bring some power"
self.page.generators = False page.noise_monitoring = False
self.page.other_companies_power = False page.sound_notes = "Loud, but not too loud"
self.page.nonstandard_equipment_power = False page.known_venue = False
self.page.multiple_electrical_environments = False page.safe_loading = False
self.page.power_notes = "Remember to bring some power" page.safe_storage = False
self.page.noise_monitoring = False page.area_outside_of_control = False
self.page.sound_notes = "Loud, but not too loud" page.barrier_required = False
self.page.known_venue = False page.nonstandard_emergency_procedure = False
self.page.safe_loading = False page.special_structures = 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.persons_responsible_structures = "Nobody and her cat, She"
self.page.suspended_structures = True page.suspended_structures = True
# TODO Test for this proper # TODO Test for this proper
self.page.rigging_plan = "https://nottinghamtec.sharepoint.com/test/" page.rigging_plan = "https://nottinghamtec.sharepoint.com/test/"
self.page.submit() page.submit()
self.assertFalse(self.page.success) assert not page.success
self.page.suspended_structures = False page.suspended_structures = False
self.page.submit() page.submit()
self.assertTrue(self.page.success) assert page.success
def test_ra_no_duplicates(logged_in_browser, live_server, ra):
# Test that we can't make another one # Test that we can't make another one
self.page = pages.CreateRiskAssessment(self.driver, self.live_server_url, event_id=self.testEvent.pk).open() page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=ra.event.pk).open()
self.assertIn('edit', self.driver.current_url) assert 'edit' in logged_in_browser.url
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)
def test_ec_create_small(self):
self.page = pages.CreateEventChecklist(self.driver, self.live_server_url, event_id=self.testEvent2.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 = "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);")
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)
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)
# Gotta scroll to make the button clickable
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
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
self.page.submit()
self.assertTrue(self.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()
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)

View File

@@ -2,6 +2,7 @@ from datetime import date, timedelta, datetime, time
from decimal import * from decimal import *
import pytz import pytz
import pytest
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from reversion import revisions as reversion from reversion import revisions as reversion
@@ -9,110 +10,56 @@ from reversion import revisions as reversion
from RIGS import models from RIGS import models
class ProfileTestCase(TestCase): def assert_decimal_equality(d1, d2):
def test_str(self): assert float(d1) == pytest.approx(float(d2))
def test_str():
profile = models.Profile(first_name='Test', last_name='Case') profile = models.Profile(first_name='Test', last_name='Case')
self.assertEqual(str(profile), 'Test Case') assert str(profile) == 'Test Case'
profile.initials = 'TC' profile.initials = 'TC'
self.assertEqual(str(profile), 'Test Case "TC"') assert str(profile) == 'Test Case "TC"'
class VatRateTestCase(TestCase): @pytest.mark.django_db
@classmethod def test_find_correct(vat_rate):
def setUpTestData(cls): new_rate = models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2')
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') r = models.VatRate.objects.find_rate('2015-03-01')
self.assertEqual(r, self.rates[0]) assert_decimal_equality(r.rate, vat_rate.rate)
r = models.VatRate.objects.find_rate('2016-03-01') r = models.VatRate.objects.find_rate('2016-03-01')
self.assertEqual(r, self.rates[1]) assert_decimal_equality(r.rate, new_rate.rate)
def test_percent_correct(self):
self.assertEqual(self.rates[0].as_percent, 20)
class EventTestCase(TestCase): def test_percent_correct(vat_rate):
@classmethod assert vat_rate.as_percent == 20
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)
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 = { def test_related_vatrate(basic_event, vat_rate):
# produce 7 normal events - 5 current assert_decimal_equality(vat_rate.rate, basic_event.vat_rate.rate)
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 class EventTest():
10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, def test_count(many_events):
description="dryhire today"), # Sanity check we have the expected events created
11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, assert models.Event.objects.count() == 18
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"),
# 4 non rig - 3 current def test_rig_count(many_events):
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")
def test_rig_count(self):
# Changed to not include unreturned dry hires in rig count # 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() current_events = models.Event.objects.current_events()
self.assertEqual(len(current_events), len(self.current_events)) assert len(current_events) == len(self.current_events)
for eid in self.current_events: for eid in current_events:
self.assertIn(models.Event.objects.get(name="TE E%d" % eid), current_events) assert models.Event.objects.get(name="TE E%d" % eid) in current_events
for eid in self.not_current_events: for eid in not_current_events:
self.assertNotIn(models.Event.objects.get(name="TE E%d" % eid), 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") v1 = models.Venue.objects.create(name="TE V1")
v2 = models.Venue.objects.create(name="TE V2") v2 = models.Venue.objects.create(name="TE V2")
@@ -127,16 +74,13 @@ class EventTestCase(TestCase):
e2.append(event) e2.append(event)
event.save() event.save()
self.assertCountEqual(e1, v1.latest_events) assert set(e1) == set(v1.latest_events)
self.assertCountEqual(e2, v2.latest_events) assert set(e2) == set(v2.latest_events)
# Cleanup
v1.delete()
v2.delete()
for (key, event) in self.events.items(): def test_related_person(many_events):
event.venue = None
def test_related_vatrate(self):
self.assertEqual(self.vatrate, models.Event.objects.all()[0].vat_rate)
def test_related_person(self):
p1 = models.Person.objects.create(name="TE P1") p1 = models.Person.objects.create(name="TE P1")
p2 = models.Person.objects.create(name="TE P2") p2 = models.Person.objects.create(name="TE P2")
@@ -151,13 +95,13 @@ class EventTestCase(TestCase):
e2.append(event) e2.append(event)
event.save() event.save()
self.assertCountEqual(e1, p1.latest_events) assert set(e1) == set(p1.latest_events)
self.assertCountEqual(e2, p2.latest_events) assert set(e2) == set(p2.latest_events)
for (key, event) in self.events.items(): p1.delete()
event.person = None p2.delete()
def test_related_organisation(self): def test_related_organisation(many_events):
o1 = models.Organisation.objects.create(name="TE O1") o1 = models.Organisation.objects.create(name="TE O1")
o2 = models.Organisation.objects.create(name="TE O2") o2 = models.Organisation.objects.create(name="TE O2")
@@ -172,13 +116,13 @@ class EventTestCase(TestCase):
e2.append(event) e2.append(event)
event.save() event.save()
self.assertCountEqual(e1, o1.latest_events) assert set(e1) == set(o1.latest_events)
self.assertCountEqual(e2, o2.latest_events) assert set(e1) == set(o2.latest_events)
for (key, event) in self.events.items(): for (key, event) in self.events.items():
event.organisation = None event.organisation = None
def test_organisation_person_join(self): def test_organisation_person_join(many_events):
p1 = models.Person.objects.create(name="TE P1") p1 = models.Person.objects.create(name="TE P1")
p2 = models.Person.objects.create(name="TE P2") p2 = models.Person.objects.create(name="TE P2")
o1 = models.Organisation.objects.create(name="TE O1") o1 = models.Organisation.objects.create(name="TE O1")
@@ -202,74 +146,77 @@ class EventTestCase(TestCase):
events = models.Event.objects.all() events = models.Event.objects.all()
# Check person's organisations # Check person's organisations
self.assertIn((o1, 2), p1.organisations) assert (o1, 2) in p1.organisations
self.assertIn((o2, 1), p1.organisations) assert (o2, 1) in p1.organisations
self.assertIn((o1, 2), p2.organisations) assert (o1, 2) in p2.organisations
self.assertEqual(len(p2.organisations), 1) assert len(p2.organisations) == 1
# Check organisation's persons # Check organisation's persons
self.assertIn((p1, 2), o1.persons) assert (p1, 2) in o1.persons
self.assertIn((p2, 2), o1.persons) assert (p2, 2) in o1.persons
self.assertIn((p1, 1), o2.persons) assert (p1, 1) in o2.persons
self.assertEqual(len(o2.persons), 1) assert len(o2.persons) == 1
def test_cancelled_property(self): def test_cancelled_property(many_events):
edit = self.events[1] edit = many_events[1]
edit.status = models.Event.CANCELLED edit.status = models.Event.CANCELLED
edit.save() edit.save()
event = models.Event.objects.get(pk=edit.pk) event = models.Event.objects.get(pk=edit.pk)
self.assertEqual(event.status, models.Event.CANCELLED) assert event.status == models.Event.CANCELLED
self.assertTrue(event.cancelled) assert event.cancelled
event.status = models.Event.PROVISIONAL event.status = models.Event.PROVISIONAL
event.save() event.save()
def test_confirmed_property(self): def test_confirmed_property(many_events):
edit = self.events[1] edit = many_events[1]
edit.status = models.Event.CONFIRMED edit.status = models.Event.CONFIRMED
edit.save() edit.save()
event = models.Event.objects.get(pk=edit.pk) event = models.Event.objects.get(pk=edit.pk)
self.assertEqual(event.status, models.Event.CONFIRMED) assert event.status == models.Event.CONFIRMED
self.assertTrue(event.confirmed) assert event.confirmed
event.status = models.Event.PROVISIONAL event.status = models.Event.PROVISIONAL
event.save() event.save()
def test_earliest_time(self):
def test_earliest_time():
event = models.Event(name="TE ET", start_date=date(2016, 0o1, 0o1)) event = models.Event(name="TE ET", start_date=date(2016, 0o1, 0o1))
# Just a start date # Just a start date
self.assertEqual(event.earliest_time, date(2016, 0o1, 0o1)) assert event.earliest_time == date(2016, 0o1, 0o1)
# With start time # With start time
event.start_time = time(9, 00) event.start_time = time(9, 00)
self.assertEqual(event.earliest_time, self.create_datetime(2016, 1, 1, 9, 00)) assert event.earliest_time == create_datetime(2016, 1, 1, 9, 00)
# With access time # With access time
event.access_at = self.create_datetime(2015, 12, 0o3, 9, 57) event.access_at = create_datetime(2015, 12, 0o3, 9, 57)
self.assertEqual(event.earliest_time, event.access_at) assert event.earliest_time == event.access_at
# With meet time # With meet time
event.meet_at = self.create_datetime(2015, 12, 0o3, 9, 55) event.meet_at = create_datetime(2015, 12, 0o3, 9, 55)
self.assertEqual(event.earliest_time, event.meet_at) assert event.earliest_time == event.meet_at
# Check order isn't important # Check order isn't important
event.start_date = date(2015, 12, 0o3) event.start_date = date(2015, 12, 0o3)
self.assertEqual(event.earliest_time, self.create_datetime(2015, 12, 0o3, 9, 00)) assert event.earliest_time == create_datetime(2015, 12, 0o3, 9, 00)
def test_latest_time(self):
def test_latest_time():
event = models.Event(name="TE LT", start_date=date(2016, 0o1, 0o1)) event = models.Event(name="TE LT", start_date=date(2016, 0o1, 0o1))
# Just start date # Just start date
self.assertEqual(event.latest_time, event.start_date) assert event.latest_time == event.start_date
# Just end date # Just end date
event.end_date = date(2016, 1, 2) event.end_date = date(2016, 1, 2)
self.assertEqual(event.latest_time, event.end_date) assert event.latest_time == event.end_date
# With end time # With end time
event.end_time = time(23, 00) event.end_time = time(23, 00)
self.assertEqual(event.latest_time, self.create_datetime(2016, 1, 2, 23, 00)) assert event.latest_time == create_datetime(2016, 1, 2, 23, 00)
def test_in_bounds(self):
def test_in_bounds():
manager = models.Event.objects manager = models.Event.objects
events = [ events = [
manager.create(name="TE IB0", start_date='2016-01-02'), # yes no manager.create(name="TE IB0", start_date='2016-01-02'), # yes no
@@ -279,28 +226,29 @@ class EventTestCase(TestCase):
manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'), 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 IB3', start_date='2015-12-31', end_date='2016-01-03'),
manager.create(name='TE IB4', start_date='2016-01-04', manager.create(name='TE IB4', start_date='2016-01-04',
access_at=self.create_datetime(2016, 0o1, 0o3, 00, 00)), access_at=create_datetime(2016, 0o1, 0o3, 00, 00)),
manager.create(name='TE IB5', start_date='2016-01-04', manager.create(name='TE IB5', start_date='2016-01-04',
meet_at=self.create_datetime(2016, 0o1, 0o2, 00, 00)), meet_at=create_datetime(2016, 0o1, 0o2, 00, 00)),
# negative check # negative check
manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'), 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), in_bounds = manager.events_in_bounds(create_datetime(2016, 1, 2, 0, 0),
self.create_datetime(2016, 1, 3, 0, 0)) create_datetime(2016, 1, 3, 0, 0))
self.assertIn(events[0], in_bounds) assert events[0] in in_bounds
self.assertIn(events[1], in_bounds) assert events[1], in_bounds
self.assertIn(events[2], in_bounds) assert events[2], in_bounds
self.assertIn(events[3], in_bounds) assert events[3], in_bounds
self.assertIn(events[4], in_bounds) assert events[4], in_bounds
self.assertIn(events[5], in_bounds) assert events[5], in_bounds
self.assertNotIn(events[6], in_bounds) assert events[6] not in in_bounds
def create_datetime(self, year, month, day, hour, min):
def create_datetime(year, month, day, hour, minute):
tz = pytz.timezone(settings.TIME_ZONE) tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, min)) return tz.localize(datetime(year, month, day, hour, minute))
class EventItemTestCase(TestCase): class EventItemTestCase(TestCase):
@@ -331,7 +279,6 @@ class EventItemTestCase(TestCase):
class EventPricingTestCase(TestCase): class EventPricingTestCase(TestCase):
def setUp(self): 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)) 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.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()) self.e2 = models.Event.objects.create(name="TP E2", start_date=date.today())
@@ -364,7 +311,6 @@ class EventPricingTestCase(TestCase):
class EventAuthorisationTestCase(TestCase): class EventAuthorisationTestCase(TestCase):
def setUp(self): 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( self.profile = models.Profile.objects.get_or_create(
first_name='Test', first_name='Test',
last_name='TEC User', last_name='TEC User',

View File

@@ -1,23 +1,25 @@
from datetime import date from datetime import date
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.management import call_command
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains 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 from RIGS import models
import pytest
pytestmark = pytest.mark.django_db
class TestAdminMergeObjects(TestCase): class TestAdminMergeObjects(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True) is_active=True, is_staff=True)
cls.persons = { cls.persons = {
1: models.Person.objects.create(name="Person 1"), 1: models.Person.objects.create(name="Person 1"),
2: models.Person.objects.create(name="Person 2"), 2: models.Person.objects.create(name="Person 2"),
@@ -168,9 +170,6 @@ class TestInvoiceDelete(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=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 = { cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today()), 1: models.Event.objects.create(name="TE E1", start_date=date.today()),
2: models.Event.objects.create(name="TE E2", start_date=date.today()) 2: models.Event.objects.create(name="TE E2", start_date=date.today())
@@ -201,7 +200,7 @@ class TestInvoiceDelete(TestCase):
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[2].pk)) self.assertTrue(models.Invoice.objects.get(pk=self.invoices[2].pk))
# Actually delete it # Actually delete it
response = self.client.post(request_url, follow=True) self.client.post(request_url, follow=True)
# Check the invoice is deleted # Check the invoice is deleted
self.assertRaises(ObjectDoesNotExist, models.Invoice.objects.get, pk=self.invoices[2].pk) self.assertRaises(ObjectDoesNotExist, models.Invoice.objects.get, pk=self.invoices[2].pk)
@@ -216,7 +215,7 @@ class TestInvoiceDelete(TestCase):
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk)) self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
# Try to actually delete it # 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 # Check this didn't work
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk)) self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
@@ -227,9 +226,6 @@ class TestPrintPaperwork(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=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 = { cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today(), 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."), description="This is an event description\nthat for a very specific reason spans two lines."),
@@ -257,102 +253,50 @@ class TestPrintPaperwork(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class TestEmbeddedViews(TestCase): def test_login_redirect(client, django_user_model):
@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.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())
}
cls.invoices = {
1: models.Invoice.objects.create(event=cls.events[1]),
2: models.Invoice.objects.create(event=cls.events[2])
}
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}) request_url = reverse('event_embed', kwargs={'pk': 1})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url) expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
# Request the page and check it redirects # Request the page and check it redirects
response = self.client.get(request_url, follow=True) response = client.get(request_url, follow=True)
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200) assertRedirects(response, expected_url, status_code=302, target_status_code=200)
# Now login # Now login
self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) login(client, django_user_model)
# And check that it no longer redirects # And check that it no longer redirects
response = self.client.get(request_url, follow=True) response = client.get(request_url, follow=True)
self.assertEqual(len(response.redirect_chain), 0) assert len(response.redirect_chain) == 0
def testLoginCookieWarning(self):
def test_login_cookie_warning(client):
login_url = reverse('login_embed') login_url = reverse('login_embed')
response = self.client.post(login_url, follow=True) response = client.post(login_url, follow=True)
self.assertContains(response, "Cookies do not seem to be enabled") assertContains(response, "Cookies do not seem to be enabled")
def testXFrameHeaders(self):
event_url = reverse('event_embed', kwargs={'pk': 1}) def test_xframe_headers(admin_client, basic_event):
event_url = reverse('event_embed', kwargs={'pk': basic_event.pk})
login_url = reverse('login_embed') login_url = reverse('login_embed')
self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) response = admin_client.get(event_url, follow=True)
with pytest.raises(KeyError):
response = self.client.get(event_url, follow=True)
with self.assertRaises(KeyError):
response._headers["X-Frame-Options"] response._headers["X-Frame-Options"]
response = self.client.get(login_url, follow=True) response = admin_client.get(login_url, follow=True)
with self.assertRaises(KeyError): with pytest.raises(KeyError):
response._headers["X-Frame-Options"] response._headers["X-Frame-Options"]
def testOEmbed(self):
event_url = reverse('event_detail', kwargs={'pk': 1}) def test_oembed(client, basic_event):
event_embed_url = reverse('event_embed', kwargs={'pk': 1}) event_url = reverse('event_detail', kwargs={'pk': basic_event.pk})
oembed_url = reverse('event_oembed', kwargs={'pk': 1}) 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_oembed_url = reverse('event_oembed', kwargs={'pk': 999})
alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999}) alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999})
# Test the meta tag is in place assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url)
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)
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')
# 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
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
def search(client, url, found, notfound, arguments): def search(client, url, found, notfound, arguments):
@@ -391,45 +335,11 @@ def test_search(admin_client):
['name', 'id', 'address']) ['name', 'id', 'address'])
def setup_for_hs(): def test_hs_list(admin_client, basic_event):
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()
request_url = reverse('hs_list') request_url = reverse('hs_list')
response = admin_client.get(request_url, follow=True) response = admin_client.get(request_url, follow=True)
assertContains(response, events[1].name) assertContains(response, basic_event.name)
assertContains(response, events[2].name) # assertContains(response, events[2].name)
assertContains(response, 'Create') assertContains(response, 'Create')
@@ -439,19 +349,18 @@ def review(client, profile, obj, request_url):
obj.refresh_from_db() obj.refresh_from_db()
assertContains(response, 'Reviewed by') assertContains(response, 'Reviewed by')
assertContains(response, profile.name) 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): def test_ra_review(admin_client, admin_user, ra):
review(admin_client, admin_user, create_ra(admin_user), 'ra_review') review(admin_client, admin_user, ra, 'ra_review')
def test_checklist_review(admin_client, admin_user): def test_checklist_review(admin_client, admin_user, checklist):
review(admin_client, admin_user, create_checklist(admin_user), 'ec_review') review(admin_client, admin_user, checklist, 'ec_review')
def test_ra_redirect(admin_client, admin_user): def test_ra_redirect(admin_client, admin_user, ra):
ra = create_ra(admin_user)
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk}) request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk}) expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})

View File

@@ -1,5 +1,4 @@
from django.contrib.auth.decorators import login_required 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.urls import path, re_path
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import RedirectView from django.views.generic import RedirectView
@@ -62,7 +61,7 @@ urlpatterns = [
path('event/<int:pk>/embed/', path('event/<int:pk>/embed/',
xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())), xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
name='event_embed'), 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'), name='event_oembed'),
path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()), path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
name='event_print'), name='event_print'),
@@ -133,6 +132,8 @@ urlpatterns = [
name='event_authorise_preview'), name='event_authorise_preview'),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(), re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
name='event_authorise'), 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 # ICS Calendar - API key authentication
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),

View File

@@ -51,7 +51,7 @@
"url": "heroku/nodejs" "url": "heroku/nodejs"
}, },
{ {
"url": "heroku/python" "url": "https://github.com/nottinghamtec/heroku-buildpack-python"
} }
] ]
} }

View File

@@ -0,0 +1 @@
default_app_config = 'assets.apps.AssetsAppConfig'

8
assets/apps.py Normal file
View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class AssetsAppConfig(AppConfig):
name = 'assets'
def ready(self):
import assets.signals

View File

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

View File

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

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

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

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

View File

@@ -2,8 +2,6 @@ import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models, connection 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 django.urls import reverse
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
@@ -12,46 +10,46 @@ from RIGS.models import RevisionMixin, Profile
class AssetCategory(models.Model): class AssetCategory(models.Model):
name = models.CharField(max_length=80)
class Meta: class Meta:
verbose_name = 'Asset Category' verbose_name = 'Asset Category'
verbose_name_plural = 'Asset Categories' verbose_name_plural = 'Asset Categories'
ordering = ['name'] ordering = ['name']
name = models.CharField(max_length=80)
def __str__(self): def __str__(self):
return self.name return self.name
class AssetStatus(models.Model): 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: class Meta:
verbose_name = 'Asset Status' verbose_name = 'Asset Status'
verbose_name_plural = 'Asset Statuses' verbose_name_plural = 'Asset Statuses'
ordering = ['name'] 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): def __str__(self):
return self.name return self.name
@reversion.register @reversion.register
class Supplier(models.Model, RevisionMixin): 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: class Meta:
ordering = ['name'] 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): def get_absolute_url(self):
return reverse('supplier_list') return reverse('supplier_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return self.name return self.name
@@ -67,17 +65,16 @@ class Connector(models.Model):
return self.description return self.description
# Things are nullable that shouldn't be because I didn't properly fix the data structure when moving this to its own model...
class CableType(models.Model): class CableType(models.Model):
class Meta:
ordering = ['plug', 'socket', '-circuits']
circuits = models.IntegerField(default=1) circuits = models.IntegerField(default=1)
cores = models.IntegerField(default=3) cores = models.IntegerField(default=3)
plug = models.ForeignKey(Connector, on_delete=models.CASCADE, plug = models.ForeignKey(Connector, on_delete=models.CASCADE,
related_name='plug', null=True) related_name='plug')
socket = models.ForeignKey(Connector, on_delete=models.CASCADE, 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): def __str__(self):
if self.plug and self.socket: if self.plug and self.socket:
@@ -85,15 +82,31 @@ class CableType(models.Model):
else: else:
return "Unknown" 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 @reversion.register
class Asset(models.Model, RevisionMixin): 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', parent = models.ForeignKey(to='self', related_name='asset_parent',
blank=True, null=True, on_delete=models.SET_NULL) blank=True, null=True, on_delete=models.SET_NULL)
asset_id = models.CharField(max_length=15, unique=True) asset_id = models.CharField(max_length=15, unique=True)
@@ -127,32 +140,18 @@ class Asset(models.Model, RevisionMixin):
reversion_perm = 'assets.asset_finance' reversion_perm = 'assets.asset_finance'
def get_available_asset_id(wanted_prefix=""): class Meta:
sql = """ ordering = ['asset_id_prefix', 'asset_id_number']
SELECT a.asset_id_number+1 permissions = [
FROM assets_asset a ('asset_finance', 'Can see financial data for assets')
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) def __str__(self):
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s; return "{} | {}".format(self.asset_id, self.description)
"""
with connection.cursor() as cursor:
cursor.execute(sql, [9000, wanted_prefix])
row = cursor.fetchone()
if row is None or row[0] is None:
return 9000
else:
return row[0]
def get_absolute_url(self): def get_absolute_url(self):
return reverse('asset_detail', kwargs={'pk': self.asset_id}) 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): def clean(self):
errdict = {} errdict = {}
if self.date_sold and self.date_acquired > self.date_sold: if self.date_sold and self.date_acquired > self.date_sold:
@@ -188,14 +187,3 @@ class Asset(models.Model, RevisionMixin):
@property @property
def display_id(self): def display_id(self):
return str(self.asset_id) 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
View 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))

View File

@@ -4,9 +4,6 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% block js %} {% 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> <script>
$('document').ready(function(){ $('document').ready(function(){
$('#asset-search-form').submit(function () { $('#asset-search-form').submit(function () {
@@ -49,7 +46,7 @@
<span>Asset with that ID does not exist!</span> <span>Asset with that ID does not exist!</span>
</div> </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"> <div class="form-group form-row">
<h3>Audit Asset:</h3> <h3>Audit Asset:</h3>
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">

View File

@@ -2,6 +2,9 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% block content %} {% block content %}
<div class="row justify-content-end">
{% include 'partials/asset_buttons.html' %}
</div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
{% include 'partials/asset_detail_form.html' %} {% include 'partials/asset_detail_form.html' %}

View File

@@ -3,13 +3,17 @@
{% load static %} {% load static %}
{% block css %} {% block css %}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/> {{ block.super }}
<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/selects.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'js/bootstrap-select.js' %}"></script> {{ block.super }}
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
<script src="{% static 'js/autocompleter.js' %}"></script> <script src="{% static 'js/autocompleter.js' %}"></script>
<script> <script>
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches; const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
@@ -30,7 +34,7 @@
}) })
.ajaxSelectPicker({ .ajaxSelectPicker({
ajax: { ajax: {
url: '{% url 'asset_search_json' %}', url: "{% url 'asset_search_json' %}",
type: "GET", type: "GET",
data: function () { data: function () {
let params = { let params = {

View File

@@ -5,16 +5,17 @@
{% load static %} {% load static %}
{% block css %} {% block css %}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/> {{ block.super }}
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/> <link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
<script src="{% static 'js/bootstrap-select.js' %}"></script> {{ block.super }}
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script> <script src="{% static 'js/selects.js' %}" async></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }}
<script> <script>
//Get querystring value //Get querystring value
function getParameterByName(name) { function getParameterByName(name) {

View File

@@ -5,13 +5,16 @@
{% button 'submit' %} {% button 'submit' %}
{% elif duplicate %} {% elif duplicate %}
<!--duplicate--> <!--duplicate-->
<button type="submit" class="btn btn-success"><i class="fas fa-tick"></i> Create Duplicate</button> <button type="submit" class="btn btn-success"><span class="fas fa-check"></span> Create Duplicate</button>
{% else %} {% else %}
<!--detail view--> <!--detail view-->
<div class="btn-group"> <div class="btn-group">
{% button 'edit' url='asset_update' pk=object.asset_id %} {% button 'edit' url='asset_update' pk=object.asset_id %}
{% button 'duplicate' url='asset_duplicate' pk=object.asset_id %} {% button 'duplicate' url='asset_duplicate' pk=object.asset_id %}
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><i class="fas fa-certificate"></i> Audit</a> <a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><span class="fas fa-certificate"></span> Audit</a>
{% if object.is_cable %}
<a type="button" class="btn btn-primary" href="{% url 'generate_label' object.asset_id %}"><span class="fas fa-barcode"></span> Generate Label</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if create or edit or duplicate %} {% if create or edit or duplicate %}

View File

@@ -25,6 +25,8 @@
{% button 'view' url='asset_detail' pk=item.asset_id clazz="btn-sm" %} {% button 'view' url='asset_detail' pk=item.asset_id clazz="btn-sm" %}
{% if perms.assets.change_asset %} {% if perms.assets.change_asset %}
{% button 'edit' url='asset_update' pk=item.asset_id clazz="btn-sm" %} {% button 'edit' url='asset_update' pk=item.asset_id clazz="btn-sm" %}
{% endif %}
{% if perms.assets.add_asset %}
{% button 'duplicate' url='asset_duplicate' pk=item.asset_id clazz="btn-sm" %} {% button 'duplicate' url='asset_duplicate' pk=item.asset_id clazz="btn-sm" %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -17,11 +17,9 @@
{% else %} {% else %}
<dl> <dl>
<dt>Cable Type</dt> <dt>Cable Type</dt>
<dd>{{ object.cable_type|default_if_none:'-' }}</dd> <dd>{% if object.cable_type %}<a href="{{object.cable_type.get_absolute_url}}">{{ object.cable_type }}</a>{%else%}-{%endif%}</dd>
<dt>Length</dt> <dt>Length</dt>
<dd>{{ object.length|default_if_none:'-' }}m</dd> <dd>{{ object.length|default_if_none:'-' }}m</dd>
<dt>Cross Sectional Area</dt> <dt>Cross Sectional Area</dt>
<dd>{{ object.csa|default_if_none:'-' }}mm²</dd> <dd>{{ object.csa|default_if_none:'-' }}mm²</dd>
</dl> </dl>

View File

@@ -28,13 +28,13 @@
<dt>Children</dt> <dt>Children</dt>
{% if object.asset_parent.all %} {% if object.asset_parent.all %}
<div style="max-height: 200px; overflow-y: auto; -webkit-overflow-scrolling: touch; ">
{% for child in object.asset_parent.all %} {% for child in object.asset_parent.all %}
<dd> <dd>
<a href="{% url 'asset_detail' child.asset_id %}"> <a href="{% url 'asset_detail' child.asset_id %}">{{ child }}</a>
{{ child.asset_id }} - {{ child.description }}
</a>
</dd> </dd>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<dd><span>-</span></dd> <dd><span>-</span></dd>
{% endif %} {% endif %}

View File

@@ -1,4 +1,5 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load linkornone from filters %}
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header"> <div class="card-header">
Purchase Details Purchase Details
@@ -51,14 +52,11 @@
{% else %} {% else %}
<dl> <dl>
<dt>Purchased From</dt> <dt>Purchased From</dt>
<dd>{{ object.purchased_from|default_if_none:'-' }}</dd> <dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
<dt>Purchase Price</dt> <dt>Purchase Price</dt>
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd> <dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
<dt>Salvage Value</dt> <dt>Salvage Value</dt>
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd> <dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
<dt>Date Acquired</dt> <dt>Date Acquired</dt>
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd> <dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
{% if object.date_sold %} {% if object.date_sold %}

35
assets/tests/conftest.py Normal file
View File

@@ -0,0 +1,35 @@
import pytest
from assets import models
import datetime
@pytest.fixture
def category(db):
category = models.AssetCategory.objects.create(name="Sound")
yield category
category.delete()
@pytest.fixture
def status(db):
status = models.AssetStatus.objects.create(name="Broken", should_show=True)
yield status
status.delete()
@pytest.fixture
def test_cable(db, category, status):
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5")
yield cable
connector.delete()
cable_type.delete()
cable.delete()
@pytest.fixture
def test_asset(db, category, status):
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26))
yield asset
asset.delete()

View File

@@ -17,6 +17,7 @@ class AssetList(BasePage):
_status_select_locator = (By.CSS_SELECTOR, 'div#status-group>div.bootstrap-select') _status_select_locator = (By.CSS_SELECTOR, 'div#status-group>div.bootstrap-select')
_category_select_locator = (By.CSS_SELECTOR, 'div#category-group>div.bootstrap-select') _category_select_locator = (By.CSS_SELECTOR, 'div#category-group>div.bootstrap-select')
_go_button_locator = (By.ID, 'id_search') _go_button_locator = (By.ID, 'id_search')
_filter_button_locator = (By.ID, 'filter-submit')
class AssetListRow(Region): class AssetListRow(Region):
_asset_id_locator = (By.CLASS_NAME, "assetID") _asset_id_locator = (By.CLASS_NAME, "assetID")
@@ -56,6 +57,9 @@ class AssetList(BasePage):
def search(self): def search(self):
self.find_element(*self._go_button_locator).click() self.find_element(*self._go_button_locator).click()
def filter(self):
self.find_element(*self._filter_button_locator).click()
@property @property
def status_selector(self): def status_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._status_select_locator)) return regions.BootstrapSelectElement(self, self.find_element(*self._status_select_locator))

View File

@@ -5,7 +5,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_equal from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal
from PyRIGS.tests.pages import animation_is_finished from PyRIGS.tests.pages import animation_is_finished
from assets import models from assets import models
from . import pages from . import pages
@@ -78,7 +78,7 @@ class TestAssetList(AutoLoginTest):
self.page.status_selector.select_all() self.page.status_selector.select_all()
self.page.status_selector.toggle() self.page.status_selector.toggle()
self.assertFalse(self.page.status_selector.is_open) self.assertFalse(self.page.status_selector.is_open)
self.page.search() self.page.filter()
self.assertTrue(len(self.page.assets) == 4) self.assertTrue(len(self.page.assets) == 4)
self.page.category_selector.toggle() self.page.category_selector.toggle()
@@ -86,7 +86,7 @@ class TestAssetList(AutoLoginTest):
self.page.category_selector.set_option("Sound", True) self.page.category_selector.set_option("Sound", True)
self.page.category_selector.close() self.page.category_selector.close()
self.assertFalse(self.page.category_selector.is_open) self.assertFalse(self.page.category_selector.is_open)
self.page.search() self.page.filter()
self.assertTrue(len(self.page.assets) == 2) self.assertTrue(len(self.page.assets) == 2)
asset_ids = list(map(lambda x: x.id, self.page.assets)) asset_ids = list(map(lambda x: x.id, self.page.assets))
self.assertEqual("1", asset_ids[0]) self.assertEqual("1", asset_ids[0])
@@ -110,7 +110,7 @@ class TestAssetForm(AutoLoginTest):
def test_asset_create(self): def test_asset_create(self):
# Test that ID is automatically assigned and properly incremented # Test that ID is automatically assigned and properly incremented
self.assertIn(self.page.asset_id, "9001") # self.assertIn(self.page.asset_id, "9001") FIXME
self.page.remove_all_required() self.page.remove_all_required()
self.page.asset_id = "XX$X" self.page.asset_id = "XX$X"
@@ -128,20 +128,20 @@ class TestAssetForm(AutoLoginTest):
self.page.serial_number = sn = "0124567890-SAUSAGE" self.page.serial_number = sn = "0124567890-SAUSAGE"
self.page.comments = comments = "This is actually a sledgehammer, not a cable..." self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
self.page.purchase_price = "12.99"
self.page.salvage_value = "99.12"
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
self.page.purchased_from_selector.toggle() self.page.purchased_from_selector.toggle()
self.assertTrue(self.page.purchased_from_selector.is_open) self.assertTrue(self.page.purchased_from_selector.is_open)
self.page.purchased_from_selector.search(self.supplier.name[:-8]) self.page.purchased_from_selector.search(self.supplier.name[:-8])
self.page.purchased_from_selector.set_option(self.supplier.name, True) self.page.purchased_from_selector.set_option(self.supplier.name, True)
self.page.purchase_price = "12.99"
self.page.salvage_value = "99.12"
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
self.page.parent_selector.toggle() self.page.parent_selector.toggle()
self.assertTrue(self.page.parent_selector.is_open) self.assertTrue(self.page.parent_selector.is_open)
self.page.parent_selector.search(self.parent.asset_id) option = str(self.parent)
# Needed here but not earlier for whatever reason self.page.parent_selector.search(option)
self.driver.implicitly_wait(1) self.driver.implicitly_wait(1)
self.page.parent_selector.set_option(self.parent.asset_id + " | " + self.parent.description, True) self.page.parent_selector.set_option(option, True)
self.assertTrue(self.page.parent_selector.options[0].selected) self.assertTrue(self.page.parent_selector.options[0].selected)
self.page.parent_selector.toggle() self.page.parent_selector.toggle()
@@ -272,6 +272,16 @@ class TestSupplierCreateAndEdit(AutoLoginTest):
self.assertTrue(self.page.success) self.assertTrue(self.page.success)
def test_audit_search(logged_in_browser, live_server, test_asset):
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
# Check that a failed search works
page.set_query("NOTFOUND")
page.search()
assert not logged_in_browser.find_by_id('modal').visible
logged_in_browser.driver.implicitly_wait(4)
assert logged_in_browser.is_text_present("Asset with that ID does not exist!")
@screenshot_failure_cls @screenshot_failure_cls
class TestAssetAudit(AutoLoginTest): class TestAssetAudit(AutoLoginTest):
def setUp(self): def setUp(self):
@@ -312,6 +322,7 @@ class TestAssetAudit(AutoLoginTest):
# Now do it properly # Now do it properly
self.page.modal.description = new_desc = "A BIG hammer" self.page.modal.description = new_desc = "A BIG hammer"
self.page.modal.submit() self.page.modal.submit()
self.driver.implicitly_wait(4)
self.wait.until(animation_is_finished()) self.wait.until(animation_is_finished())
submit_time = timezone.now() submit_time = timezone.now()
# Check data is correct # Check data is correct
@@ -319,7 +330,7 @@ class TestAssetAudit(AutoLoginTest):
self.assertEqual(self.asset.description, new_desc) self.assertEqual(self.asset.description, new_desc)
# Make sure audit 'log' was filled out # Make sure audit 'log' was filled out
self.assertEqual(self.profile.initials, self.asset.last_audited_by.initials) self.assertEqual(self.profile.initials, self.asset.last_audited_by.initials)
assert_times_equal(submit_time, self.asset.last_audited_at) assert_times_almost_equal(submit_time, self.asset.last_audited_at)
# Check we've removed it from the 'needing audit' list # Check we've removed it from the 'needing audit' list
self.assertNotIn(self.asset.asset_id, self.page.assets) self.assertNotIn(self.asset.asset_id, self.page.assets)
@@ -334,10 +345,3 @@ class TestAssetAudit(AutoLoginTest):
# Make sure audit log was NOT filled out # Make sure audit log was NOT filled out
audited = models.Asset.objects.get(asset_id=asset_row.id) audited = models.Asset.objects.get(asset_id=asset_row.id)
assert audited.last_audited_by is None assert audited.last_audited_by is None
def test_audit_search(self):
# Check that a failed search works
self.page.set_query("NOTFOUND")
self.page.search()
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
self.assertIn("Asset with that ID does not exist!", self.page.error.text)

View File

@@ -1,64 +1,41 @@
import datetime import datetime
import pytest import pytest
from django.core.management import call_command
from django.test.utils import override_settings
from django.urls import reverse from django.urls import reverse
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
from assets import models, urls from PyRIGS.tests.base import assert_oembed, login
pytestmark = pytest.mark.django_db # TODO from assets import models
from django.utils import timezone
pytestmark = pytest.mark.django_db
def login(client, django_user_model): def test_supplier_create(admin_client):
pwd = 'testuser'
usr = "TestUser"
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)
def create_test_asset():
working = models.AssetStatus.objects.create(name="Working", should_show=True)
lighting = models.AssetCategory.objects.create(name="Lighting")
asset = models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
return asset
def create_test_cable():
category = models.AssetCategory.objects.create(name="Sound")
status = models.AssetStatus.objects.create(name="Broken", should_show=True)
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
return models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5")
def test_supplier_create(client, django_user_model):
login(client, django_user_model)
url = reverse('supplier_create') url = reverse('supplier_create')
response = client.post(url) response = admin_client.post(url)
assertFormError(response, 'form', 'name', 'This field is required.') assertFormError(response, 'form', 'name', 'This field is required.')
def test_supplier_edit(client, django_user_model): def test_supplier_edit(admin_client):
login(client, django_user_model)
supplier = models.Supplier.objects.create(name="Gadgetron Corporation") supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
url = reverse('supplier_update', kwargs={'pk': supplier.pk}) url = reverse('supplier_update', kwargs={'pk': supplier.pk})
response = client.post(url, {'name': ""}) response = admin_client.post(url, {'name': ""})
assertFormError(response, 'form', 'name', 'This field is required.') assertFormError(response, 'form', 'name', 'This field is required.')
def test_404(client, django_user_model): def test_404(admin_client):
login(client, django_user_model)
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'} urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
for url_name in urls: for url_name in urls:
request_url = reverse(url_name, kwargs={'pk': "0000"}) request_url = reverse(url_name, kwargs={'pk': "0000"})
response = client.get(request_url, follow=True) response = admin_client.get(request_url, follow=True)
assert response.status_code == 404 assert response.status_code == 404
def test_embed_login_redirect(client, django_user_model): def test_embed_login_redirect(client, django_user_model, test_asset):
request_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id}) request_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url) expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
# Request the page and check it redirects # Request the page and check it redirects
@@ -79,8 +56,8 @@ def test_login_cookie_warning(client, django_user_model):
assert "Cookies do not seem to be enabled" in str(response.content) assert "Cookies do not seem to be enabled" in str(response.content)
def test_x_frame_headers(client, django_user_model): def test_x_frame_headers(client, django_user_model, test_asset):
asset_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id}) asset_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
login_url = reverse('login_embed') login_url = reverse('login_embed')
login(client, django_user_model) login(client, django_user_model)
@@ -94,100 +71,42 @@ def test_x_frame_headers(client, django_user_model):
response._headers["X-Frame-Options"] response._headers["X-Frame-Options"]
def test_oembed(client): def test_oembed(client, test_asset):
asset = create_test_asset() client.logout()
asset_url = reverse('asset_detail', kwargs={'pk': asset.asset_id}) asset_url = reverse('asset_detail', kwargs={'pk': test_asset.asset_id})
asset_embed_url = reverse('asset_embed', kwargs={'pk': asset.asset_id}) asset_embed_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id})
oembed_url = reverse('asset_oembed', kwargs={'pk': asset.asset_id}) oembed_url = reverse('asset_oembed', kwargs={'pk': test_asset.asset_id})
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999}) alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999}) alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
# Test the meta tag is in place assert_oembed(alt_asset_embed_url, alt_oembed_url, client, asset_embed_url, asset_url, oembed_url)
response = client.get(asset_url, follow=True, HTTP_HOST='example.com')
assert '<link rel="alternate" type="application/json+oembed"' in str(response.content)
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, asset_embed_url)
# Should also work for non-existant
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, alt_asset_embed_url)
@override_settings(DEBUG=True) def test_asset_create(admin_client):
def test_generate_sample_data(client): response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
# Run the management command and check there are no exceptions
call_command('generateSampleAssetsData')
# Check there are lots
assert models.Asset.objects.all().count() > 50
assert models.Supplier.objects.all().count() > 50
@override_settings(DEBUG=True)
def test_delete_sample_data(client):
call_command('deleteSampleData')
assert models.Asset.objects.all().count() == 0
assert models.Supplier.objects.all().count() == 0
def test_production_exception(client):
from django.core.management.base import CommandError
with pytest.raises(CommandError, match=".*production"):
call_command('generateSampleAssetsData')
call_command('deleteSampleData')
def test_asset_create(client, django_user_model):
login(client, django_user_model)
response = client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
assertFormError(response, 'form', 'asset_id', 'This field is required.') assertFormError(response, 'form', 'asset_id', 'This field is required.')
assertFormError(response, 'form', 'description', 'This field is required.') assert_asset_form_errors(response)
assertFormError(response, 'form', 'status', 'This field is required.')
assertFormError(response, 'form', 'category', 'This field is required.')
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
def test_cable_create(client, django_user_model): def test_cable_create(admin_client):
login(client, django_user_model) response = admin_client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True})
response = client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True})
assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number') assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
assertFormError(response, 'form', 'cable_type', 'A cable must have a type') assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway!
def test_asset_edit(admin_client, test_asset):
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
assert_asset_form_errors(response)
def test_asset_edit(client, django_user_model): def test_cable_edit(admin_client, test_cable):
login(client, django_user_model) url = reverse('asset_update', kwargs={'pk': test_cable.asset_id})
url = reverse('asset_update', kwargs={'pk': create_test_asset().asset_id})
response = client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
# assertFormError(response, 'form', 'asset_id', 'This field is required.')
assertFormError(response, 'form', 'description', 'This field is required.')
assertFormError(response, 'form', 'status', 'This field is required.')
assertFormError(response, 'form', 'category', 'This field is required.')
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
def test_cable_edit(client, django_user_model):
login(client, django_user_model)
url = reverse('asset_update', kwargs={'pk': create_test_cable().asset_id})
# TODO Why do I have to send is_cable=True here? # TODO Why do I have to send is_cable=True here?
response = client.post(url, {'is_cable': True, 'length': -3, 'csa': -3}) response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
# TODO Can't figure out how to select the 'none' option... # TODO Can't figure out how to select the 'none' option...
# assertFormError(response, 'form', 'cable_type', 'A cable must have a type') # assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
@@ -195,66 +114,18 @@ def test_cable_edit(client, django_user_model):
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
def test_asset_duplicate(client, django_user_model): def test_asset_duplicate(admin_client, test_cable):
login(client, django_user_model) url = reverse('asset_duplicate', kwargs={'pk': test_cable.asset_id})
url = reverse('asset_duplicate', kwargs={'pk': create_test_cable().asset_id}) response = admin_client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
response = client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
@override_settings(DEBUG=True) def assert_asset_form_errors(response):
def create_asset_one(): assertFormError(response, 'form', 'description', 'This field is required.')
# Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production assertFormError(response, 'form', 'status', 'This field is required.')
call_command('generateSampleData') assertFormError(response, 'form', 'category', 'This field is required.')
# Create an asset with ID 1 to make things easier in loops (we can always use pk=1) assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
category = models.AssetCategory.objects.create(name="Number One") assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True) assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
return models.Asset.objects.create(asset_id="1", description="Half Price Fish", status=status, category=category, date_acquired=datetime.date(2020, 2, 1))
def test_basic_access(client):
create_asset_one()
client.login(username="basic", password="basic")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons NOT shown in list
assertNotContains(response, 'Edit')
assertNotContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': "9000"})
response = client.get(url)
assertNotContains(response, 'Purchase Details')
assertNotContains(response, 'View Revision History')
urls = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urls:
request_url = reverse(url_name, kwargs={'pk': "9000"})
response = client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_create')
response = client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_update', kwargs={'pk': "1"})
response = client.get(request_url, follow=True)
assert response.status_code == 403
def test_keyholder_access(client):
create_asset_one()
client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': "9000"})
response = client.get(url)
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')

View File

@@ -3,6 +3,7 @@ from django.urls import path
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from PyRIGS.decorators import has_oembed, permission_required_with_403 from PyRIGS.decorators import has_oembed, permission_required_with_403
from PyRIGS.views import OEmbedView
from assets import views from assets import views
urlpatterns = [ urlpatterns = [
@@ -15,6 +16,7 @@ urlpatterns = [
(views.AssetEdit.as_view()), name='asset_update'), (views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<str:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'), path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'), path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
@@ -26,9 +28,7 @@ urlpatterns = [
xframe_options_exempt( xframe_options_exempt(
login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())), login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())),
name='asset_embed'), name='asset_embed'),
path('asset/id/<str:pk>/oembed_json/', path('asset/id/<str:pk>/oembed_json/', views.AssetOEmbed.as_view(), name='asset_oembed'),
views.AssetOembed.as_view(),
name='asset_oembed'),
path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'), path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'),
path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'), path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'),

View File

@@ -1,4 +1,5 @@
import simplejson import simplejson
import random
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import serializers from django.core import serializers
@@ -9,13 +10,18 @@ from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import generic from django.views import generic
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404
from PIL import Image, ImageDraw, ImageFont
from barcode import Code39
from barcode.writer import ImageWriter
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \ from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
is_ajax is_ajax, OEmbedView
from assets import forms, models from assets import forms, models
from assets.models import get_available_asset_id
@method_decorator(csrf_exempt, name='dispatch')
class AssetList(LoginRequiredMixin, generic.ListView): class AssetList(LoginRequiredMixin, generic.ListView):
model = models.Asset model = models.Asset
template_name = 'asset_list.html' template_name = 'asset_list.html'
@@ -28,9 +34,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
return initial return initial
def get_queryset(self): def get_queryset(self):
if self.request.method == 'POST': if self.request.method == 'GET' and len(self.request.GET) > 0:
self.form = forms.AssetSearchForm(data=self.request.POST)
elif self.request.method == 'GET' and len(self.request.GET) > 0:
self.form = forms.AssetSearchForm(data=self.request.GET) self.form = forms.AssetSearchForm(data=self.request.GET)
else: else:
self.form = forms.AssetSearchForm(data=self.get_initial()) self.form = forms.AssetSearchForm(data=self.get_initial())
@@ -57,7 +61,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
queryset = queryset.filter( queryset = queryset.filter(
status__in=models.AssetStatus.objects.filter(should_show=True)) status__in=models.AssetStatus.objects.filter(should_show=True))
return queryset return queryset.select_related('category', 'status')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetList, self).get_context_data(**kwargs) context = super(AssetList, self).get_context_data(**kwargs)
@@ -142,7 +146,7 @@ class AssetCreate(LoginRequiredMixin, generic.CreateView):
def get_initial(self, *args, **kwargs): def get_initial(self, *args, **kwargs):
initial = super().get_initial(*args, **kwargs) initial = super().get_initial(*args, **kwargs)
initial["asset_id"] = models.Asset.get_available_asset_id() initial["asset_id"] = get_available_asset_id()
return initial return initial
def get_success_url(self): def get_success_url(self):
@@ -166,37 +170,23 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
return context return context
class AssetOembed(generic.View):
model = models.Asset
def get(self, request, pk=None):
embed_url = reverse('asset_embed', args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
class AssetEmbed(AssetDetail): class AssetEmbed(AssetDetail):
template_name = 'asset_embed.html' template_name = 'asset_embed.html'
@method_decorator(csrf_exempt, name='dispatch') class AssetOEmbed(OEmbedView):
model = models.Asset
url_name = 'asset_embed'
class AssetAuditList(AssetList): class AssetAuditList(AssetList):
template_name = 'asset_audit_list.html' template_name = 'asset_audit_list.html'
hide_hidden_status = False hide_hidden_status = False
# TODO Refresh this when the modal is submitted # TODO Refresh this when the modal is submitted
def get_queryset(self): def get_queryset(self):
self.form = forms.AssetSearchForm(data={}) self.form = forms.AssetSearchForm(data=self.request.GET)
return self.model.objects.filter(Q(last_audited_at__isnull=True)) return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetAuditList, self).get_context_data(**kwargs) context = super(AssetAuditList, self).get_context_data(**kwargs)
@@ -304,7 +294,9 @@ class CableTypeList(generic.ListView):
model = models.CableType model = models.CableType
template_name = 'cable_type_list.html' template_name = 'cable_type_list.html'
paginate_by = 40 paginate_by = 40
# ordering = ['__str__']
def get_queryset(self):
return self.model.objects.select_related('plug', 'socket')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -352,3 +344,37 @@ class CableTypeUpdate(generic.UpdateView):
def get_success_url(self): def get_success_url(self):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk}) return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
class GenerateLabel(generic.View):
def get(self, request, pk):
black = (0, 0, 0)
white = (255, 255, 255)
size = (700, 200)
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
obj = get_object_or_404(models.Asset, asset_id=pk)
asset_id = "Asset: {}".format(obj.asset_id)
length = "Length: {}m".format(obj.length)
csa = "CSA: {}mm²".format(obj.csa)
image = Image.new("RGB", size, white)
logo = Image.open("static/imgs/square_logo.png")
draw = ImageDraw.Draw(image)
draw.text((210, 140), asset_id, fill=black, font=font)
draw.text((210, 170), length, fill=black, font=font)
draw.text((350, 170), csa, fill=black, font=font)
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
logo_size = (200, 200)
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
width, height = barcode_image.size
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
response = HttpResponse(content_type="image/png")
image.save(response, "PNG")
return response

View File

@@ -1,9 +1,78 @@
from django.conf import settings from django.conf import settings
import django import django
import pytest
from django.core.management import call_command
from RIGS.models import VatRate, Profile
import random
from django.db import connection
from PyRIGS.tests import pages
import os
from selenium import webdriver
def pytest_configure(): def pytest_configure():
settings.PASSWORD_HASHERS = ( settings.PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
) )
settings.WHITENOISE_USE_FINDERS = True
settings.WHITENOISE_AUTOREFRESH = True
# TODO Why do we need this, with the above options enabled?
settings.STATICFILES_DIRS += [
os.path.join(settings.BASE_DIR, 'static/'),
]
django.setup() django.setup()
@pytest.fixture # Overrides the one from pytest-django
def admin_user(admin_user):
admin_user.username = "EventTest"
admin_user.first_name = "Event"
admin_user.last_name = "Test"
admin_user.initials = "ETU"
admin_user.save()
return admin_user
@pytest.fixture
def logged_in_browser(live_server, admin_user, browser, db):
login_page = pages.LoginPage(browser.driver, live_server.url).open()
login_page.login(admin_user.username, "password")
yield browser
@pytest.fixture(scope='session')
def splinter_driver_kwargs():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
options.add_argument("--headless")
if settings.CI:
options.add_argument("--no-sandbox")
return {"options": options}
@pytest.fixture(scope='session')
def splinter_webdriver():
return 'chrome'
@pytest.fixture(scope='session')
def splinter_screenshot_dir():
return 'screenshots/'
@pytest.fixture(autouse=True) # Also enables DB access for all tests as a useful side effect
def vat_rate(db):
vat_rate = VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
yield vat_rate
vat_rate.delete()
def _has_transactional_marker(item):
db_marker = item.get_closest_marker("django_db")
if db_marker and db_marker.kwargs.get("transaction"):
return 1
return 0
def pytest_collection_modifyitems(items):
items.sort(key=_has_transactional_marker)

View File

@@ -2,40 +2,52 @@
var gulp = require('gulp'); var gulp = require('gulp');
var terser = require('gulp-terser'); const terser = require('gulp-uglify');
var sass = require('gulp-sass'); const sass = require('gulp-sass');
var flatten = require('gulp-flatten'); const flatten = require('gulp-flatten');
var autoprefixer = require('autoprefixer') const autoprefixer = require('autoprefixer')
var postcss = require('gulp-postcss') const postcss = require('gulp-postcss')
var sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
var browsersync = require('browser-sync').create(); const browsersync = require('browser-sync').create();
var { exec } = require("child_process"); const { exec } = require("child_process");
var spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
const cssnano = require('cssnano');
const con = require('gulp-concat');
const gulpif = require('gulp-if');
sass.compiler = require('node-sass'); sass.compiler = require('node-sass');
function fonts(done) {
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*')
.pipe(gulp.dest('pipeline/built_assets/fonts'))
.pipe(browsersync.stream());
}
function styles(done) { function styles(done) {
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
return gulp.src(['pipeline/source_assets/scss/**/*.scss', return gulp.src(['pipeline/source_assets/scss/**/*.scss',
'node_modules/fullcalendar/main.min.css', 'node_modules/fullcalendar/main.css',
'node_modules/bootstrap-select/dist/css/bootstrap-select.css', 'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css', 'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
'node_modules/flatpickr/dist/flatpickr.css']) 'node_modules/flatpickr/dist/flatpickr.css',])
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError)) .pipe(sass().on('error', sass.logError))
.pipe(postcss([ autoprefixer() ])) .pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.css')))
.pipe(postcss([ autoprefixer(), cssnano() ]))
.pipe(sourcemaps.write()) .pipe(sourcemaps.write())
.pipe(gulp.dest('pipeline/built_assets/css')) .pipe(gulp.dest('pipeline/built_assets/css'))
.pipe(browsersync.stream()); .pipe(browsersync.stream());
} }
function scripts() { function scripts() {
return gulp.src(['pipeline/source_assets/js/**/*.js', const dest = 'pipeline/built_assets/js';
'node_modules/jquery/dist/jquery.js', const base_scripts = ["src.js", "util.js", "alert.js", "collapse.js", "dropdown.js", "modal.js", "konami.js"];
const bs_select = ["bootstrap-select.js", "ajax-bootstrap-select.js"]
const interaction = ["html5sortable.min.js", "interaction.js"]
const jpop = ["jquery.min.js", "popper.min.js"]
return gulp.src(['node_modules/jquery/dist/jquery.min.js',
/* JQuery Plugins */ /* JQuery Plugins */
'node_modules/jquery-ui-dist/jquery-ui.js', 'node_modules/popper.js/dist/umd/popper.min.js',
'node_modules/popper.js/dist/umd/popper.js',
'node_modules/raven-js/dist/raven.js', //TODO Upgrade to Sentry
/* Bootstrap Plugins */ /* Bootstrap Plugins */
'node_modules/bootstrap/js/dist/util.js', 'node_modules/bootstrap/js/dist/util.js',
'node_modules/bootstrap/js/dist/tooltip.js', 'node_modules/bootstrap/js/dist/tooltip.js',
@@ -45,18 +57,22 @@ function scripts() {
'node_modules/bootstrap/js/dist/modal.js', 'node_modules/bootstrap/js/dist/modal.js',
'node_modules/bootstrap/js/dist/alert.js', 'node_modules/bootstrap/js/dist/alert.js',
'node_modules/html5sortable/dist/html5sortable.min.js',
'node_modules/clipboard/dist/clipboard.min.js', 'node_modules/clipboard/dist/clipboard.min.js',
'node_modules/flatpickr/dist/flatpickr.min.js', 'node_modules/flatpickr/dist/flatpickr.min.js',
'node_modules/@fortawesome/fontawesome-free/js/all.js',
'node_modules/moment/moment.js', 'node_modules/moment/moment.js',
'node_modules/fullcalendar/main.min.js', 'node_modules/fullcalendar/main.js',
'node_modules/bootstrap-select/dist/js/bootstrap-select.js', 'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js', 'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
'node_modules/konami/konami.js', 'node_modules/konami/konami.js',
'node_modules/dark-mode-switch/dark-mode-switch.min.js']) 'pipeline/source_assets/js/**/*.js',])
.pipe(gulpif(function(file) { return base_scripts.includes(file.relative);}, con('base.js')))
.pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.js')))
.pipe(gulpif(function(file) { return interaction.includes(file.relative);}, con('interaction.js')))
.pipe(gulpif(function(file) { return jpop.includes(file.relative);}, con('jpop.js')))
.pipe(flatten()) .pipe(flatten())
.pipe(terser()) .pipe(terser())
.pipe(gulp.dest('pipeline/built_assets/js')) .pipe(gulp.dest(dest))
.pipe(browsersync.stream()); .pipe(browsersync.stream());
} }
@@ -64,7 +80,8 @@ function browserSync(done) {
spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'}); spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'});
// TODO Wait for Django server to come up before browsersync, it seems inconsistent // TODO Wait for Django server to come up before browsersync, it seems inconsistent
browsersync.init({ browsersync.init({
notify: true, notify: false,
open: false,
port: 8001, port: 8001,
proxy: 'localhost:8000' proxy: 'localhost:8000'
}); });
@@ -77,11 +94,10 @@ function browserSyncReload(done) {
} }
function watchFiles() { function watchFiles() {
gulp.watch("RIGS/static/scss/**/*.scss", styles); gulp.watch("pipeline/source_assets/scss/**/*.scss", styles);
// TODO This prevents reload looping, but means we don't reload if new third party scripts are added gulp.watch("pipeline/source_assets/js/**/*.js", scripts);
gulp.watch("RIGS/static/js/src/**/*.js", scripts);
gulp.watch("**/templates/*.html", browserSyncReload); gulp.watch("**/templates/*.html", browserSyncReload);
} }
exports.build = gulp.parallel(styles, scripts); exports.build = gulp.parallel(styles, scripts, fonts);
exports.watch = gulp.parallel(watchFiles, browserSync); exports.watch = gulp.parallel(watchFiles, browserSync);

2862
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,29 +6,31 @@
"license": "Custom", "license": "Custom",
"dependencies": { "dependencies": {
"@forevolve/bootstrap-dark": "^1.0.0-alpha.1075", "@forevolve/bootstrap-dark": "^1.0.0-alpha.1075",
"@fortawesome/fontawesome-free": "^5.13.1", "@fortawesome/fontawesome-free": "^5.15.2",
"ajax-bootstrap-select": "^1.4.5", "ajax-bootstrap-select": "^1.4.5",
"autocompleter": "^6.0.3", "autocompleter": "^6.0.3",
"autoprefixer": "^9.8.0", "autoprefixer": "^9.8.0",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
"bootstrap-select": "^1.13.17", "bootstrap-select": "^1.13.17",
"clipboard": "^2.0.6", "clipboard": "^2.0.6",
"dark-mode-switch": "^1.0.0", "cssnano": "^4.1.10",
"flatpickr": "^4.6.6", "flatpickr": "^4.6.6",
"fullcalendar": "^5.3.2", "fullcalendar": "^5.3.2",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-flatten": "^0.4.0", "gulp-flatten": "^0.4.0",
"gulp-if": "^3.0.0",
"gulp-postcss": "^8.0.0", "gulp-postcss": "^8.0.0",
"gulp-sass": "^4.1.0", "gulp-sass": "^4.1.0",
"gulp-sourcemaps": "^2.6.5", "gulp-sourcemaps": "^2.6.5",
"gulp-terser": "^1.4.1", "gulp-uglify": "^3.0.2",
"html5sortable": "^0.10.0",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"jquery-ui-dist": "^1.12.1",
"konami": "^1.6.2", "konami": "^1.6.2",
"moment": "^2.27.0", "moment": "^2.27.0",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"raven-js": "^3.27.2" "uglify-js": "^3.12.6"
}, },
"devDependencies": { "devDependencies": {
"browser-sync": "^2.26.12" "browser-sync": "^2.26.12"

View File

@@ -117,23 +117,9 @@ $('body').on('submit', '.itemised_form', function (e) {
$('#id_items_json').val(JSON.stringify(objectitems)); $('#id_items_json').val(JSON.stringify(objectitems));
}); });
// Return a helper with preserved width of cells sortable("#item-table tbody")[0].addEventListener('sortupdate', function (e) {
var fixHelper = function (e, ui) { var items = e.detail.destination.items;
ui.children().each(function () { for(var i in items) {
$(this).width($(this).width()); objectitems[items[i].dataset.pk].fields.order = i;
});
return ui;
};
$("#item-table tbody").sortable({
helper: fixHelper,
update: function (e, ui) {
info = $(this).sortable("toArray");
itemorder = new Array();
$.each(info, function (key, value) {
pk = $('#' + value).data('pk');
objectitems[pk].fields.order = key;
});
} }
}); });

View File

@@ -0,0 +1,39 @@
Date.prototype.getISOString = function () {
var yyyy = this.getFullYear().toString();
var mm = (this.getMonth() + 1).toString(); // getMonth() is zero-based
var dd = this.getDate().toString();
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
};
jQuery(document).ready(function () {
jQuery(document).on('click', '.modal-href', function (e) {
$link = jQuery(this);
// Anti modal inception
if ($link.parents('#modal').length == 0) {
e.preventDefault();
modaltarget = $link.data('target');
modalobject = "";
jQuery('#modal').load($link.attr('href'), function (e) {
jQuery('#modal').modal();
});
}
});
var easter_egg = new Konami();
easter_egg.code = function () {
var s = document.createElement('script');
s.type = 'text/javascript';
document.body.appendChild(s);
s.src = '{% static "js/asteroids.min.js"%}';
ga('send', 'event', 'easter_egg', 'activated');
}
easter_egg.load();
});
//CTRL-Enter form submission
document.body.addEventListener('keydown', function(e) {
if(e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
var target = e.target;
if(target.form) {
target.form.submit();
}
}
});
$('.navbar-collapse').addClass('collapse');

View File

@@ -111,4 +111,26 @@
.custom-control-input:focus ~ .custom-control-label::before { .custom-control-input:focus ~ .custom-control-label::before {
box-shadow: 0 0 0 $input-focus-width rgba($primary, 0.7) !important; box-shadow: 0 0 0 $input-focus-width rgba($primary, 0.7) !important;
} }
.source {
color: white !important;
}
.embed_container {
border-color: #3853a4 !important;
background: #222;
color: $gray-100;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
border: 1px solid $info;
-webkit-text-fill-color: white;
-webkit-box-shadow: 0 0 0px 1000px rgba($info, .3) inset;
transition: background-color 5000s ease-in-out 0s;
}
} }

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