mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-10 00:39:40 +00:00
Compare commits
125 Commits
3b9848d457
...
checkin
| Author | SHA1 | Date | |
|---|---|---|---|
|
b151e1fcf3
|
|||
|
013922bd90
|
|||
|
f72b611e77
|
|||
|
8da90ba670
|
|||
| 0d1beeaead | |||
| 51ff06fadd | |||
| 06137ab180 | |||
| e7dd8f4cf9 | |||
| 2fb8b8c81b | |||
|
dd02df4a17
|
|||
|
8cc40fe9fa
|
|||
|
904ef1f180
|
|||
|
417ec8ff3d
|
|||
|
97dac51a52
|
|||
|
fc8bb2dc4b
|
|||
|
67d2f80815
|
|||
|
724762a1e8
|
|||
|
6ea5dc9698
|
|||
|
|
eb45db8950 | ||
|
15a9a03200
|
|||
| 676cee164b | |||
| cccb84e4dc | |||
|
742836486e
|
|||
|
e774b06ac4
|
|||
|
b1a2859f1b
|
|||
|
dc71c2de62
|
|||
| 8863d86ed0 | |||
|
|
6550ed2318 | ||
|
|
c9759a6339 | ||
|
|
b637c4e452 | ||
|
|
8986b94b07 | ||
| 86c033ba97 | |||
| 52fd662340 | |||
| 9818ed995f | |||
| 5178614d71 | |||
| a7bf990666 | |||
|
a4a28a6130
|
|||
|
e3d8cf8978
|
|||
|
626779ef25
|
|||
| fa1dc31639 | |||
| d69543e309 | |||
|
|
a24e6d4495 | ||
|
|
fa5792914a | ||
| 0117091f3e | |||
|
37101d3340
|
|||
| de4bed92a4 | |||
|
|
3767923175 | ||
|
|
1d77cf95d3 | ||
|
|
1f21d0b265 | ||
| 7846a6d31e | |||
| d28b73a0b8 | |||
|
|
5c2e8b391c | ||
|
|
548bc1df81 | ||
|
|
c1d2bce8fb | ||
|
c71beab278
|
|||
|
259932a548
|
|||
|
7526485837
|
|||
| 39ed5aefb4 | |||
|
e7e760de2e
|
|||
| 9091197639 | |||
|
4f4baa62c1
|
|||
|
b9f8621e1a
|
|||
|
4b1dc37a7f
|
|||
|
|
9273ca35cf | ||
|
|
4a4b7fa30d | ||
| a44a532c7d | |||
| 3a2e5c943b | |||
| 426a9088cc | |||
|
|
1369a2f978 | ||
|
38eafbced3
|
|||
|
900002bf71
|
|||
|
2869c9fcc3
|
|||
|
00eb4e0e27
|
|||
|
23e17b0e34
|
|||
|
bf268a4566
|
|||
|
dedb8d81fe
|
|||
|
7d785f4f1b
|
|||
|
5eb113156b
|
|||
|
ab03ad081a
|
|||
| cd5889f60e | |||
| f18bf3b077 | |||
|
3d36d986a4
|
|||
|
41f5a23ef0
|
|||
|
09f48f740d
|
|||
|
805d77af20
|
|||
|
fabab87e23
|
|||
|
a95779e04e
|
|||
|
24e6ba540d
|
|||
|
14d3522b81
|
|||
|
e4cfaba57d
|
|||
|
d9664422c5
|
|||
|
27bb3f1d8e
|
|||
|
151ac8b3bd
|
|||
|
c2dcd86d5d
|
|||
|
6c14b30c13
|
|||
|
5215af349a
|
|||
|
a5e888fef5
|
|||
|
|
2ae4e4142c | ||
|
8799f822bb
|
|||
|
2dd3d306b4
|
|||
|
042004e1ae
|
|||
|
733ea69cc5
|
|||
|
bbea47e8ec
|
|||
|
c4aafbd7e5
|
|||
|
ccdc13df93
|
|||
|
aa19ceaf18
|
|||
|
05d280172d
|
|||
|
2f51b7b1d3
|
|||
|
8d1edb54ea
|
|||
| 54c90a7be4 | |||
|
3e1e0079d8
|
|||
|
b6952aeb52
|
|||
|
d33a4231fb
|
|||
|
8dea6aeab0
|
|||
|
34c03e379d
|
|||
|
988fb78b45
|
|||
|
eda314c092
|
|||
|
8ef520619a
|
|||
|
95931f86b4
|
|||
|
cc2cb5c4d1
|
|||
|
3ae507b469
|
|||
|
33754eed60
|
|||
|
15ab626593
|
|||
|
7bc47b446c
|
|||
|
83b287a418
|
151
.github/workflows/combine-prs.yml
vendored
Normal file
151
.github/workflows/combine-prs.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
name: 'Combine PRs'
|
||||||
|
|
||||||
|
# Controls when the action will run - in this case triggered manually
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branchPrefix:
|
||||||
|
description: 'Branch prefix to find combinable PRs based on'
|
||||||
|
required: true
|
||||||
|
default: 'dependabot'
|
||||||
|
mustBeGreen:
|
||||||
|
description: 'Only combine PRs that are green (status is success)'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
combineBranchName:
|
||||||
|
description: 'Name of the branch to combine PRs into'
|
||||||
|
required: true
|
||||||
|
default: 'combine-prs-branch'
|
||||||
|
ignoreLabel:
|
||||||
|
description: 'Exclude PRs with this label'
|
||||||
|
required: true
|
||||||
|
default: 'nocombine'
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
# This workflow contains a single job called "combine-prs"
|
||||||
|
combine-prs:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v6
|
||||||
|
id: create-combined-pr
|
||||||
|
name: Create Combined PR
|
||||||
|
with:
|
||||||
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
script: |
|
||||||
|
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo
|
||||||
|
});
|
||||||
|
let branchesAndPRStrings = [];
|
||||||
|
let baseBranch = null;
|
||||||
|
let baseBranchSHA = null;
|
||||||
|
for (const pull of pulls) {
|
||||||
|
const branch = pull['head']['ref'];
|
||||||
|
console.log('Pull for branch: ' + branch);
|
||||||
|
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
|
||||||
|
console.log('Branch matched prefix: ' + branch);
|
||||||
|
let statusOK = true;
|
||||||
|
if(${{ github.event.inputs.mustBeGreen }}) {
|
||||||
|
console.log('Checking green status: ' + branch);
|
||||||
|
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number:$pull_number) {
|
||||||
|
commits(last: 1) {
|
||||||
|
nodes {
|
||||||
|
commit {
|
||||||
|
statusCheckRollup {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const vars = {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: pull['number']
|
||||||
|
};
|
||||||
|
const result = await github.graphql(stateQuery, vars);
|
||||||
|
const [{ commit }] = result.repository.pullRequest.commits.nodes;
|
||||||
|
const state = commit.statusCheckRollup.state
|
||||||
|
console.log('Validating status: ' + state);
|
||||||
|
if(state != 'SUCCESS') {
|
||||||
|
console.log('Discarding ' + branch + ' with status ' + state);
|
||||||
|
statusOK = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Checking labels: ' + branch);
|
||||||
|
const labels = pull['labels'];
|
||||||
|
for(const label of labels) {
|
||||||
|
const labelName = label['name'];
|
||||||
|
console.log('Checking label: ' + labelName);
|
||||||
|
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
|
||||||
|
console.log('Discarding ' + branch + ' with label ' + labelName);
|
||||||
|
statusOK = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusOK) {
|
||||||
|
console.log('Adding branch to array: ' + branch);
|
||||||
|
const prString = '#' + pull['number'] + ' ' + pull['title'];
|
||||||
|
branchesAndPRStrings.push({ branch, prString });
|
||||||
|
baseBranch = pull['base']['ref'];
|
||||||
|
baseBranchSHA = pull['base']['sha'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (branchesAndPRStrings.length == 0) {
|
||||||
|
core.setFailed('No PRs/branches matched criteria');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await github.rest.git.createRef({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
sha: baseBranchSHA
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let combinedPRs = [];
|
||||||
|
let mergeFailedPRs = [];
|
||||||
|
for(const { branch, prString } of branchesAndPRStrings) {
|
||||||
|
try {
|
||||||
|
await github.rest.repos.merge({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
base: '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
head: branch,
|
||||||
|
});
|
||||||
|
console.log('Merged branch ' + branch);
|
||||||
|
combinedPRs.push(prString);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to merge branch ' + branch);
|
||||||
|
mergeFailedPRs.push(prString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Creating combined PR');
|
||||||
|
const combinedPRsString = combinedPRs.join('\n');
|
||||||
|
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
|
||||||
|
if(mergeFailedPRs.length > 0) {
|
||||||
|
const mergeFailedPRsString = mergeFailedPRs.join('\n');
|
||||||
|
body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
|
||||||
|
}
|
||||||
|
await github.rest.pulls.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: 'Combined PR',
|
||||||
|
head: '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
base: baseBranch,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
14
.github/workflows/deploy.yml
vendored
Normal file
14
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: Manual Deploy
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: akhileshns/heroku-deploy@v3.12.12 # This is the action
|
||||||
|
with:
|
||||||
|
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
|
||||||
|
heroku_app_name: "pyrigs" #Must be unique in Heroku
|
||||||
|
heroku_email: "aj@aronajones.com"
|
||||||
24
.github/workflows/django.yml
vendored
24
.github/workflows/django.yml
vendored
@@ -13,26 +13,20 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9.1
|
python-version: 3.9
|
||||||
- uses: actions/cache@v2
|
cache: 'pipenv'
|
||||||
id: pcache
|
|
||||||
with:
|
|
||||||
path: ~/.local/share/virtualenvs
|
|
||||||
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 pipenv
|
python3 -m pip install --upgrade pip pipenv
|
||||||
pipenv install -d
|
pipenv install -d
|
||||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||||
- name: Cache Static Files
|
- name: Cache Static Files
|
||||||
id: static-cache
|
id: static-cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: 'pipeline/built_assets'
|
path: 'pipeline/built_assets'
|
||||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||||
@@ -43,9 +37,9 @@ jobs:
|
|||||||
- name: Basic Checks
|
- name: Basic Checks
|
||||||
run: |
|
run: |
|
||||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
pipenv run pycodestyle . --exclude=migrations,node_modules
|
||||||
pipenv run python manage.py check
|
pipenv run python3 manage.py check
|
||||||
pipenv run python manage.py makemigrations --check --dry-run
|
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||||
pipenv run python manage.py collectstatic --noinput
|
pipenv run python3 manage.py collectstatic --noinput
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: pipenv run pytest -n auto -vv --cov
|
run: pipenv run pytest -n auto -vv --cov
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
|
|||||||
19
Pipfile
19
Pipfile
@@ -11,7 +11,6 @@ asgiref = "~=3.3.1"
|
|||||||
beautifulsoup4 = "~=4.9.3"
|
beautifulsoup4 = "~=4.9.3"
|
||||||
Brotli = "~=1.0.9"
|
Brotli = "~=1.0.9"
|
||||||
cachetools = "~=4.2.1"
|
cachetools = "~=4.2.1"
|
||||||
certifi = "~=2020.12.5"
|
|
||||||
chardet = "~=4.0.0"
|
chardet = "~=4.0.0"
|
||||||
configparser = "~=5.0.1"
|
configparser = "~=5.0.1"
|
||||||
contextlib2 = "~=0.6.0.post1"
|
contextlib2 = "~=0.6.0.post1"
|
||||||
@@ -20,32 +19,30 @@ cssutils = "~=1.0.2"
|
|||||||
dj-database-url = "~=0.5.0"
|
dj-database-url = "~=0.5.0"
|
||||||
dj-static = "~=0.0.6"
|
dj-static = "~=0.0.6"
|
||||||
Django = "~=3.2"
|
Django = "~=3.2"
|
||||||
django-debug-toolbar = "~=3.2"
|
django-debug-toolbar = "~=4.0.0"
|
||||||
django-filter = "~=2.4.0"
|
django-filter = "~=2.4.0"
|
||||||
django-ical = "~=1.7.1"
|
django-ical = "~=1.7.1"
|
||||||
django-recurrence = "~=1.10.3"
|
django-recurrence = "~=1.10.3"
|
||||||
django-registration-redux = "~=2.9"
|
django-registration-redux = "~=2.9"
|
||||||
django-reversion = "~=3.0.9"
|
django-reversion = "~=3.0.9"
|
||||||
django-toolbelt = "~=0.0.1"
|
|
||||||
django-widget-tweaks = "~=1.4.8"
|
django-widget-tweaks = "~=1.4.8"
|
||||||
django-htmlmin = "~=0.11.0"
|
django-htmlmin = "~=0.11.0"
|
||||||
envparse = "~=0.2.0"
|
envparse = "*"
|
||||||
gunicorn = "~=20.0.4"
|
gunicorn = "~=20.0.4"
|
||||||
icalendar = "~=4.0.7"
|
icalendar = "~=4.0.7"
|
||||||
idna = "~=2.10"
|
idna = "~=2.10"
|
||||||
lxml = "~=4.7.1"
|
|
||||||
Markdown = "~=3.3.3"
|
Markdown = "~=3.3.3"
|
||||||
msgpack = "~=1.0.2"
|
msgpack = "~=1.0.2"
|
||||||
pep517 = "~=0.9.1"
|
pep517 = "~=0.9.1"
|
||||||
Pillow = "~=9.0.0"
|
Pillow = "~=9.3.0"
|
||||||
premailer = "~=3.7.0"
|
premailer = "~=3.7.0"
|
||||||
progress = "~=1.5"
|
progress = "~=1.5"
|
||||||
psutil = "~=5.8.0"
|
psutil = "~=5.8.0"
|
||||||
psycopg2 = "~=2.8.6"
|
psycopg2 = "~=2.8.6"
|
||||||
Pygments = "~=2.7.4"
|
Pygments = "~=2.7.4"
|
||||||
pyparsing = "~=2.4.7"
|
pyparsing = "~=2.4.7"
|
||||||
PyPDF2 = "~=1.26.0"
|
PyPDF2 = "~=1.27.5"
|
||||||
PyPOM = "~=2.2.0"
|
PyPOM = "~=2.2.4"
|
||||||
python-dateutil = "~=2.8.1"
|
python-dateutil = "~=2.8.1"
|
||||||
pytoml = "~=0.1.21"
|
pytoml = "~=0.1.21"
|
||||||
pytz = "~=2020.5"
|
pytz = "~=2020.5"
|
||||||
@@ -80,10 +77,12 @@ importlib-metadata = "*"
|
|||||||
django-hcaptcha = "*"
|
django-hcaptcha = "*"
|
||||||
"z3c.rml" = "*"
|
"z3c.rml" = "*"
|
||||||
pikepdf = "*"
|
pikepdf = "*"
|
||||||
|
django-queryable-properties = "*"
|
||||||
|
django-mass-edit = "*"
|
||||||
|
selenium = "~=4.9.1"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
selenium = "~=3.141.0"
|
pycodestyle = "~=2.9.1"
|
||||||
pycodestyle = "*"
|
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
django-coverage-plugin = "*"
|
django-coverage-plugin = "*"
|
||||||
pytest-cov = "*"
|
pytest-cov = "*"
|
||||||
|
|||||||
1249
Pipfile.lock
generated
1249
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,8 @@ from RIGS import models
|
|||||||
|
|
||||||
def get_oembed(login_url, request, oembed_view, kwargs):
|
def get_oembed(login_url, request, oembed_view, kwargs):
|
||||||
context = {}
|
context = {}
|
||||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
|
context['oembed_url'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
|
||||||
reverse(oembed_view, kwargs=kwargs))
|
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
|
||||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
|
||||||
resp = render(request, 'login_redirect.html', context=context)
|
resp = render(request, 'login_redirect.html', context=context)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
|
|||||||
if oembed_view is not None:
|
if oembed_view is not None:
|
||||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
|
||||||
|
|
||||||
_checklogin.__doc__ = view_func.__doc__
|
_checklogin.__doc__ = view_func.__doc__
|
||||||
_checklogin.__dict__ = view_func.__dict__
|
_checklogin.__dict__ = view_func.__dict__
|
||||||
@@ -55,7 +54,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
|||||||
if oembed_view is not None:
|
if oembed_view is not None:
|
||||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
|
||||||
else:
|
else:
|
||||||
resp = render(request, '403.html')
|
resp = render(request, '403.html')
|
||||||
resp.status_code = 403
|
resp.status_code = 403
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ if not DEBUG:
|
|||||||
|
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
||||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
DOMAIN = env('DOMAIN', default='example.com')
|
||||||
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
|
||||||
|
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
||||||
|
|
||||||
@@ -63,17 +64,18 @@ INSTALLED_APPS = (
|
|||||||
'assets',
|
'assets',
|
||||||
'training',
|
'training',
|
||||||
|
|
||||||
'debug_toolbar',
|
# 'debug_toolbar',
|
||||||
'registration',
|
'registration',
|
||||||
'reversion',
|
'reversion',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'hcaptcha',
|
'hcaptcha',
|
||||||
|
'massadmin',
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
'reversion.middleware.RevisionMiddleware',
|
'reversion.middleware.RevisionMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from pytest_django.asserts import assertTemplateUsed, assertInHTML
|
|||||||
from PyRIGS import urls
|
from PyRIGS import urls
|
||||||
from RIGS.models import Event, Profile
|
from RIGS.models import Event, Profile
|
||||||
from assets.models import Asset
|
from assets.models import Asset
|
||||||
|
from training.tests.test_unit import get_response
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.template.defaultfilters import striptags
|
from django.template.defaultfilters import striptags
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
@@ -135,3 +136,11 @@ def test_keyholder_access(client):
|
|||||||
assertContains(response, 'View Revision History')
|
assertContains(response, 'View Revision History')
|
||||||
client.logout()
|
client.logout()
|
||||||
call_command('deleteSampleData')
|
call_command('deleteSampleData')
|
||||||
|
|
||||||
|
|
||||||
|
def test_search(admin_client, admin_user):
|
||||||
|
url = reverse('search')
|
||||||
|
response = admin_client.get(url, {'q': "Definetelynothingfoundifwesearchthis"})
|
||||||
|
assertContains(response, "No results found")
|
||||||
|
response = admin_client.get(url, {'q': admin_user.first_name})
|
||||||
|
assertContains(response, admin_user.first_name)
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ urlpatterns = [
|
|||||||
name="api_secure"),
|
name="api_secure"),
|
||||||
|
|
||||||
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
|
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
|
||||||
|
path('search/', login_required(views.Search.as_view()), name='search'),
|
||||||
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
|
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
|
||||||
|
|
||||||
path('', include('users.urls')),
|
path('', include('users.urls')),
|
||||||
|
|
||||||
|
path('admin/', include('massadmin.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")),
|
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
|
||||||
]
|
]
|
||||||
|
|||||||
146
PyRIGS/views.py
146
PyRIGS/views.py
@@ -1,7 +1,18 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import operator
|
import operator
|
||||||
from functools import reduce
|
import re
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from functools import reduce
|
||||||
|
from itertools import chain
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||||
|
from z3c.rml import rml2pdf
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
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
|
||||||
@@ -12,6 +23,8 @@ 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 django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
from assets import models as asset_models
|
from assets import models as asset_models
|
||||||
@@ -22,12 +35,20 @@ def is_ajax(request):
|
|||||||
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
||||||
|
|
||||||
|
|
||||||
|
def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||||
|
for field, model in form.related_models.items():
|
||||||
|
value = form[field].value()
|
||||||
|
if value is not None and value != '':
|
||||||
|
context[field] = model.objects.get(pk=value)
|
||||||
|
|
||||||
|
|
||||||
class Index(generic.TemplateView): # 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
|
||||||
template_name = 'index.html'
|
template_name = 'index.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['rig_count'] = models.Event.objects.rig_count()
|
context['rig_count'] = models.Event.objects.rig_count()
|
||||||
|
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now())
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -120,7 +141,7 @@ class SecureAPIRequest(generic.View):
|
|||||||
'text': o.name,
|
'text': o.name,
|
||||||
}
|
}
|
||||||
try: # See if there is a valid update URL
|
try: # See if there is a valid update URL
|
||||||
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
|
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
pass
|
pass
|
||||||
results.append(data)
|
results.append(data)
|
||||||
@@ -182,20 +203,7 @@ class GenericListView(generic.ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = self.request.GET.get('q', "")
|
object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
|
||||||
|
|
||||||
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(
|
|
||||||
phone__startswith=q) | Q(phone__endswith=q)
|
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter)
|
|
||||||
|
|
||||||
orderBy = self.request.GET.get('orderBy', "name")
|
orderBy = self.request.GET.get('orderBy', "name")
|
||||||
if orderBy != "":
|
if orderBy != "":
|
||||||
@@ -236,6 +244,53 @@ class GenericCreateView(generic.CreateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class Search(generic.ListView):
|
||||||
|
template_name = 'search_results.html'
|
||||||
|
paginate_by = 20
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context['count'] = self.count or 0
|
||||||
|
context['query'] = self.request.GET.get('q')
|
||||||
|
context['page_title'] = f"{context['count']} search results for <b>{context['query']}</b>"
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
request = self.request
|
||||||
|
query = request.GET.get('q', None)
|
||||||
|
|
||||||
|
if query is not None:
|
||||||
|
event_results = models.Event.objects.search(query)
|
||||||
|
person_results = models.Person.objects.search(query)
|
||||||
|
organisation_results = models.Organisation.objects.search(query)
|
||||||
|
venue_results = models.Venue.objects.search(query)
|
||||||
|
invoice_results = models.Invoice.objects.search(query)
|
||||||
|
asset_results = asset_models.Asset.objects.search(query)
|
||||||
|
supplier_results = asset_models.Supplier.objects.search(query)
|
||||||
|
trainee_results = training_models.Trainee.objects.search(query)
|
||||||
|
training_item_results = training_models.TrainingItem.objects.search(query)
|
||||||
|
|
||||||
|
# combine querysets
|
||||||
|
queryset_chain = chain(
|
||||||
|
event_results,
|
||||||
|
person_results,
|
||||||
|
organisation_results,
|
||||||
|
venue_results,
|
||||||
|
invoice_results,
|
||||||
|
asset_results,
|
||||||
|
supplier_results,
|
||||||
|
trainee_results,
|
||||||
|
training_item_results,
|
||||||
|
)
|
||||||
|
qs = sorted(queryset_chain,
|
||||||
|
key=lambda instance: instance.pk,
|
||||||
|
reverse=True)
|
||||||
|
self.count = len(qs) # since qs is actually a list
|
||||||
|
return qs
|
||||||
|
return models.Event.objects.none() # just an empty queryset as default
|
||||||
|
|
||||||
|
|
||||||
class SearchHelp(generic.TemplateView):
|
class SearchHelp(generic.TemplateView):
|
||||||
template_name = 'search_help.html'
|
template_name = 'search_help.html'
|
||||||
|
|
||||||
@@ -265,3 +320,62 @@ class OEmbedView(generic.View):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_info_string(user):
|
||||||
|
user_str = f"by {user.name} " if user else ""
|
||||||
|
time = timezone.now().strftime('%d/%m/%Y %H:%I')
|
||||||
|
return f"[Paperwork generated {user_str}on {time}"
|
||||||
|
|
||||||
|
|
||||||
|
def render_pdf_response(template, context, append_terms):
|
||||||
|
merger = PdfFileMerger()
|
||||||
|
rml = template.render(context)
|
||||||
|
buffer = rml2pdf.parseString(rml)
|
||||||
|
merger.append(PdfFileReader(buffer))
|
||||||
|
buffer.close()
|
||||||
|
|
||||||
|
if append_terms:
|
||||||
|
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
||||||
|
merger.append(BytesIO(terms.read()))
|
||||||
|
|
||||||
|
merged = BytesIO()
|
||||||
|
merger.write(merged)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='application/pdf')
|
||||||
|
f = context['filename']
|
||||||
|
response['Content-Disposition'] = f'filename="{f}"'
|
||||||
|
response.write(merged.getvalue())
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class PrintView(generic.View):
|
||||||
|
append_terms = False
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
|
||||||
|
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'object': obj,
|
||||||
|
'current_user': self.request.user,
|
||||||
|
'object_name': object_name,
|
||||||
|
'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]",
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
|
||||||
|
|
||||||
|
|
||||||
|
class PrintListView(generic.ListView):
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context['current_user'] = self.request.user
|
||||||
|
context['info_string'] = get_info_string(self.request.user) + "]"
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
self.object_list = self.get_queryset()
|
||||||
|
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)
|
||||||
|
|||||||
199
RIGS/admin.py
199
RIGS/admin.py
@@ -8,6 +8,7 @@ from django.db.models import Count
|
|||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.db import IntegrityError
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
@@ -19,19 +20,142 @@ admin.site.register(models.VatRate, VersionAdmin)
|
|||||||
admin.site.register(models.Event, VersionAdmin)
|
admin.site.register(models.Event, VersionAdmin)
|
||||||
admin.site.register(models.EventItem, VersionAdmin)
|
admin.site.register(models.EventItem, VersionAdmin)
|
||||||
admin.site.register(models.Invoice, VersionAdmin)
|
admin.site.register(models.Invoice, VersionAdmin)
|
||||||
|
admin.site.register(models.EventCheckIn)
|
||||||
|
|
||||||
|
|
||||||
def approve_user(modeladmin, request, queryset):
|
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
||||||
queryset.update(is_approved=True)
|
def merge_model_instances(primary_object, alias_objects):
|
||||||
|
"""
|
||||||
|
Merge several model instances into one, the `primary_object`.
|
||||||
|
Use this function to merge model objects and migrate all of the related
|
||||||
|
fields from the alias objects the primary object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get related fields
|
||||||
|
related_fields = list(filter(
|
||||||
|
lambda x: x.is_relation is True,
|
||||||
|
primary_object._meta.get_fields()))
|
||||||
|
|
||||||
|
many_to_many_fields = list(filter(
|
||||||
|
lambda x: x.many_to_many is True, related_fields))
|
||||||
|
|
||||||
|
related_fields = list(filter(
|
||||||
|
lambda x: x.many_to_many is False, related_fields))
|
||||||
|
|
||||||
|
# Loop through all alias objects and migrate their references to the
|
||||||
|
# primary object
|
||||||
|
deleted_objects = []
|
||||||
|
deleted_objects_count = 0
|
||||||
|
for alias_object in alias_objects:
|
||||||
|
# Migrate all foreign key references from alias object to primary
|
||||||
|
# object.
|
||||||
|
for many_to_many_field in many_to_many_fields:
|
||||||
|
alias_varname = many_to_many_field.name
|
||||||
|
related_objects = getattr(alias_object, alias_varname)
|
||||||
|
for obj in related_objects.all():
|
||||||
|
try:
|
||||||
|
# Handle regular M2M relationships.
|
||||||
|
getattr(alias_object, alias_varname).remove(obj)
|
||||||
|
getattr(primary_object, alias_varname).add(obj)
|
||||||
|
except AttributeError:
|
||||||
|
# Handle M2M relationships with a 'through' model.
|
||||||
|
# This does not delete the 'through model.
|
||||||
|
# TODO: Allow the user to delete a duplicate 'through' model.
|
||||||
|
through_model = getattr(alias_object, alias_varname).through
|
||||||
|
kwargs = {
|
||||||
|
many_to_many_field.m2m_reverse_field_name(): obj,
|
||||||
|
many_to_many_field.m2m_field_name(): alias_object,
|
||||||
|
}
|
||||||
|
through_model_instances = through_model.objects.filter(**kwargs)
|
||||||
|
for instance in through_model_instances:
|
||||||
|
# Re-attach the through model to the primary_object
|
||||||
|
setattr(
|
||||||
|
instance,
|
||||||
|
many_to_many_field.m2m_field_name(),
|
||||||
|
primary_object)
|
||||||
|
instance.save()
|
||||||
|
# TODO: Here, try to delete duplicate instances that are
|
||||||
|
# disallowed by a unique_together constraint
|
||||||
|
|
||||||
|
for related_field in related_fields:
|
||||||
|
if related_field.one_to_many:
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
alias_varname = related_field.get_accessor_name()
|
||||||
|
related_objects = getattr(alias_object, alias_varname)
|
||||||
|
for obj in related_objects.all():
|
||||||
|
field_name = related_field.field.name
|
||||||
|
setattr(obj, field_name, primary_object)
|
||||||
|
obj.save()
|
||||||
|
except IntegrityError:
|
||||||
|
pass # Skip to avoid integrity error from unique_together
|
||||||
|
elif related_field.one_to_one or related_field.many_to_one:
|
||||||
|
alias_varname = related_field.name
|
||||||
|
if hasattr(alias_object, alias_varname):
|
||||||
|
related_object = getattr(alias_object, alias_varname)
|
||||||
|
primary_related_object = getattr(primary_object, alias_varname)
|
||||||
|
if primary_related_object is None:
|
||||||
|
setattr(primary_object, alias_varname, related_object)
|
||||||
|
primary_object.save()
|
||||||
|
elif related_field.one_to_one:
|
||||||
|
related_object.delete()
|
||||||
|
|
||||||
|
if alias_object.id:
|
||||||
|
deleted_objects += [alias_object]
|
||||||
|
alias_object.delete()
|
||||||
|
deleted_objects_count += 1
|
||||||
|
|
||||||
|
return primary_object, deleted_objects, deleted_objects_count
|
||||||
|
|
||||||
|
|
||||||
approve_user.short_description = "Approve selected users"
|
class AssociateAdmin(VersionAdmin):
|
||||||
|
search_fields = ['id', 'name']
|
||||||
|
list_display_links = ['id', 'name']
|
||||||
|
actions = ['merge']
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super().get_queryset(request).annotate(event_count=Count('event'))
|
||||||
|
|
||||||
|
def number_of_events(self, obj):
|
||||||
|
return obj.latest_events.count()
|
||||||
|
|
||||||
|
number_of_events.admin_order_field = 'event_count'
|
||||||
|
|
||||||
|
def merge(self, request, queryset):
|
||||||
|
if request.POST.get('post'): # Has the user confirmed which is the master record?
|
||||||
|
try:
|
||||||
|
master_object_pk = request.POST.get('master')
|
||||||
|
master_object = queryset.get(pk=master_object_pk)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all())
|
||||||
|
reversion.set_comment('Merging Objects')
|
||||||
|
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
|
||||||
|
else: # Present the confirmation screen
|
||||||
|
class TempForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = queryset.model
|
||||||
|
fields = self.merge_fields
|
||||||
|
|
||||||
|
forms = []
|
||||||
|
for obj in queryset:
|
||||||
|
forms.append(TempForm(instance=obj))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'title': _("Are you sure?"),
|
||||||
|
'queryset': queryset,
|
||||||
|
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||||
|
'forms': forms
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Profile)
|
@admin.register(models.Profile)
|
||||||
class ProfileAdmin(UserAdmin):
|
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
||||||
# Don't know how to add 'is_approved' whilst preserving the default list...
|
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
|
||||||
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
|
list_display_links = ['username']
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
(_('Personal info'), {
|
(_('Personal info'), {
|
||||||
@@ -49,62 +173,12 @@ class ProfileAdmin(UserAdmin):
|
|||||||
)
|
)
|
||||||
form = user_forms.ProfileChangeForm
|
form = user_forms.ProfileChangeForm
|
||||||
add_form = user_forms.ProfileCreationForm
|
add_form = user_forms.ProfileCreationForm
|
||||||
actions = [approve_user]
|
actions = ['approve_user', 'merge']
|
||||||
|
|
||||||
|
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
|
||||||
|
|
||||||
class AssociateAdmin(VersionAdmin):
|
def approve_user(modeladmin, request, queryset):
|
||||||
list_display = ('id', 'name', 'number_of_events')
|
queryset.update(is_approved=True)
|
||||||
search_fields = ['id', 'name']
|
|
||||||
list_display_links = ['id', 'name']
|
|
||||||
actions = ['merge']
|
|
||||||
|
|
||||||
merge_fields = ['name']
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
|
|
||||||
|
|
||||||
def number_of_events(self, obj):
|
|
||||||
return obj.latest_events.count()
|
|
||||||
|
|
||||||
number_of_events.admin_order_field = 'event_count'
|
|
||||||
|
|
||||||
def merge(self, request, queryset):
|
|
||||||
if request.POST.get('post'): # Has the user confirmed which is the master record?
|
|
||||||
try:
|
|
||||||
masterObjectPk = request.POST.get('master')
|
|
||||||
masterObject = queryset.get(pk=masterObjectPk)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
|
|
||||||
return
|
|
||||||
|
|
||||||
with transaction.atomic(), reversion.create_revision():
|
|
||||||
for obj in queryset.exclude(pk=masterObjectPk):
|
|
||||||
events = obj.event_set.all()
|
|
||||||
for event in events:
|
|
||||||
masterObject.event_set.add(event)
|
|
||||||
obj.delete()
|
|
||||||
reversion.set_comment('Merging Objects')
|
|
||||||
|
|
||||||
self.message_user(request, "Objects successfully merged.")
|
|
||||||
return
|
|
||||||
else: # Present the confirmation screen
|
|
||||||
|
|
||||||
class TempForm(ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = queryset.model
|
|
||||||
fields = self.merge_fields
|
|
||||||
|
|
||||||
forms = []
|
|
||||||
for obj in queryset:
|
|
||||||
forms.append(TempForm(instance=obj))
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'title': _("Are you sure?"),
|
|
||||||
'queryset': queryset,
|
|
||||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
|
||||||
'forms': forms
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Person)
|
@admin.register(models.Person)
|
||||||
@@ -133,3 +207,8 @@ class RiskAssessmentAdmin(VersionAdmin):
|
|||||||
@admin.register(models.EventChecklist)
|
@admin.register(models.EventChecklist)
|
||||||
class EventChecklistAdmin(VersionAdmin):
|
class EventChecklistAdmin(VersionAdmin):
|
||||||
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.PowerTestRecord)
|
||||||
|
class EventChecklistAdmin(VersionAdmin):
|
||||||
|
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
||||||
|
|||||||
119
RIGS/forms.py
119
RIGS/forms.py
@@ -44,7 +44,7 @@ class EventForm(forms.ModelForm):
|
|||||||
return simplejson.dumps(items)
|
return simplejson.dumps(items)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(EventForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['items_json'].initial = self._get_items_json
|
self.fields['items_json'].initial = self._get_items_json
|
||||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
||||||
@@ -153,6 +153,10 @@ class EventAuthorisationRequestForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentForm(forms.ModelForm):
|
class EventRiskAssessmentForm(forms.ModelForm):
|
||||||
|
related_models = {
|
||||||
|
'power_mic': models.Profile,
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
@@ -172,9 +176,9 @@ class EventRiskAssessmentForm(forms.ModelForm):
|
|||||||
unexpected_values = []
|
unexpected_values = []
|
||||||
for field, value in models.RiskAssessment.expected_values.items():
|
for field, value in models.RiskAssessment.expected_values.items():
|
||||||
if self.cleaned_data.get(field) != value:
|
if self.cleaned_data.get(field) != value:
|
||||||
unexpected_values.append("<li>{}</li>".format(self._meta.model._meta.get_field(field).help_text))
|
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
|
||||||
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
|
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
|
||||||
raise forms.ValidationError("Your answers to these questions: <ul>{}</ul> require consulting with a supervisor.".format(''.join([str(elem) for elem in unexpected_values])), code='unusual_answers')
|
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
|
||||||
return super(EventRiskAssessmentForm, self).clean()
|
return super(EventRiskAssessmentForm, self).clean()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -196,86 +200,41 @@ class EventChecklistForm(forms.ModelForm):
|
|||||||
|
|
||||||
related_models = {
|
related_models = {
|
||||||
'venue': models.Venue,
|
'venue': models.Venue,
|
||||||
'power_mic': models.Profile,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Two possible formats
|
|
||||||
def parsedatetime(self, date_string):
|
|
||||||
try:
|
|
||||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
|
|
||||||
except ValueError:
|
|
||||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
|
|
||||||
|
|
||||||
# There's probably a thousand better ways to do this, but this one is mine
|
|
||||||
def clean(self):
|
|
||||||
vehicles = {key: val for key, val in self.data.items()
|
|
||||||
if key.startswith('vehicle')}
|
|
||||||
for key in vehicles:
|
|
||||||
pk = int(key.split('_')[1])
|
|
||||||
driver_key = 'driver_' + str(pk)
|
|
||||||
if(self.data[driver_key] == ''):
|
|
||||||
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
item = models.EventChecklistVehicle.objects.get(pk=pk)
|
|
||||||
except models.EventChecklistVehicle.DoesNotExist:
|
|
||||||
item = models.EventChecklistVehicle()
|
|
||||||
|
|
||||||
item.vehicle = vehicles['vehicle_' + str(pk)]
|
|
||||||
item.driver = models.Profile.objects.get(pk=self.data[driver_key])
|
|
||||||
item.full_clean('checklist')
|
|
||||||
|
|
||||||
# item does not have a database pk yet as it isn't saved
|
|
||||||
self.items['v' + str(pk)] = item
|
|
||||||
|
|
||||||
crewmembers = {key: val for key, val in self.data.items()
|
|
||||||
if key.startswith('crewmember')}
|
|
||||||
other_fields = ['start', 'role', 'end']
|
|
||||||
for key in crewmembers:
|
|
||||||
pk = int(key.split('_')[1])
|
|
||||||
|
|
||||||
for field in other_fields:
|
|
||||||
value = self.data['{}_{}'.format(field, pk)]
|
|
||||||
if value == '':
|
|
||||||
raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field))
|
|
||||||
|
|
||||||
try:
|
|
||||||
item = models.EventChecklistCrew.objects.get(pk=pk)
|
|
||||||
except models.EventChecklistCrew.DoesNotExist:
|
|
||||||
item = models.EventChecklistCrew()
|
|
||||||
|
|
||||||
item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)])
|
|
||||||
item.start = self.parsedatetime(self.data['start_' + str(pk)])
|
|
||||||
item.role = self.data['role_' + str(pk)]
|
|
||||||
item.end = self.parsedatetime(self.data['end_' + str(pk)])
|
|
||||||
item.full_clean('checklist')
|
|
||||||
|
|
||||||
# item does not have a database pk yet as it isn't saved
|
|
||||||
self.items['c' + str(pk)] = item
|
|
||||||
|
|
||||||
return super(EventChecklistForm, self).clean()
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
checklist = super(EventChecklistForm, self).save(commit=False)
|
|
||||||
if (commit):
|
|
||||||
# Remove all existing, to be recreated from the form
|
|
||||||
checklist.vehicles.all().delete()
|
|
||||||
checklist.crew.all().delete()
|
|
||||||
checklist.save()
|
|
||||||
|
|
||||||
for key in self.items:
|
|
||||||
item = self.items[key]
|
|
||||||
reversion.add_to_revision(item)
|
|
||||||
# finish and save new database items
|
|
||||||
item.checklist = checklist
|
|
||||||
item.full_clean()
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
self.items.clear()
|
|
||||||
|
|
||||||
return checklist
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
exclude = ['reviewed_at', 'reviewed_by']
|
exclude = ['reviewed_at', 'reviewed_by']
|
||||||
|
|
||||||
|
|
||||||
|
class PowerTestRecordForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for name, field in self.fields.items():
|
||||||
|
if field.__class__ == forms.NullBooleanField:
|
||||||
|
# Only display yes/no to user, the 'none' is only ever set in the background
|
||||||
|
field.widget = forms.CheckboxInput()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.PowerTestRecord
|
||||||
|
fields = '__all__'
|
||||||
|
exclude = ['reviewed_at', 'reviewed_by']
|
||||||
|
|
||||||
|
|
||||||
|
class EventCheckInForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['time'].initial = timezone.now()
|
||||||
|
self.fields['role'].initial = "Crew"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.EventCheckIn
|
||||||
|
fields = '__all__'
|
||||||
|
exclude = ['end_time']
|
||||||
|
|
||||||
|
|
||||||
|
class EditCheckInForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.EventCheckIn
|
||||||
|
fields = '__all__'
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ 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=new_event, power_mic=random.choice(self.profiles),
|
models.EventChecklist.objects.create(event=new_event,
|
||||||
safe_parking=bool(random.getrandbits(1)),
|
safe_parking=bool(random.getrandbits(1)),
|
||||||
safe_packing=bool(random.getrandbits(1)),
|
safe_packing=bool(random.getrandbits(1)),
|
||||||
exits=bool(random.getrandbits(1)),
|
exits=bool(random.getrandbits(1)),
|
||||||
@@ -287,6 +287,4 @@ class Command(BaseCommand):
|
|||||||
ear_plugs=bool(random.getrandbits(1)),
|
ear_plugs=bool(random.getrandbits(1)),
|
||||||
hs_location="Locked away safely",
|
hs_location="Locked away safely",
|
||||||
extinguishers_location="Somewhere, I forgot",
|
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))
|
||||||
|
|||||||
38
RIGS/management/commands/send_reminders.py
Normal file
38
RIGS/management/commands/send_reminders.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import premailer
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
events = models.Event.objects.current_events().select_related('riskassessment')
|
||||||
|
for event in events:
|
||||||
|
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
|
||||||
|
# 48 hours = 172800 seconds
|
||||||
|
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
|
||||||
|
context = {
|
||||||
|
"event": event,
|
||||||
|
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
|
||||||
|
}
|
||||||
|
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
f"{event} - Risk Assessment Incomplete",
|
||||||
|
get_template("email/ra_reminder.txt").render(context),
|
||||||
|
to=[target],
|
||||||
|
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
|
||||||
|
)
|
||||||
|
css = finders.find('css/email.css')
|
||||||
|
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
|
||||||
|
msg.attach_alternative(html, 'text/html')
|
||||||
|
msg.send()
|
||||||
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-10-20 23:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0044_profile_is_supervisor'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='is_approved',
|
||||||
|
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
71
RIGS/migrations/0046_create_powertests.py
Normal file
71
RIGS/migrations/0046_create_powertests.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2023-05-08 15:58
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import versioning.versioning
|
||||||
|
|
||||||
|
def migrate_old_data(apps, schema_editor):
|
||||||
|
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
||||||
|
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
|
||||||
|
for ec in EventChecklist.objects.all():
|
||||||
|
# New highscore for the most pythonic BS I've ever written.
|
||||||
|
PowerTestRecord.objects.create(event=ec.event, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created)})
|
||||||
|
|
||||||
|
|
||||||
|
def revert(apps, schema_editor):
|
||||||
|
apps.get_model('RIGS', 'PowerTestRecord').objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0045_alter_profile_is_approved'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PowerTestRecord',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_tests', to='RIGS.event')),
|
||||||
|
('notes', models.TextField(blank=True, default='')),
|
||||||
|
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue')),
|
||||||
|
('reviewed_at', models.DateTimeField(null=True)),
|
||||||
|
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
|
||||||
|
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
|
||||||
|
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
|
||||||
|
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
|
||||||
|
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
|
||||||
|
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
|
||||||
|
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
|
||||||
|
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
|
||||||
|
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
|
||||||
|
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
|
||||||
|
('fd_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||||
|
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
|
||||||
|
('w1_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||||
|
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||||
|
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||||
|
('w1_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||||
|
('w2_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||||
|
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||||
|
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||||
|
('w2_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||||
|
('w3_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||||
|
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||||
|
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||||
|
('w3_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||||
|
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
|
||||||
|
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
|
||||||
|
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'ordering': ['event'],
|
||||||
|
'permissions': [('review_power', 'Can review Power Test Records')],
|
||||||
|
},
|
||||||
|
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
||||||
|
]
|
||||||
41
RIGS/migrations/0047_auto_20230517_0944.py
Normal file
41
RIGS/migrations/0047_auto_20230517_0944.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-17 08:44
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_old_data(apps, schema_editor):
|
||||||
|
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
||||||
|
EventCheckIn = apps.get_model('RIGS', 'EventCheckIn')
|
||||||
|
for ec in EventChecklist.objects.all():
|
||||||
|
for crew in ec.crew.all():
|
||||||
|
vehicle = ec.vehicles.get(driver=crew.crewmember) or None
|
||||||
|
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=vehicle.vehicle)
|
||||||
|
|
||||||
|
|
||||||
|
def revert(apps, schema_editor):
|
||||||
|
apps.get_model('RIGS', 'EventCheckIn').objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0046_create_powertests'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventCheckIn',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('time', models.DateTimeField()),
|
||||||
|
('role', models.CharField(blank=True, max_length=50)),
|
||||||
|
('vehicle', models.CharField(blank=True, max_length=100)),
|
||||||
|
('end_time', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.event')),
|
||||||
|
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
||||||
|
]
|
||||||
156
RIGS/migrations/0048_auto_20230518_1256.py
Normal file
156
RIGS/migrations/0048_auto_20230518_1256.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-18 11:56
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0047_auto_20230517_0944'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklistvehicle',
|
||||||
|
name='checklist',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklistvehicle',
|
||||||
|
name='driver',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='all_rcds_tested',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='earthing',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_phase_rotation',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_pssc',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_voltage_l1',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_voltage_l2',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_voltage_l3',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='labelling',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='pat',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='power_mic',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='public_sockets_tested',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='rcds',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='source_rcd',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='supply_test',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_description',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_polarity',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_voltage',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_description',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_polarity',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_voltage',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_description',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_polarity',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_voltage',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='power_mic',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='reviewed_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='reviewed_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='riskassessment',
|
||||||
|
name='reviewed_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='EventChecklistCrew',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='EventChecklistVehicle',
|
||||||
|
),
|
||||||
|
]
|
||||||
254
RIGS/models.py
254
RIGS/models.py
@@ -8,6 +8,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.db.models import Q, F
|
||||||
from django.conf import settings
|
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
|
||||||
@@ -20,11 +21,22 @@ from reversion.models import Version
|
|||||||
from versioning.versioning import RevisionMixin
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_pk(filt, query):
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
filt = filt | Q(pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
return filt
|
||||||
|
|
||||||
|
|
||||||
class Profile(AbstractUser):
|
class Profile(AbstractUser):
|
||||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||||
phone = models.CharField(max_length=13, blank=True, default='')
|
phone = models.CharField(max_length=13, blank=True, default='')
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||||
is_approved = models.BooleanField(default=False)
|
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
||||||
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||||
dark_theme = models.BooleanField(default=False)
|
dark_theme = models.BooleanField(default=False)
|
||||||
@@ -51,7 +63,7 @@ class Profile(AbstractUser):
|
|||||||
def name(self):
|
def name(self):
|
||||||
name = self.get_full_name()
|
name = self.get_full_name()
|
||||||
if self.initials:
|
if self.initials:
|
||||||
name += ' "{}"'.format(self.initials)
|
name += f' "{self.initials}"'
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -69,16 +81,33 @@ class Profile(AbstractUser):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def current_event(self):
|
||||||
|
q = EventCheckIn.objects.filter(person=self, end_time=None)
|
||||||
|
return q.latest('time') if q.exists() else None
|
||||||
|
|
||||||
|
|
||||||
|
class ContactableManager(models.Manager):
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
|
||||||
|
phone__startswith=query) | Q(phone__endswith=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
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, default='')
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
email = models.EmailField(blank=True, default='')
|
email = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
address = models.TextField(blank=True, default='')
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
notes = models.TextField(blank=True, default='')
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
string = self.name
|
string = self.name
|
||||||
if self.notes is not None:
|
if self.notes is not None:
|
||||||
@@ -110,12 +139,12 @@ 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, default='')
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
email = models.EmailField(blank=True, default='')
|
email = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
address = models.TextField(blank=True, default='')
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
notes = models.TextField(blank=True, default='')
|
notes = models.TextField(blank=True, default='')
|
||||||
union_account = models.BooleanField(default=False)
|
union_account = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
string = self.name
|
string = self.name
|
||||||
if self.notes is not None:
|
if self.notes is not None:
|
||||||
@@ -184,9 +213,10 @@ class Venue(models.Model, RevisionMixin):
|
|||||||
email = models.EmailField(blank=True, default='')
|
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, default='')
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
address = models.TextField(blank=True, default='')
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
string = self.name
|
string = self.name
|
||||||
if self.notes and len(self.notes) > 0:
|
if self.notes and len(self.notes) > 0:
|
||||||
@@ -260,6 +290,23 @@ class EventManager(models.Manager):
|
|||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(notes__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is N###### then do a simple PK filter
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
@reversion.register(follow=['items'])
|
||||||
class Event(models.Model, RevisionMixin):
|
class Event(models.Model, RevisionMixin):
|
||||||
@@ -314,10 +361,8 @@ class Event(models.Model, RevisionMixin):
|
|||||||
def display_id(self):
|
def display_id(self):
|
||||||
if self.pk:
|
if self.pk:
|
||||||
if self.is_rig:
|
if self.is_rig:
|
||||||
return str("N%05d" % self.pk)
|
return f"N{self.pk:05d}"
|
||||||
|
|
||||||
return self.pk
|
return self.pk
|
||||||
|
|
||||||
return "????"
|
return "????"
|
||||||
|
|
||||||
# Calculated values
|
# Calculated values
|
||||||
@@ -364,7 +409,15 @@ class Event(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def hs_done(self):
|
def hs_done(self):
|
||||||
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
return self.riskassessment is not None and self.has_checklist and self.has_power
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_checklist(self):
|
||||||
|
return self.checklists.exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_power(self):
|
||||||
|
return self.power_tests.exists()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_start_time(self):
|
def has_start_time(self):
|
||||||
@@ -437,6 +490,15 @@ class Event(models.Model, RevisionMixin):
|
|||||||
else:
|
else:
|
||||||
return bool(self.purchase_order)
|
return bool(self.purchase_order)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_check_in(self):
|
||||||
|
earliest = self.earliest_time
|
||||||
|
if isinstance(self.earliest_time, datetime.date):
|
||||||
|
earliest = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
earliest = tz.localize(earliest)
|
||||||
|
return not self.dry_hire and earliest <= timezone.now()
|
||||||
|
|
||||||
objects = EventManager()
|
objects = EventManager()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@@ -530,6 +592,34 @@ class InvoiceManager(models.Manager):
|
|||||||
query = self.raw(sql)
|
query = self.raw(sql)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(event__name__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
or_lookup = or_lookup | Q(event__pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
||||||
|
elif query[0] == "#":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['payment_set'])
|
@reversion.register(follow=['payment_set'])
|
||||||
class Invoice(models.Model, RevisionMixin):
|
class Invoice(models.Model, RevisionMixin):
|
||||||
@@ -569,14 +659,14 @@ class Invoice(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def activity_feed_string(self):
|
def activity_feed_string(self):
|
||||||
return f"#{self.display_id} for Event {self.event.display_id}"
|
return f"{self.display_id} for Event {self.event.display_id}"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_id(self):
|
def display_id(self):
|
||||||
return "{:05d}".format(self.pk)
|
return f"#{self.pk:05d}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-invoice_date']
|
ordering = ['-invoice_date']
|
||||||
@@ -620,8 +710,21 @@ def validate_url(value):
|
|||||||
raise ValidationError('URL must point to a location on the TEC Sharepoint')
|
raise ValidationError('URL must point to a location on the TEC Sharepoint')
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewableModel(models.Model):
|
||||||
|
reviewed_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||||
|
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@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]
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
class RiskAssessment(models.Model, RevisionMixin):
|
class RiskAssessment(ReviewableModel, RevisionMixin):
|
||||||
SMALL = (0, 'Small')
|
SMALL = (0, 'Small')
|
||||||
MEDIUM = (1, 'Medium')
|
MEDIUM = (1, 'Medium')
|
||||||
LARGE = (2, 'Large')
|
LARGE = (2, 'Large')
|
||||||
@@ -669,10 +772,6 @@ class RiskAssessment(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
# Blimey that was a lot of options
|
# Blimey that was a lot of options
|
||||||
|
|
||||||
reviewed_at = models.DateTimeField(null=True)
|
|
||||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
|
||||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
supervisor_consulted = models.BooleanField(null=True)
|
supervisor_consulted = models.BooleanField(null=True)
|
||||||
|
|
||||||
expected_values = {
|
expected_values = {
|
||||||
@@ -681,7 +780,7 @@ class RiskAssessment(models.Model, RevisionMixin):
|
|||||||
'contractors': False,
|
'contractors': False,
|
||||||
'other_companies': False,
|
'other_companies': False,
|
||||||
'crew_fatigue': False,
|
'crew_fatigue': False,
|
||||||
'big_power': False,
|
# 'big_power': False Doesn't require checking with a super either way
|
||||||
'generators': False,
|
'generators': False,
|
||||||
'other_companies_power': False,
|
'other_companies_power': False,
|
||||||
'nonstandard_equipment_power': False,
|
'nonstandard_equipment_power': False,
|
||||||
@@ -709,10 +808,6 @@ 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,24 +818,29 @@ class RiskAssessment(models.Model, RevisionMixin):
|
|||||||
else:
|
else:
|
||||||
return self.SMALL[0]
|
return self.SMALL[0]
|
||||||
|
|
||||||
@property
|
def get_event_size_display(self):
|
||||||
def activity_feed_string(self):
|
return self.SIZES[self.event_size][1] + " Event"
|
||||||
return str(self.event)
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk} | {self.event}"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ra_detail', kwargs={'pk': self.pk})
|
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def __str__(self):
|
@property
|
||||||
return "%i - %s" % (self.pk, self.event)
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['vehicles', 'crew'])
|
@reversion.register
|
||||||
class EventChecklist(models.Model, RevisionMixin):
|
class EventChecklist(ReviewableModel, RevisionMixin):
|
||||||
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
||||||
|
|
||||||
# General
|
# General
|
||||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
|
|
||||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
|
|
||||||
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
|
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
|
|
||||||
@@ -754,6 +854,32 @@ class EventChecklist(models.Model, RevisionMixin):
|
|||||||
hs_location = models.CharField(blank=True, default='', 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, default='', 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")
|
||||||
|
|
||||||
|
inverted_fields = []
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['event']
|
||||||
|
permissions = [
|
||||||
|
('review_eventchecklist', 'Can review Event Checklists')
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk} - {self.event}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class PowerTestRecord(ReviewableModel, RevisionMixin):
|
||||||
|
event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE)
|
||||||
|
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
|
||||||
|
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
|
||||||
|
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
# 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?")
|
||||||
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
||||||
@@ -789,58 +915,40 @@ class EventChecklist(models.Model, RevisionMixin):
|
|||||||
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
||||||
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
||||||
|
|
||||||
reviewed_at = models.DateTimeField(null=True)
|
|
||||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
|
||||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
inverted_fields = []
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['event']
|
ordering = ['event']
|
||||||
permissions = [
|
permissions = [
|
||||||
('review_eventchecklist', 'Can review Event Checklists')
|
('review_power', 'Can review Power Test Records')
|
||||||
]
|
]
|
||||||
|
|
||||||
@cached_property
|
def __str__(self):
|
||||||
def fieldz(self):
|
return f"{self.pk} - {self.event}"
|
||||||
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):
|
|
||||||
return reverse('ec_detail', kwargs={'pk': self.pk})
|
class EventCheckIn(models.Model):
|
||||||
|
event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)
|
||||||
|
person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkins', on_delete=models.CASCADE)
|
||||||
|
time = models.DateTimeField()
|
||||||
|
role = models.CharField(max_length=50, blank=True)
|
||||||
|
vehicle = models.CharField(max_length=100, blank=True)
|
||||||
|
end_time = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%i - %s" % (self.pk, self.event)
|
return f"{self.person} on {self.event}"
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventChecklistVehicle(models.Model, RevisionMixin):
|
|
||||||
checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE)
|
|
||||||
vehicle = models.CharField(max_length=255)
|
|
||||||
driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{} driven by {}".format(self.vehicle, str(self.driver))
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventChecklistCrew(models.Model, RevisionMixin):
|
|
||||||
checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE)
|
|
||||||
crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE)
|
|
||||||
role = models.CharField(max_length=255)
|
|
||||||
start = models.DateTimeField()
|
|
||||||
end = models.DateTimeField()
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.start > self.end:
|
sass = " Please invent time travel and retry."
|
||||||
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
|
if self.time > timezone.now():
|
||||||
|
raise ValidationError("May not check in in the future." + sass)
|
||||||
|
if self.end_time and self.end_time < self.time:
|
||||||
|
raise ValidationError("May not check out before you've checked in." + sass)
|
||||||
|
|
||||||
def __str__(self):
|
def get_absolute_url(self):
|
||||||
return "{} ({})".format(str(self.crewmember), self.role)
|
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||||
|
|
||||||
|
def active(self):
|
||||||
|
return end_time is not None
|
||||||
|
|||||||
@@ -54,23 +54,23 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
||||||
context['to_name'] = instance.event.organisation.name
|
context['to_name'] = instance.event.organisation.name
|
||||||
|
|
||||||
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
|
subject = f"{instance.event.display_id} | {instance.event.name} - Event Authorised"
|
||||||
|
|
||||||
client_email = EmailMultiAlternatives(
|
client_email = EmailMultiAlternatives(
|
||||||
subject,
|
subject,
|
||||||
get_template("eventauthorisation_client_success.txt").render(context),
|
get_template("email/eventauthorisation_client_success.txt").render(context),
|
||||||
to=[instance.email],
|
to=[instance.email],
|
||||||
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
||||||
)
|
)
|
||||||
|
|
||||||
css = finders.find('css/email.css')
|
css = finders.find('css/email.css')
|
||||||
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
|
html = Premailer(get_template("email/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')
|
||||||
|
|
||||||
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
||||||
|
|
||||||
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
|
client_email.attach(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf',
|
||||||
merged.getvalue(),
|
merged.getvalue(),
|
||||||
'application/pdf'
|
'application/pdf'
|
||||||
)
|
)
|
||||||
@@ -82,7 +82,7 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
|
|
||||||
mic_email = EmailMessage(
|
mic_email = EmailMessage(
|
||||||
subject,
|
subject,
|
||||||
get_template("eventauthorisation_mic_success.txt").render(context),
|
get_template("email/eventauthorisation_mic_success.txt").render(context),
|
||||||
to=[mic_email_address]
|
to=[mic_email_address]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,13 +116,13 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|||||||
}
|
}
|
||||||
|
|
||||||
email = EmailMultiAlternatives(
|
email = EmailMultiAlternatives(
|
||||||
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
|
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
||||||
get_template("admin_awaiting_approval.txt").render(context),
|
get_template("email/admin_awaiting_approval.txt").render(context),
|
||||||
to=[admin.email],
|
to=[admin.email],
|
||||||
reply_to=[user.email],
|
reply_to=[user.email],
|
||||||
)
|
)
|
||||||
css = finders.find('css/email.css')
|
css = finders.find('css/email.css')
|
||||||
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
html = Premailer(get_template("email/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')
|
||||||
email.send()
|
email.send()
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 63 KiB |
142
RIGS/templates/base_print.xml
Normal file
142
RIGS/templates/base_print.xml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE document SYSTEM "rml.dtd">
|
||||||
|
<document filename="{{filename}}">
|
||||||
|
<docinit>
|
||||||
|
<registerTTFont faceName="OpenSans" fileName="static/fonts/OpenSans-Regular.tff"/>
|
||||||
|
<registerTTFont faceName="OpenSans-Bold" fileName="static/fonts/OpenSans-Bold.tff"/>
|
||||||
|
<registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/>
|
||||||
|
</docinit>
|
||||||
|
|
||||||
|
<stylesheet>
|
||||||
|
<initialize>
|
||||||
|
<color id="LightGray" RGB="#D3D3D3"/>
|
||||||
|
<color id="DarkGray" RGB="#707070"/>
|
||||||
|
<color id="Brand" RGB="#3853a4"/>
|
||||||
|
</initialize>
|
||||||
|
|
||||||
|
<paraStyle name="style.para" fontName="OpenSans" />
|
||||||
|
<paraStyle name="blockPara" spaceAfter="5" spaceBefore="5"/>
|
||||||
|
<paraStyle name="style.Heading1" fontName="OpenSans" fontSize="16" leading="18" spaceAfter="0"/>
|
||||||
|
<paraStyle name="style.Heading2" fontName="OpenSans-Bold" fontSize="10" spaceAfter="2"/>
|
||||||
|
<paraStyle name="style.Heading3" fontName="OpenSans" fontSize="10" spaceAfter="0"/>
|
||||||
|
<paraStyle name="center" alignment="center"/>
|
||||||
|
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
|
||||||
|
|
||||||
|
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
|
||||||
|
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
|
||||||
|
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
|
||||||
|
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
|
||||||
|
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
|
||||||
|
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
|
||||||
|
<paraStyle name="style.emheader" fontName="OpenSans" textColor="White" fontSize="12" backColor="Brand" leading="20" borderPadding="4"/>
|
||||||
|
<paraStyle name="style.breakbefore" parent="emheader" pageBreakBefore="1"/>
|
||||||
|
|
||||||
|
<blockTableStyle id="eventSpecifics">
|
||||||
|
<blockValign value="top"/>
|
||||||
|
<lineStyle kind="LINEAFTER" colorName="LightGrey" start="0,0" stop="1,0" thickness="1"/>
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<blockTableStyle id="headLayout">
|
||||||
|
<blockValign value="top"/>
|
||||||
|
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<blockTableStyle id="eventDetails">
|
||||||
|
<blockValign value="top"/>
|
||||||
|
<blockTopPadding start="0,0" stop="-1,0" length="0"/>
|
||||||
|
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<blockTableStyle id="itemTable">
|
||||||
|
<blockValign value="top"/>
|
||||||
|
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="0,0" stop="-1,-1" thickness="1"/>
|
||||||
|
{#<lineStyle kind="box" colorName="black" thickness="1" start="0,0" stop="-1,-1"/>#}
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<blockTableStyle id="totalTable">
|
||||||
|
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
|
||||||
|
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="-2,0" stop="-1,-1" thickness="1"/>
|
||||||
|
{# <lineStyle cap="default" kind="grid" colorName="black" thickness="1" start="1,0" stop="-1,-1"/> #}
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<blockTableStyle id="infoTable" keepWithNext="true">
|
||||||
|
<blockLeftPadding start="0,0" stop="-1,-1" length="0"/>
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<blockTableStyle id="paymentTable">
|
||||||
|
<blockBackground colorName="LightGray" start="0,1" stop="3,1"/>
|
||||||
|
<blockFont name="OpenSans-Bold" start="0,1" stop="0,1"/>
|
||||||
|
<blockFont name="OpenSans-Bold" start="2,1" stop="2,1"/>
|
||||||
|
<lineStyle kind="outline" colorName="black" thickness="1" start="0,1" stop="3,1"/>
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<blockTableStyle id="signatureTable">
|
||||||
|
<blockTopPadding length="20" />
|
||||||
|
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
|
||||||
|
<lineStyle kind="linebelow" start="1,0" stop="1,0" colorName="black"/>
|
||||||
|
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
|
||||||
|
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
|
||||||
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<listStyle name="ol"
|
||||||
|
bulletFormat="%s."
|
||||||
|
bulletFontSize="10" />
|
||||||
|
|
||||||
|
<listStyle name="ul"
|
||||||
|
start="bulletchar"
|
||||||
|
bulletFontSize="10"/>
|
||||||
|
</stylesheet>
|
||||||
|
|
||||||
|
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||||
|
<pageTemplate id="Headed" >
|
||||||
|
<pageGraphics>
|
||||||
|
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
||||||
|
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
||||||
|
|
||||||
|
{# logo positioned 42 from left, 33 from top #}
|
||||||
|
<image file="static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
|
||||||
|
|
||||||
|
<setFont name="OpenSans-Bold" size="22.5" leading="10"/>
|
||||||
|
<drawString x="137" y="780">TEC PA & Lighting</drawString>
|
||||||
|
|
||||||
|
<setFont name="OpenSans" size="9"/>
|
||||||
|
<drawString x="137" y="760">Portland Building, University Park, Nottingham, NG7 2RD</drawString>
|
||||||
|
<drawString x="137" y="746">www.nottinghamtec.co.uk</drawString>
|
||||||
|
<drawString x="265" y="746">info@nottinghamtec.co.uk</drawString>
|
||||||
|
<drawString x="137" y="732">Phone: (0115) 846 8720</drawString>
|
||||||
|
|
||||||
|
<setFont name="OpenSans" size="10" />
|
||||||
|
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
|
||||||
|
<setFont name="OpenSans" size="7" />
|
||||||
|
<drawCenteredString x="302.5" y="26">
|
||||||
|
{{info_string}}
|
||||||
|
</drawCenteredString>
|
||||||
|
</pageGraphics>
|
||||||
|
|
||||||
|
<frame id="main" x1="50" y1="65" width="495" height="645"/>
|
||||||
|
</pageTemplate>
|
||||||
|
|
||||||
|
<pageTemplate id="Main">
|
||||||
|
<pageGraphics>
|
||||||
|
<image file="static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
|
||||||
|
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
||||||
|
|
||||||
|
<setFont name="OpenSans" size="10"/>
|
||||||
|
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
|
||||||
|
<setFont name="OpenSans" size="7" />
|
||||||
|
<drawCenteredString x="302.5" y="26">
|
||||||
|
{{info_string}}
|
||||||
|
</drawCenteredString>
|
||||||
|
</pageGraphics>
|
||||||
|
<frame id="main" x1="50" y1="65" width="495" height="727"/>
|
||||||
|
</pageTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<story firstPageTemplate="Headed">
|
||||||
|
<setNextFrame name="main"/>
|
||||||
|
<nextFrame/>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</story>
|
||||||
|
|
||||||
|
</document>
|
||||||
@@ -34,16 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.RIGS.view_riskassessment %}
|
{% if perms.RIGS.view_riskassessment %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item"><a class="nav-link" href="{% url 'hs_list' %}">H&S</a></li>
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownHS" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
H&S
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownHS">
|
|
||||||
<a class="dropdown-item" href="{% url 'hs_list' %}"><span class="fas fa-eye"></span> Overview</a>
|
|
||||||
<a class="dropdown-item" href="{% url 'ra_list' %}"><span class="fas fa-file-medical"></span> Risk Assessments</a>
|
|
||||||
<a class="dropdown-item" href="{% url 'ec_list' %}"><span class="fas fa-tasks"></span> Event Checklists</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.RIGS.view_invoice %}
|
{% if perms.RIGS.view_invoice %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|||||||
@@ -27,15 +27,12 @@
|
|||||||
|
|
||||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
themeSystem: 'bootstrap',
|
themeSystem: 'bootstrap',
|
||||||
//defaultView: 'dayGridMonth', This is now default
|
|
||||||
aspectRatio: 1.5,
|
aspectRatio: 1.5,
|
||||||
eventTimeFormat: {
|
eventTimeFormat: {
|
||||||
'hour': '2-digit',
|
'hour': '2-digit',
|
||||||
'minute': '2-digit',
|
'minute': '2-digit',
|
||||||
'hour12': false
|
'hour12': false
|
||||||
},
|
},
|
||||||
//nowIndicator: true,
|
|
||||||
//firstDay: 1,
|
|
||||||
headerToolbar: false,
|
headerToolbar: false,
|
||||||
editable: false,
|
editable: false,
|
||||||
dayMaxEventRows: true, // allow "more" link when too many events
|
dayMaxEventRows: true, // allow "more" link when too many events
|
||||||
@@ -58,8 +55,10 @@
|
|||||||
};
|
};
|
||||||
$(doc).each(function() {
|
$(doc).each(function() {
|
||||||
end = $(this).attr('latest')
|
end = $(this).attr('latest')
|
||||||
|
allDay = false
|
||||||
if(end.indexOf("T") < 0){ //If latest does not contain a time
|
if(end.indexOf("T") < 0){ //If latest does not contain a time
|
||||||
end = moment(end).add(1, 'days') //End date is non-inclusive, so add a day
|
end = moment(end + " 23:59").format("YYYY-MM-DD[T]HH:mm:ss")
|
||||||
|
allDay = true
|
||||||
}
|
}
|
||||||
|
|
||||||
thisEvent = {
|
thisEvent = {
|
||||||
@@ -67,7 +66,8 @@
|
|||||||
'end': end,
|
'end': end,
|
||||||
'className': 'modal-href',
|
'className': 'modal-href',
|
||||||
'title': $(this).attr('title'),
|
'title': $(this).attr('title'),
|
||||||
'url': $(this).attr('url')
|
'url': $(this).attr('url'),
|
||||||
|
'allDay': allDay
|
||||||
}
|
}
|
||||||
|
|
||||||
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
||||||
|
|||||||
5
RIGS/templates/email/eventauthorisation_mic_success.txt
Normal file
5
RIGS/templates/email/eventauthorisation_mic_success.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
|
||||||
|
|
||||||
|
Just to let you know your event N{{object.eventdisplay_id}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
||||||
|
|
||||||
|
The TEC Rig Information Gathering System
|
||||||
16
RIGS/templates/email/ra_reminder.html
Normal file
16
RIGS/templates/email/ra_reminder.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'base_client_email.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},</p>
|
||||||
|
|
||||||
|
{% if event.mic %}
|
||||||
|
<p>Just to let you know your event {{event.display_id}} <em>requires<em> a pre-event risk assessment completing prior to the event. Please do so as soon as possible.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>Fill it out here:</p>
|
||||||
|
<a href="{{url}}" class="btn btn-info"><span class="fas fa-paperclip"></span> Create Risk Assessment</a>
|
||||||
|
|
||||||
|
<p>TEC PA & Lighting</p>
|
||||||
|
{% endblock %}
|
||||||
9
RIGS/templates/email/ra_reminder.txt
Normal file
9
RIGS/templates/email/ra_reminder.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},
|
||||||
|
|
||||||
|
{% if event.mic %}
|
||||||
|
Just to let you know your event {{event.display_id}} requires a risk assessment completing prior to the event. Please do so as soon as possible.
|
||||||
|
{% else %}
|
||||||
|
This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
The TEC Rig Information Gathering System
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
|
|
||||||
{% load markdown_tags %}
|
{% load markdown_tags %}
|
||||||
|
{% load button from filters %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3 py-3">
|
<div class="row my-3 py-3">
|
||||||
@@ -52,6 +54,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if event.can_check_in %}
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">Crew Record</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Vehicle</th>
|
||||||
|
<th scope="col">Start Time</th>
|
||||||
|
<th scope="col">Role</th>
|
||||||
|
<th scope="col">End Time</th>
|
||||||
|
<th scope="col">{% if request.user.pk is event.mic.pk %}<a href="{% url 'event_checkin_override' event.pk %}" class="btn btn-sm btn-success"><span class="fas fa-plus"></span> Add</a>{% endif %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="crewmembers">
|
||||||
|
{% for crew in object.crew.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{crew.person}}</td>
|
||||||
|
<td>{{crew.vehicle|default:"None"}}</td>
|
||||||
|
<td>{{crew.time}}</td>
|
||||||
|
<td>{{crew.role}}</td>
|
||||||
|
<td>{% if crew.end_time %}{{crew.end_time}}{% else %}<span class="text-success fas fa-clock" data-toggle="tooltip" title="This person is currently checked into this event"></span>{% endif %}</td>
|
||||||
|
<td>{% if crew.end_time %}{% if crew.person.pk == request.user.pk or event.mic.pk == request.user.pk %}{% button 'edit' 'edit_checkin' crew.pk clazz='btn-sm modal-href' %}{% endif %}{%endif%}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center bg-warning">Apparently this event happened by magic...</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</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 'partials/event_detail_buttons.html' %}
|
{% include 'partials/event_detail_buttons.html' %}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-9 col-md-7 col-lg-8">
|
<div class="col-sm-9 col-md-7 col-lg-8">
|
||||||
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
|
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
|
||||||
{% if person %}
|
{% if person %}
|
||||||
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
|
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-9 col-md-7 col-lg-8">
|
<div class="col-sm-9 col-md-7 col-lg-8">
|
||||||
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
|
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
|
||||||
{% if organisation %}
|
{% if organisation %}
|
||||||
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
|
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-9 col-md-7 col-lg-8">
|
<div class="col-sm-9 col-md-7 col-lg-8">
|
||||||
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
||||||
{% if venue %}
|
{% if venue %}
|
||||||
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
|
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -277,10 +277,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-4 col-sm-8">
|
<div class="col-sm-offset-4 col-sm-8">
|
||||||
<div class="checkbox">
|
|
||||||
<label data-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end">
|
<label data-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end">
|
||||||
{% render_field form.dry_hire %}{{ form.dry_hire.label }}
|
{{ form.dry_hire.label }} {% render_field form.dry_hire %}
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +300,7 @@
|
|||||||
class="col-sm-4 col-form-label">{{ form.mic.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.mic.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||||
{% if mic %}
|
{% if mic %}
|
||||||
<option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option>
|
<option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -316,7 +314,7 @@
|
|||||||
class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||||
{% if checked_in_by %}
|
{% if checked_in_by %}
|
||||||
<option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option>
|
<option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,136 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
{% extends 'base_print.xml' %}
|
||||||
<!DOCTYPE document SYSTEM "rml.dtd">
|
|
||||||
<document filename="{{filename}}">
|
|
||||||
<docinit>
|
|
||||||
<registerTTFont faceName="OpenSans" fileName="static/fonts/OpenSans-Regular.tff"/>
|
|
||||||
<registerTTFont faceName="OpenSans-Bold" fileName="static/fonts/OpenSans-Bold.tff"/>
|
|
||||||
<registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/>
|
|
||||||
</docinit>
|
|
||||||
|
|
||||||
<stylesheet>
|
{% block content %}
|
||||||
<initialize>
|
{% include "event_print_page.xml" %}
|
||||||
<color id="LightGray" RGB="#D3D3D3"/>
|
{% endblock %}
|
||||||
<color id="DarkGray" RGB="#707070"/>
|
|
||||||
</initialize>
|
|
||||||
|
|
||||||
<paraStyle name="style.para" fontName="OpenSans" />
|
|
||||||
<paraStyle name="blockPara" spaceAfter="5" spaceBefore="5"/>
|
|
||||||
<paraStyle name="style.Heading1" fontName="OpenSans" fontSize="16" leading="18" spaceAfter="0"/>
|
|
||||||
<paraStyle name="style.Heading2" fontName="OpenSans-Bold" fontSize="10" spaceAfter="2"/>
|
|
||||||
<paraStyle name="style.Heading3" fontName="OpenSans" fontSize="10" spaceAfter="0"/>
|
|
||||||
<paraStyle name="center" alignment="center"/>
|
|
||||||
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
|
|
||||||
|
|
||||||
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
|
|
||||||
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
|
|
||||||
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
|
|
||||||
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
|
|
||||||
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
|
|
||||||
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
|
|
||||||
|
|
||||||
<blockTableStyle id="eventSpecifics">
|
|
||||||
<blockValign value="top"/>
|
|
||||||
<lineStyle kind="LINEAFTER" colorName="LightGrey" start="0,0" stop="1,0" thickness="1"/>
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<blockTableStyle id="headLayout">
|
|
||||||
<blockValign value="top"/>
|
|
||||||
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<blockTableStyle id="eventDetails">
|
|
||||||
<blockValign value="top"/>
|
|
||||||
<blockTopPadding start="0,0" stop="-1,0" length="0"/>
|
|
||||||
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<blockTableStyle id="itemTable">
|
|
||||||
<blockValign value="top"/>
|
|
||||||
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="0,0" stop="-1,-1" thickness="1"/>
|
|
||||||
{#<lineStyle kind="box" colorName="black" thickness="1" start="0,0" stop="-1,-1"/>#}
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<blockTableStyle id="totalTable">
|
|
||||||
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
|
|
||||||
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="-2,0" stop="-1,-1" thickness="1"/>
|
|
||||||
{# <lineStyle cap="default" kind="grid" colorName="black" thickness="1" start="1,0" stop="-1,-1"/> #}
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<blockTableStyle id="infoTable" keepWithNext="true">
|
|
||||||
<blockLeftPadding start="0,0" stop="-1,-1" length="0"/>
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<blockTableStyle id="paymentTable">
|
|
||||||
<blockBackground colorName="LightGray" start="0,1" stop="3,1"/>
|
|
||||||
<blockFont name="OpenSans-Bold" start="0,1" stop="0,1"/>
|
|
||||||
<blockFont name="OpenSans-Bold" start="2,1" stop="2,1"/>
|
|
||||||
<lineStyle kind="outline" colorName="black" thickness="1" start="0,1" stop="3,1"/>
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<blockTableStyle id="signatureTable">
|
|
||||||
<blockTopPadding length="20" />
|
|
||||||
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
|
|
||||||
<lineStyle kind="linebelow" start="1,0" stop="1,0" colorName="black"/>
|
|
||||||
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
|
|
||||||
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
|
|
||||||
</blockTableStyle>
|
|
||||||
|
|
||||||
<listStyle name="ol"
|
|
||||||
bulletFormat="%s."
|
|
||||||
bulletFontSize="10" />
|
|
||||||
|
|
||||||
<listStyle name="ul"
|
|
||||||
start="bulletchar"
|
|
||||||
bulletFontSize="10"/>
|
|
||||||
</stylesheet>
|
|
||||||
|
|
||||||
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
|
||||||
<pageTemplate id="Headed" >
|
|
||||||
<pageGraphics>
|
|
||||||
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
|
||||||
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
|
||||||
|
|
||||||
{# logo positioned 42 from left, 33 from top #}
|
|
||||||
<image file="static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
|
|
||||||
|
|
||||||
<setFont name="OpenSans-Bold" size="22.5" leading="10"/>
|
|
||||||
<drawString x="137" y="780">TEC PA & Lighting</drawString>
|
|
||||||
|
|
||||||
<setFont name="OpenSans" size="9"/>
|
|
||||||
<drawString x="137" y="760">Portland Building, University Park, Nottingham, NG7 2RD</drawString>
|
|
||||||
<drawString x="137" y="746">www.nottinghamtec.co.uk</drawString>
|
|
||||||
<drawString x="265" y="746">info@nottinghamtec.co.uk</drawString>
|
|
||||||
<drawString x="137" y="732">Phone: (0115) 846 8720</drawString>
|
|
||||||
|
|
||||||
<setFont name="OpenSans" size="10" />
|
|
||||||
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
|
|
||||||
<setFont name="OpenSans" size="7" />
|
|
||||||
<drawCenteredString x="302.5" y="26">
|
|
||||||
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
|
|
||||||
</drawCenteredString>
|
|
||||||
</pageGraphics>
|
|
||||||
|
|
||||||
<frame id="main" x1="50" y1="65" width="495" height="645"/>
|
|
||||||
</pageTemplate>
|
|
||||||
|
|
||||||
<pageTemplate id="Main">
|
|
||||||
<pageGraphics>
|
|
||||||
<image file="static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
|
|
||||||
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
|
||||||
|
|
||||||
<setFont name="OpenSans" size="10"/>
|
|
||||||
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
|
|
||||||
<setFont name="OpenSans" size="7" />
|
|
||||||
<drawCenteredString x="302.5" y="26">
|
|
||||||
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
|
|
||||||
</drawCenteredString>
|
|
||||||
</pageGraphics>
|
|
||||||
<frame id="main" x1="50" y1="65" width="495" height="727"/>
|
|
||||||
</pageTemplate>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<story firstPageTemplate="Headed">
|
|
||||||
{% include "event_print_page.xml" %}
|
|
||||||
</story>
|
|
||||||
|
|
||||||
</document>
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
{% load markdown_tags %}
|
{% load markdown_tags %}
|
||||||
{% load filters %}
|
{% load filters %}
|
||||||
|
|
||||||
<setNextFrame name="main"/>
|
|
||||||
<nextFrame/>
|
|
||||||
<blockTable style="headLayout" colWidths="330,165">
|
<blockTable style="headLayout" colWidths="330,165">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></h1>
|
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'</h1>
|
||||||
|
|
||||||
<para style="style.event_description">
|
<para style="style.event_description">
|
||||||
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
||||||
@@ -180,7 +178,7 @@
|
|||||||
{% for item in object.items.all %}
|
{% for item in object.items.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<para>{{ item.name }}</para>
|
<para><b>{{ item.name }}</b></para>
|
||||||
{% if item.description %}
|
{% if item.description %}
|
||||||
{{ item.description|markdown:"rml" }}
|
{{ item.description|markdown:"rml" }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
|
|
||||||
|
|
||||||
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
|
||||||
|
|
||||||
The TEC Rig Information Gathering System
|
|
||||||
@@ -5,21 +5,6 @@
|
|||||||
|
|
||||||
{% block title %}Request Authorisation{% endblock %}
|
{% block title %}Request Authorisation{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
|
||||||
<script src="{% static 'js/popover.js' %}"></script>
|
|
||||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
|
||||||
<script>
|
|
||||||
var clipboard = new ClipboardJS('.btn');
|
|
||||||
|
|
||||||
clipboard.on('success', function(e) {
|
|
||||||
$(e.trigger).popover('show');
|
|
||||||
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
|
|
||||||
e.clearSelection();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
@@ -33,11 +18,11 @@
|
|||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
{% if object.person.email %}
|
{% if object.person.email %}
|
||||||
<dt>Person Email</dt>
|
<dt>Person Email</dt>
|
||||||
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
|
<dd><span id="person-email" class="pr-1">{{ object.person.email }}</span> {% button 'copy' id='#person-email' %}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.organisation.email %}
|
{% if object.organisation.email %}
|
||||||
<dt>Organisation Email</dt>
|
<dt>Organisation Email</dt>
|
||||||
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
|
<dd><span id="org-email" class="pr-1">{{ object.organisation.email }}</span> {% button 'copy' id='#org-email' %}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -57,11 +42,20 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
<script src="{% static 'js/popover.js' %}"></script>
|
||||||
|
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
$('#auth-request-form').on('submit', function () {
|
$('#auth-request-form').on('submit', function () {
|
||||||
$('#auth-request-form button').attr('disabled', true);
|
$('#auth-request-form button').attr('disabled', true);
|
||||||
});
|
});
|
||||||
|
var clipboard = new ClipboardJS('.btn');
|
||||||
|
|
||||||
|
clipboard.on('success', function(e) {
|
||||||
|
$(e.trigger).popover('show');
|
||||||
|
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
|
||||||
|
e.clearSelection();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
79
RIGS/templates/hs/event_checklist_detail.html
Normal file
79
RIGS/templates/hs/event_checklist_detail.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
|
{% load help_text from filters %}
|
||||||
|
{% load profile_by_index from filters %}
|
||||||
|
{% load yesnoi from filters %}
|
||||||
|
{% load button from filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right my-3">
|
||||||
|
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||||
|
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
||||||
|
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-sm-12">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">General</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-6">Date</dt>
|
||||||
|
<dd class="col-6">
|
||||||
|
{{ object.date }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-6">Venue</dt>
|
||||||
|
<dd class="col-6">
|
||||||
|
{% if object.venue %}
|
||||||
|
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
|
||||||
|
{{ object.venue }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-sm-12">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">Safety Checks</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-10">{{ object|help_text:'safe_parking'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.safe_parking|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'safe_packing'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.safe_packing|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'exits'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.exits|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'trip_hazard'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.trip_hazard|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'warning_signs'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.warning_signs|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'ear_plugs'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.ear_plugs|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||||
|
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
|
||||||
|
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
{% include 'partials/last_edited.html' with target="eventchecklist_history" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
101
RIGS/templates/hs/event_checklist_form.html
Normal file
101
RIGS/templates/hs/event_checklist_form.html
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
{% load static %}
|
||||||
|
{% load help_text from filters %}
|
||||||
|
{% load profile_by_index from filters %}
|
||||||
|
{% load button from filters %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block preload_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/selects.js' %}"></script>
|
||||||
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-12">
|
||||||
|
{% include 'form_errors.html' %}
|
||||||
|
|
||||||
|
<form role="form" method="POST" action="{% if edit %}{% url 'ec_edit' pk=object.pk %}{% else %}{% url 'event_ec' pk=event.pk %}{% endif %}">
|
||||||
|
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
||||||
|
value="{{event.pk}}"/>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Event Information</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-4">Event Date</dt>
|
||||||
|
<dd class="col-8">{{ event.start_date}}{%if event.end_date %}-{{ event.end_date}}{%endif%}</dd>
|
||||||
|
<dt class="col-4">Event Name</dt>
|
||||||
|
<dd class="col-8">{{ event.name }}</dd>
|
||||||
|
<dt class="col-4">Client</dt>
|
||||||
|
<dd class="col-8">{{ event.person }}</dd>
|
||||||
|
<dt class="col-4">Event Size</dt>
|
||||||
|
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
|
||||||
|
</dl>
|
||||||
|
<div class="form-group form-row">
|
||||||
|
<label for="{{ form.date.id_for_label }}"
|
||||||
|
class="col-4 col-form-label">{{ form.date.label }}</label>
|
||||||
|
{% if not form.date.value %}
|
||||||
|
{% render_field form.date class+="form-control col-8" value=event.start_date %}
|
||||||
|
{% else %}
|
||||||
|
{% render_field form.date class+="form-control col-8" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
|
||||||
|
<label for="{{ form.venue.id_for_label }}"
|
||||||
|
class="col-4 col-form-label">{{ form.venue.label }}</label>
|
||||||
|
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
||||||
|
{% if venue %}
|
||||||
|
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
|
||||||
|
{% elif event.venue %}
|
||||||
|
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Safety Checks</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.safe_parking %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.safe_packing %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.exits %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.trip_hazard %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.warning_signs %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.ear_plugs %}
|
||||||
|
<div class="row pt-3">
|
||||||
|
<label class="col-5" for="{{ form.hs_location.id_for_label }}">{{ form.hs_location.help_text }}</label>
|
||||||
|
{% render_field form.hs_location class+="form-control col-7 col-md-4" %}
|
||||||
|
</div>
|
||||||
|
<div class="row pt-1">
|
||||||
|
<label class="col-5" for="{{ form.extinguishers_location.id_for_label }}">{{ form.extinguishers_location.help_text }}</label>
|
||||||
|
{% render_field form.extinguishers_location class+="form-control col-7 col-md-4" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-sm-12 text-right">
|
||||||
|
{% button 'submit' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
105
RIGS/templates/hs/eventcheckin_form.html
Normal file
105
RIGS/templates/hs/eventcheckin_form.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
{% load static %}
|
||||||
|
{% load button from filters %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block preload_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/selects.js' %}"></script>
|
||||||
|
<script src="{% static 'js/easymde.min.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-12">
|
||||||
|
{% include 'form_errors.html' %}
|
||||||
|
<form id="checkin" role="form" method="POST" action="{{ form.action|default:request.path }}">
|
||||||
|
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
||||||
|
value="{{event.pk}}"/>
|
||||||
|
{% if not request.is_ajax and self.request.user.pk is form.event.mic.pk %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.person.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">{{ form.person.label }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||||
|
{% if person %}
|
||||||
|
<option value="{{form.person.value}}" selected="selected" >{{ person.name }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<input type="hidden" name="{{ form.person.name }}" id="{{ form.person.id_for_label }}"
|
||||||
|
value="{{request.user.pk}}"/>
|
||||||
|
{% endif %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.time.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">Start Time</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% render_field form.time class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.role.id_for_label }}" class="col col-form-label">Role</label>
|
||||||
|
<div class="row pl-3">
|
||||||
|
<div class="col-md-6 col-sm-12">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_role').value='MIC'">MIC</button>
|
||||||
|
<button type="button" class="btn btn-danger" onclick="document.getElementById('id_role').value='Power MIC'">Power MIC</button>
|
||||||
|
<button type="button" class="btn btn-info" onclick="document.getElementById('id_role').value='Crew'">Crew</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mt-2">
|
||||||
|
{% render_field form.role class+="form-control" placeholder="Other (enter text)" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.vehicle.id_for_label }}" class="col col-form-label">Vehicle (if applicable)</label>
|
||||||
|
<div class="row pl-3">
|
||||||
|
<div class="col-md-6 col-sm-12">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_vehicle').value='Virgil'"><span class="fas fa-truck-moving"></span> Virgil</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="document.getElementById('id_vehicle').value='Virgil + Erms'"><span class="fas fa-trailer"></span><span class="fas fa-truck-moving"></span> Virgil + Erms</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mt-2">
|
||||||
|
{% render_field form.vehicle class+="form-control" placeholder="Other (enter text)" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if edit or manual %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.end_time.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">End Time</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% render_field form.end_time class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if not request.is_ajax %}
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-sm-12 text-right">
|
||||||
|
{% button 'submit' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
<div class="col-sm-12 text-right pr-0">
|
||||||
|
<button type="submit" class="btn btn-primary" title="Save" form="checkin"
|
||||||
|
><span class="fas fa-save align-middle"></span> <span class="d-none d-sm-inline align-middle">Save</span></button>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<th scope="col">Dates</th>
|
<th scope="col">Dates</th>
|
||||||
<th scope="col">RA</th>
|
<th scope="col">RA</th>
|
||||||
<th scope="col">Checklists</th>
|
<th scope="col">Checklists</th>
|
||||||
|
<th scope="col">Power Records</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -35,6 +36,14 @@
|
|||||||
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
||||||
class="d-none d-sm-inline">Create</span></a>
|
class="d-none d-sm-inline">Create</span></a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for record in event.power_tests.all %}
|
||||||
|
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
|
||||||
|
<br/>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
||||||
|
class="hidden-xs">Create</span></a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr class="bg-warning text-dark">
|
<tr class="bg-warning text-dark">
|
||||||
@@ -7,20 +7,22 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 text-right my-3">
|
<div class="col-12 text-right my-3">
|
||||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
{% button 'edit' url='pt_edit' pk=object.pk %}
|
||||||
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
||||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="card mb-3">
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="card-header">{% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
|
||||||
<div class="card mb-3">
|
<div class="card-body">
|
||||||
<div class="card-header">General</div>
|
<dl class="row">
|
||||||
<div class="card-body">
|
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-6">Date</dt>
|
|
||||||
<dd class="col-6">
|
<dd class="col-6">
|
||||||
{{ object.date }}
|
{% if object.power_mic %}
|
||||||
|
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-6">Venue</dt>
|
<dt class="col-6">Venue</dt>
|
||||||
<dd class="col-6">
|
<dd class="col-6">
|
||||||
@@ -30,90 +32,11 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
|
<dt class="col-6">Notes</dt>
|
||||||
<dd class="col-6">
|
<dd class="col-6">
|
||||||
{% if object.power_mic %}
|
{{ object.notes }}
|
||||||
<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>
|
|
||||||
<ul>
|
|
||||||
{% for i in object.vehicles.all %}
|
|
||||||
<li>{{i}}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 col-sm-12">
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Safety Checks</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-10">{{ object|help_text:'safe_parking'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.safe_parking|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'safe_packing'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.safe_packing|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'exits'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.exits|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'trip_hazard'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.trip_hazard|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'warning_signs'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.warning_signs|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'ear_plugs'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.ear_plugs|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Crew Record</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Crewmember</th>
|
|
||||||
<th scope="col">Start Time</th>
|
|
||||||
<th scope="col">Role</th>
|
|
||||||
<th scope="col">End Time</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="crewmemberst">
|
|
||||||
{% for crew in object.crew.all %}
|
|
||||||
<tr>
|
|
||||||
<td>{{crew.crewmember}}</td>
|
|
||||||
<td>{{crew.start}}</td>
|
|
||||||
<td>{{crew.role}}</td>
|
|
||||||
<td>{{crew.end}}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if object.event.riskassessment.event_size == 0 %}
|
{% if object.event.riskassessment.event_size == 0 %}
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
||||||
@@ -19,50 +19,15 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready(function () {
|
|
||||||
$('button[data-action=add]').on('click', function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
let target = $($(this).attr('data-target'));
|
|
||||||
let newID = Number(target.attr('data-pk'));
|
|
||||||
let newRow = $($(this).attr('data-clone'))
|
|
||||||
.clone().attr('style', "")
|
|
||||||
.attr('id', function(i, val){
|
|
||||||
return val.split("_")[0] + '_' + newID;
|
|
||||||
})
|
|
||||||
.appendTo(target);
|
|
||||||
newRow.find('select,input').attr('name', function(i, val){
|
|
||||||
return val.split("_")[0] + '_' + newID;
|
|
||||||
})//Disabled is to prevent the hidden row being sent to the form
|
|
||||||
.removeAttr('disabled');
|
|
||||||
newRow.find('button[data-action=delete]').attr('data-id', newID);
|
|
||||||
newRow.find('select').addClass('selectpicker');
|
|
||||||
newRow.find('.selectpicker').selectpicker('refresh');
|
|
||||||
$(".selectpicker").each(function(){initPicker($(this))});
|
|
||||||
initDatetime();
|
|
||||||
$(target).attr('data-pk', newID - 1);
|
|
||||||
});
|
|
||||||
$(document).on('click', 'button[data-action=delete]', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
$(this).closest('tr').remove();
|
|
||||||
});
|
|
||||||
//Somewhat rudimentary way of ensuring people fill in completely (if it hits the database validation the whole table row disappears when the page reloads...)
|
|
||||||
//the not is to avoid adding it to some of bootstrap-selects extra crap
|
|
||||||
$('#vehiclest,#crewmemberst').on('change', 'select,input', function () {
|
|
||||||
$(this).closest('tr').find("select,input").not(':input[type=search]').attr('required', 'true');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
{% include 'form_errors.html' %}
|
{% include 'form_errors.html' %}
|
||||||
{% if edit %}
|
{% if edit %}
|
||||||
<form role="form" method="POST" action="{% url 'ec_edit' pk=object.pk %}">
|
<form role="form" method="POST" action="{% url 'pt_edit' pk=object.pk %}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<form role="form" method="POST" action="{% url 'event_ec' pk=event.pk %}">
|
<form role="form" method="POST" action="{% url 'event_pt' pk=event.pk %}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
||||||
value="{{event.pk}}"/>
|
value="{{event.pk}}"/>
|
||||||
@@ -82,14 +47,17 @@
|
|||||||
<dt class="col-4">Event Size</dt>
|
<dt class="col-4">Event Size</dt>
|
||||||
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
|
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="form-group form-row">
|
<hr>
|
||||||
<label for="{{ form.date.id_for_label }}"
|
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
|
||||||
class="col-4 col-form-label">{{ form.date.label }}</label>
|
<label for="{{ form.power_mic.id_for_label }}"
|
||||||
{% if not form.date.value %}
|
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
|
||||||
{% render_field form.date class+="form-control col-8" value=event.start_date %}
|
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
|
||||||
{% else %}
|
{% if power_mic %}
|
||||||
{% render_field form.date class+="form-control col-8" %}
|
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
|
||||||
{% endif %}
|
{% elif event.riskassessment.power_mic %}
|
||||||
|
<option value="{{event.riskassessment.power_mic.pk}}" selected="selected">{{ event.riskassessment.power_mic.name }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
|
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
|
||||||
<label for="{{ form.venue.id_for_label }}"
|
<label for="{{ form.venue.id_for_label }}"
|
||||||
@@ -102,129 +70,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
|
<label for="{{ form.notes.id_for_label }}">Notes</label>
|
||||||
<label for="{{ form.power_mic.id_for_label }}"
|
{% render_field form.notes class+="form-control" %}
|
||||||
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
|
</div>
|
||||||
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
|
|
||||||
{% if power_mic %}
|
|
||||||
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
|
|
||||||
{% elif event.riskassessment.power_mic %}
|
|
||||||
<option value="{{event.riskassessment.power_mic.pk}}" selected="selected">{{ event.riskassessment.power_mic.name }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<p class="pt-3 font-weight-bold">List vehicles and their drivers</p>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Vehicle</th>
|
|
||||||
<th scope="col">Driver</th>
|
|
||||||
<th scope="col"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="vehiclest" data-pk="-1">
|
|
||||||
<tr id="vehicles_new" style="display: none;">
|
|
||||||
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td>
|
|
||||||
<td><select data-container="body" class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" name="driver_new" disabled="true"></select></td>
|
|
||||||
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
|
|
||||||
</tr>
|
|
||||||
{% for i in object.vehicles.all %}
|
|
||||||
<tr id="vehicles_{{i.pk}}">
|
|
||||||
<td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td>
|
|
||||||
<td>
|
|
||||||
<select data-container="body" name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
|
||||||
{% if i.driver != '' %}
|
|
||||||
<option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{i.pk}}' data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<button type="button" class="btn btn-secondary" id="vehicle-add" data-action='add' data-target='#vehiclest' data-clone='#vehicles_new'><span class="fas fa-plus"></span> Add Vehicle</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row my-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Safety Checks</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.safe_parking %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.safe_packing %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.exits %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.trip_hazard %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.warning_signs %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.ear_plugs %}
|
|
||||||
<div class="row pt-3">
|
|
||||||
<label class="col-5" for="{{ form.hs_location.id_for_label }}">{{ form.hs_location.help_text }}</label>
|
|
||||||
{% render_field form.hs_location class+="form-control col-7 col-md-4" %}
|
|
||||||
</div>
|
|
||||||
<div class="row pt-1">
|
|
||||||
<label class="col-5" for="{{ form.extinguishers_location.id_for_label }}">{{ form.extinguishers_location.help_text }}</label>
|
|
||||||
{% render_field form.extinguishers_location class+="form-control col-7 col-md-4" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row my-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Crew Record</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Person</th>
|
|
||||||
<th scope="col">Start Time</th>
|
|
||||||
<th scope="col">Role</th>
|
|
||||||
<th scope="col">End Time</th>
|
|
||||||
<th scope="col"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="crewmemberst" data-pk="-1">
|
|
||||||
<tr id="crew_new" style="display: none;">
|
|
||||||
<td>
|
|
||||||
<select name="crewmember_new" class="form-control" data-container="body" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select>
|
|
||||||
</td>
|
|
||||||
<td style="min-width: 15ch"><input name="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled=""/></td>
|
|
||||||
<td style="min-width: 15ch"><input name="role_new" type="text" class="form-control" value="{{ i.role }}" disabled="true"/></td>
|
|
||||||
<td style="min-width: 15ch"><input name="end_new" type="datetime-local" class="form-control" value="{{ i.end }}" disabled="true" /></td>
|
|
||||||
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
|
|
||||||
</tr>
|
|
||||||
{% for crew in object.crew.all %}
|
|
||||||
<tr id="crew_{{crew.pk}}">
|
|
||||||
<td>
|
|
||||||
<select data-container="body" name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
|
||||||
{% if crew.crewmember != '' %}
|
|
||||||
<option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td><input name="start_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.start|date:'Y-m-d' }}T{{ crew.start|date:'H:i:s' }}"/></td>
|
|
||||||
<td><input name="role_{{crew.pk}}" type="text" class="form-control" value="{{ crew.role }}"/></td>
|
|
||||||
<td><input name="end_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.end|date:'Y-m-d' }}T{{ crew.end|date:'H:i:s' }}"/></td>
|
|
||||||
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="text-right">
|
|
||||||
<button type="button" class="btn btn-secondary" data-action='add' data-target='#crewmemberst' data-clone='#crew_new'><span class="fas fa-plus"></span> Add Crewmember</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
135
RIGS/templates/hs/ra_print.xml
Normal file
135
RIGS/templates/hs/ra_print.xml
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{% extends 'base_print.xml' %}
|
||||||
|
{% load filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<spacer length="15"/>
|
||||||
|
<h1>Event Specific Risk Assessment for <strong>{{ object.event }}</strong></h1>
|
||||||
|
<spacer length="15"/>
|
||||||
|
<h2>Client: {{ object.event.person|default:object.event.organisation }} | Venue: {{ object.event.venue }} | MIC: {{ object.event.mic }}</h2>
|
||||||
|
<spacer length="15"/>
|
||||||
|
<hr/>
|
||||||
|
<blockTable colWidths="425,100" spaceAfter="15">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><h3><strong>General</strong></h3></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'nonstandard_equipment'|striptags }}</para></td>
|
||||||
|
<td>{{ object.nonstandard_equipment|yesno|capfirst }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'nonstandard_use'|striptags }}</para></td>
|
||||||
|
<td>{{ object.nonstandard_use|yesno|capfirst }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'contractors'|striptags }}</para></td>
|
||||||
|
<td>{{ object.contractors|yesno|capfirst }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'other_companies'|striptags }}</para></td>
|
||||||
|
<td>{{ object.other_companies|yesno|capfirst }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'crew_fatigue'|striptags }}</para></td>
|
||||||
|
<td>{{ object.crew_fatigue|yesno|capfirst }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'general_notes'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.general_notes|default:'No' }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><h3><strong>Power</strong></h3><spacer length="4"/><para textColor="white" backColor={% if object.event_size == 0 %}"green"{% elif object.event_size == 1 %}"yellow"{% else %}"red"{% endif %} borderPadding="3">{{ object.get_event_size_display }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'big_power'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.big_power|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'power_mic'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.power_mic|default:object.event.mic }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'outside'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.outside|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'generators'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.generators|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'other_companies_power'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.other_companies_power|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'nonstandard_equipment_power'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.nonstandard_equipment_power|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'multiple_electrical_environments'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.multiple_electrical_environments|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'power_notes'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.power_notes|default:'No' }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><h3><strong>Sound</strong></h3></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'noise_monitoring'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.noise_monitoring|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'sound_notes'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.sound_notes|default:'No' }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><h3><strong>Site Details</strong></h3></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'known_venue'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.known_venue|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'safe_loading'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.safe_loading|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'safe_storage'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.safe_storage|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'area_outside_of_control'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.area_outside_of_control|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'barrier_required'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.barrier_required|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'nonstandard_emergency_procedure'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.nonstandard_emergency_procedure|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><h3><strong>Structures</strong></h3></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'special_structures'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.special_structures|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'suspended_structures'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.suspended_structures|yesno|capfirst }}</para></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><para>{{ object|help_text:'persons_responsible_structures'|striptags }}</para></td>
|
||||||
|
<td><para>{{ object.persons_responsible_structures|default:'N/A' }}</para></td>
|
||||||
|
</tr>
|
||||||
|
</blockTable>
|
||||||
|
<spacer length="15"/>\
|
||||||
|
<hr/>
|
||||||
|
<spacer length="15"/>
|
||||||
|
<para><em>Assessment completed by {{ object.last_edited_by }} on {{ object.last_edited_at }}</em></para>
|
||||||
|
{% if object.reviewed_by %}
|
||||||
|
<para><em>Reviewed by {{ object.reviewed_by }} on {{ object.reviewed.at }}</em></para>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
{% load help_text from filters %}
|
{% load filters %}
|
||||||
{% load yesnoi from filters %}
|
|
||||||
{% load linkornone from filters %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row py-3">
|
<div class="row py-3">
|
||||||
@@ -47,7 +45,7 @@
|
|||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-6">{{ object|help_text:'power_mic'|safe }}</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:object.event.mic }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
|
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
@@ -144,7 +142,7 @@
|
|||||||
</dd>
|
</dd>
|
||||||
<dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
|
<dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
|
||||||
<dd class="col-12">
|
<dd class="col-12">
|
||||||
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
|
{{ object.persons_responsible_structures|default:'N/A'|linebreaks }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
|
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
|
||||||
<dd class="col-12">
|
<dd class="col-12">
|
||||||
@@ -157,6 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 text-right">
|
<div class="col-12 text-right">
|
||||||
|
{% button 'print' 'ra_print' object.pk %}
|
||||||
<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>
|
||||||
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>
|
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>
|
||||||
@@ -98,9 +98,9 @@
|
|||||||
<label for="{{ form.power_mic.id_for_label }}"
|
<label for="{{ form.power_mic.id_for_label }}"
|
||||||
class="col col-form-label">{{ form.power_mic.help_text|safe }}</label>
|
class="col col-form-label">{{ form.power_mic.help_text|safe }}</label>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||||
{% if object.power_mic %}
|
{% if power_mic %}
|
||||||
<option value="{{object.power_mic.pk}}" selected="selected">{{ object.power_mic.name }}</option>
|
<option value="{{form.power_mic.value}}" selected="selected">{{ power_mic }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
|
||||||
{% load paginator from filters %}
|
|
||||||
{% load help_text from filters %}
|
|
||||||
{% load verbose_name from filters %}
|
|
||||||
{% load get_field from filters %}
|
|
||||||
|
|
||||||
{% block title %}{{ title }} List{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h2>{{title}} List</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table mb-0 table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Event</th>
|
|
||||||
{# mmm hax #}
|
|
||||||
{% if object_list.0 != None %}
|
|
||||||
{% for field in object_list.0.fieldz %}
|
|
||||||
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
<th scope="col"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for object in object_list %}
|
|
||||||
<tr class="{% if object.reviewed_by %}table-success{%endif%}">
|
|
||||||
{# General #}
|
|
||||||
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
|
|
||||||
{% for field in object_list.0.fieldz %}
|
|
||||||
<td>{{ object|get_field:field }}</td>
|
|
||||||
{% endfor %}
|
|
||||||
{# Buttons #}
|
|
||||||
<td>
|
|
||||||
{% include 'partials/hs_status.html' %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr class="bg-warning">
|
|
||||||
<td colspan="6">Nothing found</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if is_paginated %}
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
{% paginator %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<dt class="col-sm-6">Phone Number</dt>
|
<dt class="col-sm-6">Phone Number</dt>
|
||||||
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
|
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
|
||||||
<dt class="col-sm-6">Has SU Account</dt>
|
<dt class="col-sm-6">Has SU Account</dt>
|
||||||
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
|
<dd class="col-sm-6">{{ object.organisation.union_account|yesno|capfirst }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,6 +47,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p><strong>Voltage Drop on Circuit:</strong> 5% (approx. 12v)</p>
|
<p><strong>Voltage Drop on Circuit:</strong> ≤5% (approx. 12v)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 active
|
btn-success active disabled
|
||||||
{% 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 %}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
btn-secondary
|
btn-secondary
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"
|
"
|
||||||
href="{% url 'event_authorise_request' object.pk %}">
|
{% if event.authorised %}aria-disabled="true"{% else %}href="{% url 'event_authorise_request' object.pk %}"{% endif %}>
|
||||||
<span class="fas fa-paper-plane"></span>
|
<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 %}
|
||||||
@@ -49,5 +49,13 @@
|
|||||||
{% 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>
|
<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>
|
||||||
|
|
||||||
|
{% if event.can_check_in %}
|
||||||
|
{% if request.user.current_event %}
|
||||||
|
<a href="{% url 'event_checkout' %}" class="btn btn-warning">Check Out</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'event_checkin' event.pk %}" class="btn btn-success modal-href"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{% if object.venue %}
|
{% if object.venue %}
|
||||||
<dt class="col-sm-6">Venue Notes</dt>
|
<dt class="col-sm-6">Venue Notes</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
{{ object.venue.notes }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%}
|
{{ object.venue.notes|markdown }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%}
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,21 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not event.dry_hire %}
|
{% if not event.dry_hire %}
|
||||||
{% if event.riskassessment %}
|
{% if event.has_checklist %}
|
||||||
<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>
|
<a href="{{ event.riskassessment.get_absolute_url }}"><span class="badge badge-success">RA: <span class="fas fa-check{% if event.riskassessment.reviewed_by %}-double{%endif%}"></span></a>
|
||||||
{% 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 %}
|
||||||
{% endif %}
|
{% if event.has_checklist %}
|
||||||
{% if not event.dry_hire %}
|
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span> {% if event.checklists.count > 1 %}({{event.checklists.count}}){% endif %}</span>
|
||||||
{% if event.hs_done %}
|
|
||||||
{# TODO Display status of all checklists #}
|
|
||||||
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span></span>
|
|
||||||
{% 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 %}
|
||||||
|
{% if event.has_power %}
|
||||||
|
<span class="badge badge-success">Power Record: <span class="fas fa-check"></span> {% if event.power_tests.count > 1 %}({{event.power_tests.count}}){% endif %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-danger">Power Record: <span class="fas fa-times"></span></span>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.RIGS.view_invoice %}
|
{% if perms.RIGS.view_invoice %}
|
||||||
{% if event.invoice %}
|
{% if event.invoice %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% load namewithnotes from filters %}
|
{% load namewithnotes from filters %}
|
||||||
|
{% load markdown_tags %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table mb-0" id="event_table">
|
<table class="table mb-0" id="event_table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -29,7 +30,15 @@
|
|||||||
<!---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" style="text-align: justify;">
|
||||||
|
{% if not event.cancelled %}
|
||||||
|
{% if event.meet_at %}
|
||||||
|
<span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.access_at %}
|
||||||
|
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
|
<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" }}
|
||||||
@@ -43,14 +52,6 @@
|
|||||||
{% endif %}</strong>
|
{% endif %}</strong>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not event.cancelled %}
|
|
||||||
{% if event.meet_at %}
|
|
||||||
<br><span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
|
|
||||||
{% endif %}
|
|
||||||
{% if event.access_at %}
|
|
||||||
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:" D d/m/Y H:i" }}</strong></span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<!---Details-->
|
<!---Details-->
|
||||||
<td id="event_details" class="w-100">
|
<td id="event_details" class="w-100">
|
||||||
@@ -74,7 +75,7 @@
|
|||||||
</h5>
|
</h5>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not event.cancelled and event.description %}
|
{% if not event.cancelled and event.description %}
|
||||||
<p>{{ event.description|linebreaksbr }}</p>
|
<p>{{ event.description|markdown }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'partials/event_status.html' %}
|
{% include 'partials/event_status.html' %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -10,10 +10,18 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<h5>Event Checklists:</h5>
|
<h5>Event Checklists:</h5>
|
||||||
{% for checklist in event.checklists.all %}
|
{% for checklist in event.checklists.all %}
|
||||||
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
|
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
|
||||||
<br/>
|
<br/>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
|
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
|
||||||
class="hidden-xs">Create</span></a>
|
class="hidden-xs">Create</span></a>
|
||||||
|
<hr>
|
||||||
|
<h5>Power Test Records:</h5>
|
||||||
|
{% for record in event.power_tests.all %}
|
||||||
|
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
|
||||||
|
<br/>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
|
||||||
|
class="hidden-xs">Create</span></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,9 +118,9 @@ def orderby(request, field, attr):
|
|||||||
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
||||||
def get_field(obj, field, autoescape=True):
|
def get_field(obj, field, autoescape=True):
|
||||||
value = getattr(obj, field)
|
value = getattr(obj, field)
|
||||||
if(isinstance(value, bool)):
|
if (isinstance(value, bool)):
|
||||||
value = yesnoi(value, field in obj.inverted_fields)
|
value = yesnoi(value, field in obj.inverted_fields)
|
||||||
elif(isinstance(value, str)):
|
elif (isinstance(value, str)):
|
||||||
value = truncatewords(value, 20)
|
value = truncatewords(value, 20)
|
||||||
return mark_safe(value)
|
return mark_safe(value)
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ def get_list(dictionary, key):
|
|||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def profile_by_index(value):
|
def profile_by_index(value):
|
||||||
if(value):
|
if (value):
|
||||||
return models.Profile.objects.get(pk=int(value))
|
return models.Profile.objects.get(pk=int(value))
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
@@ -196,8 +196,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
|||||||
text = "Edit"
|
text = "Edit"
|
||||||
elif type == 'print':
|
elif type == 'print':
|
||||||
clazz += " btn-primary "
|
clazz += " btn-primary "
|
||||||
icon = "fa-print"
|
icon = "fa-download"
|
||||||
text = "Print"
|
text = "Export"
|
||||||
elif type == 'duplicate':
|
elif type == 'duplicate':
|
||||||
clazz += " btn-info "
|
clazz += " btn-info "
|
||||||
icon = "fa-copy"
|
icon = "fa-copy"
|
||||||
@@ -216,6 +216,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
|||||||
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
|
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': 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}
|
||||||
|
elif type == 'today':
|
||||||
|
return {'today': True, 'id': id}
|
||||||
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}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,15 +43,22 @@ def venue(db):
|
|||||||
|
|
||||||
@pytest.fixture # TODO parameterise with Event sizes
|
@pytest.fixture # TODO parameterise with Event sizes
|
||||||
def checklist(basic_event, venue, admin_user, ra):
|
def checklist(basic_event, venue, admin_user, ra):
|
||||||
checklist = models.EventChecklist.objects.create(event=basic_event, power_mic=admin_user, safe_parking=False,
|
checklist = models.EventChecklist.objects.create(event=basic_event, safe_parking=False,
|
||||||
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
||||||
ear_plugs=False, hs_location="Locked away safely",
|
ear_plugs=False, hs_location="Locked away safely",
|
||||||
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
|
extinguishers_location="Somewhere, I forgot",
|
||||||
date=timezone.now(), venue=venue)
|
date=timezone.now(), venue=venue)
|
||||||
yield checklist
|
yield checklist
|
||||||
checklist.delete()
|
checklist.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def power_test(basic_event, venue, admin_user, ra):
|
||||||
|
power_test = models.PowerTestRecord.objects.create(event=basic_event, venue=venue)
|
||||||
|
yield power_test
|
||||||
|
power_test.delete()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def many_events(db, admin_user, scope="class"):
|
def many_events(db, admin_user, scope="class"):
|
||||||
many_events = {
|
many_events = {
|
||||||
|
|||||||
@@ -230,11 +230,6 @@ 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[select[@id='id_power_mic']]")
|
|
||||||
_add_vehicle_locator = (By.XPATH, "//button[contains(., 'Vehicle')]")
|
|
||||||
_add_crew_locator = (By.XPATH, "//button[contains(., 'Crew')]")
|
|
||||||
_vehicle_row_locator = ('xpath', "//tr[@id[starts-with(., 'vehicle') and not(contains(.,'new'))]]")
|
|
||||||
_crew_row_locator = ('xpath', "//tr[@id[starts-with(., 'crew') and not(contains(.,'new'))]]")
|
|
||||||
|
|
||||||
form_items = {
|
form_items = {
|
||||||
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
|
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
|
||||||
@@ -245,6 +240,20 @@ class CreateEventChecklist(FormPage):
|
|||||||
'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')),
|
'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')),
|
||||||
'hs_location': (regions.TextBox, (By.ID, 'id_hs_location')),
|
'hs_location': (regions.TextBox, (By.ID, 'id_hs_location')),
|
||||||
'extinguishers_location': (regions.TextBox, (By.ID, 'id_extinguishers_location')),
|
'extinguishers_location': (regions.TextBox, (By.ID, 'id_extinguishers_location')),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self):
|
||||||
|
return '{event_id}' not in self.driver.current_url
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePowerTestRecord(FormPage):
|
||||||
|
URL_TEMPLATE = 'event/{event_id}/power'
|
||||||
|
|
||||||
|
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
||||||
|
_power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]")
|
||||||
|
|
||||||
|
form_items = {
|
||||||
'rcds': (regions.CheckBox, (By.ID, 'id_rcds')),
|
'rcds': (regions.CheckBox, (By.ID, 'id_rcds')),
|
||||||
'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')),
|
'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')),
|
||||||
'earthing': (regions.CheckBox, (By.ID, 'id_earthing')),
|
'earthing': (regions.CheckBox, (By.ID, 'id_earthing')),
|
||||||
@@ -263,58 +272,10 @@ class CreateEventChecklist(FormPage):
|
|||||||
'w1_earth_fault': (regions.TextBox, (By.ID, 'id_w1_earth_fault')),
|
'w1_earth_fault': (regions.TextBox, (By.ID, 'id_w1_earth_fault')),
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_vehicle(self):
|
|
||||||
self.find_element(*self._add_vehicle_locator).click()
|
|
||||||
|
|
||||||
def add_crew(self):
|
|
||||||
self.find_element(*self._add_crew_locator).click()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
|
|||||||
|
|
||||||
class Header(Region):
|
class Header(Region):
|
||||||
def find_link(self, link_text):
|
def find_link(self, link_text):
|
||||||
return self.driver.find_element_by_partial_link_text(link_text)
|
return self.driver.find_element(By.PARTIAL_LINK_TEXT, link_text)
|
||||||
|
|
||||||
|
|
||||||
class ItemRow(Region):
|
class ItemRow(Region):
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class TestEventCreate(BaseRigboardTest):
|
|||||||
self.assertEqual("Test Item 1", testitem['name'])
|
self.assertEqual("Test Item 1", testitem['name'])
|
||||||
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
|
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
|
||||||
|
|
||||||
total = self.driver.find_element_by_id('total')
|
total = self.driver.find_element(By.ID, 'total')
|
||||||
ActionChains(self.driver).move_to_element(total).perform()
|
ActionChains(self.driver).move_to_element(total).perform()
|
||||||
|
|
||||||
# See new item appear in table
|
# See new item appear in table
|
||||||
@@ -224,9 +224,9 @@ class TestEventCreate(BaseRigboardTest):
|
|||||||
self.assertEqual('47.90', row.subtotal)
|
self.assertEqual('47.90', row.subtotal)
|
||||||
|
|
||||||
# Check totals TODO convert to page properties
|
# Check totals TODO convert to page properties
|
||||||
self.assertEqual("47.90", self.driver.find_element_by_id('sumtotal').text)
|
self.assertEqual("47.90", self.driver.find_element(By.ID, 'sumtotal').text)
|
||||||
self.assertIn("(TBC)", self.driver.find_element_by_id('vat-rate').text)
|
self.assertIn("(TBC)", self.driver.find_element(By.ID, 'vat-rate').text)
|
||||||
self.assertEqual("9.58", self.driver.find_element_by_id('vat').text)
|
self.assertEqual("9.58", self.driver.find_element(By.ID, 'vat').text)
|
||||||
self.assertEqual("57.48", total.text)
|
self.assertEqual("57.48", total.text)
|
||||||
|
|
||||||
self.page.submit()
|
self.page.submit()
|
||||||
@@ -318,7 +318,7 @@ class TestEventDuplicate(BaseRigboardTest):
|
|||||||
|
|
||||||
self.assertFalse(newEvent.authorised)
|
self.assertFalse(newEvent.authorised)
|
||||||
|
|
||||||
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element_by_xpath('//h2').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
|
||||||
@@ -327,26 +327,25 @@ class TestEventDuplicate(BaseRigboardTest):
|
|||||||
self.assertIn("Test Item 2", table.text)
|
self.assertIn("Test Item 2", table.text)
|
||||||
self.assertIn("Test Item 3", table.text)
|
self.assertIn("Test Item 3", table.text)
|
||||||
|
|
||||||
infoPanel = self.driver.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..')
|
||||||
self.assertIn("N%05d" % self.testEvent.pk,
|
self.assertIn("N%05d" % self.testEvent.pk, infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
|
||||||
# Check the PO hasn't carried through
|
# Check the PO hasn't carried through
|
||||||
self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
|
self.assertNotIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
self.assertIn("N%05d" % self.testEvent.pk,
|
self.assertIn("N%05d" % self.testEvent.pk,
|
||||||
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
self.driver.get(self.live_server_url + '/event/' + str(self.testEvent.pk)) # Go back to the old event
|
self.driver.get(self.live_server_url + '/event/' + str(self.testEvent.pk)) # Go back to the old event
|
||||||
|
|
||||||
# Check that based-on hasn't crept into the old event
|
# Check that based-on hasn't crept into the old event
|
||||||
infoPanel = self.driver.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..')
|
||||||
self.assertNotIn("N%05d" % self.testEvent.pk,
|
self.assertNotIn("N%05d" % self.testEvent.pk,
|
||||||
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
# Check the PO remains on the old event
|
# Check the PO remains on the old event
|
||||||
self.assertIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
|
self.assertIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
self.assertNotIn("N%05d" % self.testEvent.pk,
|
self.assertNotIn("N%05d" % self.testEvent.pk,
|
||||||
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
# Check the items are as they were
|
# Check the items are as they were
|
||||||
table = self.page.item_table # ID number is known, see above
|
table = self.page.item_table # ID number is known, see above
|
||||||
@@ -677,14 +676,6 @@ def small_ec(page, admin_user):
|
|||||||
page.ear_plugs = True
|
page.ear_plugs = True
|
||||||
page.hs_location = "The Moon"
|
page.hs_location = "The Moon"
|
||||||
page.extinguishers_location = "With the rest of the fire"
|
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):
|
def test_ec_create_small(logged_in_browser, live_server, admin_user, ra):
|
||||||
@@ -705,14 +696,15 @@ def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra)
|
|||||||
page.ear_plugs = True
|
page.ear_plugs = True
|
||||||
page.hs_location = "Death Valley"
|
page.hs_location = "Death Valley"
|
||||||
page.extinguishers_location = "With the rest of the fire"
|
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
|
# Gotta scroll to make the button clickable
|
||||||
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||||
|
page.submit()
|
||||||
|
assert page.success
|
||||||
|
|
||||||
|
|
||||||
|
def test_power_checklist(logged_in_browser, live_server, admin_user, power_test, medium_ra):
|
||||||
|
page = pages.CreatePowerTestRecord(logged_in_browser.driver, live_server.url, event_id=medium_ra.event.pk).open()
|
||||||
page.earthing = True
|
page.earthing = True
|
||||||
page.pat = True
|
page.pat = True
|
||||||
page.source_rcd = True
|
page.source_rcd = True
|
||||||
@@ -727,56 +719,15 @@ def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra)
|
|||||||
page.w1_polarity = True
|
page.w1_polarity = True
|
||||||
page.w1_voltage = 240
|
page.w1_voltage = 240
|
||||||
page.w1_earth_fault = "0.42"
|
page.w1_earth_fault = "0.42"
|
||||||
|
# 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.submit()
|
page.submit()
|
||||||
assert page.success
|
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?
|
# 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):
|
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()
|
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open()
|
||||||
|
|||||||
35
RIGS/urls.py
35
RIGS/urls.py
@@ -75,25 +75,40 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
|
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
|
||||||
name='event_ra'),
|
name='event_ra'),
|
||||||
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentDetail.as_view()),
|
path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()),
|
||||||
name='ra_detail'),
|
name='ra_detail'),
|
||||||
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
||||||
name='ra_edit'),
|
name='ra_edit'),
|
||||||
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentList.as_view()),
|
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.MarkReviewed.as_view()),
|
||||||
name='ra_list'),
|
name='ra_review', kwargs={'model': 'RiskAssessment'}),
|
||||||
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()),
|
path('event/ra/<int:pk>/print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'),
|
||||||
name='ra_review'),
|
|
||||||
|
|
||||||
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
||||||
name='event_ec'),
|
name='event_ec'),
|
||||||
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistDetail.as_view()),
|
path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()),
|
||||||
name='ec_detail'),
|
name='ec_detail'),
|
||||||
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
||||||
name='ec_edit'),
|
name='ec_edit'),
|
||||||
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistList.as_view()),
|
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.MarkReviewed.as_view()),
|
||||||
name='ec_list'),
|
name='ec_review', kwargs={'model': 'EventChecklist'}),
|
||||||
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.EventChecklistReview.as_view()),
|
|
||||||
name='ec_review'),
|
path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()),
|
||||||
|
name='event_pt'),
|
||||||
|
path('event/power/<int:pk>/', login_required(views.PowerTestDetail.as_view()),
|
||||||
|
name='pt_detail'),
|
||||||
|
path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()),
|
||||||
|
name='pt_edit'),
|
||||||
|
path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()),
|
||||||
|
name='pt_review', kwargs={'model': 'PowerTestRecord'}),
|
||||||
|
|
||||||
|
path('event/<int:pk>/checkin/', login_required(views.EventCheckIn.as_view()),
|
||||||
|
name='event_checkin'),
|
||||||
|
path('event/checkout/', login_required(views.EventCheckOut.as_view()),
|
||||||
|
name='event_checkout'),
|
||||||
|
path('event/<int:pk>/checkin/edit/', login_required(views.EventCheckInEdit.as_view()),
|
||||||
|
name='edit_checkin'),
|
||||||
|
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()),
|
||||||
|
name='event_checkin_override'),
|
||||||
|
|
||||||
# Finance
|
# Finance
|
||||||
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ class InvoiceIndex(generic.ListView):
|
|||||||
total = 0
|
total = 0
|
||||||
for i in context['object_list']:
|
for i in context['object_list']:
|
||||||
total += i.balance
|
total += i.balance
|
||||||
context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total)
|
event_count = len(list(context['object_list']))
|
||||||
|
context['page_title'] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})"
|
||||||
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
|
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class InvoiceDetail(generic.DetailView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
|
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
|
||||||
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) "
|
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})"
|
||||||
if self.object.void:
|
if self.object.void:
|
||||||
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
|
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
|
||||||
elif self.object.is_closed:
|
elif self.object.is_closed:
|
||||||
@@ -59,11 +60,14 @@ class InvoicePrint(generic.View):
|
|||||||
object = invoice.event
|
object = invoice.event
|
||||||
template = get_template('event_print.xml')
|
template = get_template('event_print.xml')
|
||||||
|
|
||||||
|
name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||||
|
filename = f"Invoice {invoice.display_id} for {object.display_id} {name}.pdf"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'object': object,
|
'object': object,
|
||||||
'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': filename
|
||||||
}
|
}
|
||||||
|
|
||||||
rml = template.render(context)
|
rml = template.render(context)
|
||||||
@@ -73,7 +77,7 @@ class InvoicePrint(generic.View):
|
|||||||
pdfData = buffer.read()
|
pdfData = buffer.read()
|
||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
response = HttpResponse(content_type='application/pdf')
|
||||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
response['Content-Disposition'] = f'filename="{filename}"'
|
||||||
response.write(pdfData)
|
response.write(pdfData)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -124,32 +128,7 @@ class InvoiceArchive(generic.ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = self.request.GET.get('q', "")
|
return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date')
|
||||||
|
|
||||||
filter = Q(event__name__icontains=q)
|
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
filter = filter | Q(event__pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if q[0] == "N":
|
|
||||||
val = int(q[1:])
|
|
||||||
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
|
||||||
elif q[0] == "#":
|
|
||||||
val = int(q[1:])
|
|
||||||
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
|
|
||||||
|
|
||||||
return object_list
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceWaiting(generic.ListView):
|
class InvoiceWaiting(generic.ListView):
|
||||||
@@ -163,7 +142,7 @@ class InvoiceWaiting(generic.ListView):
|
|||||||
objects = self.get_queryset()
|
objects = self.get_queryset()
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
total += obj.sum_total
|
total += obj.sum_total
|
||||||
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total)
|
context['page_title'] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|||||||
278
RIGS/views/hs.py
278
RIGS/views/hs.py
@@ -1,16 +1,48 @@
|
|||||||
|
from django.apps import apps
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
|
from RIGS.views.rigboard import get_related
|
||||||
|
from PyRIGS.views import PrintView, ModalURLMixin
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentCreate(generic.CreateView):
|
class HSCreateView(generic.CreateView):
|
||||||
|
def get_form(self, **kwargs):
|
||||||
|
form = super().get_form(**kwargs)
|
||||||
|
epk = self.kwargs.get('pk')
|
||||||
|
event = models.Event.objects.get(pk=epk)
|
||||||
|
form.instance.event = event
|
||||||
|
return form
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
epk = self.kwargs.get('pk')
|
||||||
|
event = models.Event.objects.get(pk=epk)
|
||||||
|
context['event'] = event
|
||||||
|
context['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}'
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class MarkReviewed(generic.View):
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk'))
|
||||||
|
with reversion.create_revision():
|
||||||
|
reversion.set_user(self.request.user)
|
||||||
|
obj.reviewed_by = self.request.user
|
||||||
|
obj.reviewed_at = timezone.now()
|
||||||
|
obj.save()
|
||||||
|
return HttpResponseRedirect(reverse('hs_list'))
|
||||||
|
|
||||||
|
|
||||||
|
class EventRiskAssessmentCreate(HSCreateView):
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
template_name = 'risk_assessment_form.html'
|
template_name = 'hs/risk_assessment_form.html'
|
||||||
form_class = forms.EventRiskAssessmentForm
|
form_class = forms.EventRiskAssessmentForm
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
@@ -21,32 +53,17 @@ class EventRiskAssessmentCreate(generic.CreateView):
|
|||||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||||
|
|
||||||
if ra is not None:
|
if ra is not None:
|
||||||
return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
|
return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk}))
|
||||||
|
|
||||||
return super(EventRiskAssessmentCreate, self).get(self)
|
return super(EventRiskAssessmentCreate, self).get(self)
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
|
||||||
form = super(EventRiskAssessmentCreate, self).get_form(**kwargs)
|
|
||||||
epk = self.kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
form.instance.event = event
|
|
||||||
return form
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventRiskAssessmentCreate, self).get_context_data(**kwargs)
|
|
||||||
epk = self.kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
context['event'] = event
|
|
||||||
context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id)
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
return reverse('ra_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
class EventRiskAssessmentEdit(generic.UpdateView):
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
template_name = 'risk_assessment_form.html'
|
template_name = 'hs/risk_assessment_form.html'
|
||||||
form_class = forms.EventRiskAssessmentForm
|
form_class = forms.EventRiskAssessmentForm
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -54,7 +71,7 @@ class EventRiskAssessmentEdit(generic.UpdateView):
|
|||||||
ra.reviewed_by = None
|
ra.reviewed_by = None
|
||||||
ra.reviewed_at = None
|
ra.reviewed_at = None
|
||||||
ra.save()
|
ra.save()
|
||||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
return reverse('ra_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
|
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
|
||||||
@@ -62,63 +79,34 @@ class EventRiskAssessmentEdit(generic.UpdateView):
|
|||||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
ra = models.RiskAssessment.objects.get(pk=rpk)
|
||||||
context['event'] = ra.event
|
context['event'] = ra.event
|
||||||
context['edit'] = True
|
context['edit'] = True
|
||||||
context['page_title'] = 'Edit Risk Assessment for Event {}'.format(ra.event.display_id)
|
context['page_title'] = f'Edit Risk Assessment for Event {ra.event.display_id}'
|
||||||
|
get_related(context['form'], context)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentDetail(generic.DetailView):
|
class EventRiskAssessmentDetail(generic.DetailView):
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
template_name = 'risk_assessment_detail.html'
|
template_name = 'hs/risk_assessment_detail.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventRiskAssessmentDetail, self).get_context_data(**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)
|
context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentList(generic.ListView):
|
|
||||||
paginate_by = 20
|
|
||||||
model = models.RiskAssessment
|
|
||||||
template_name = 'hs_object_list.html'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
|
|
||||||
context['title'] = 'Risk Assessment'
|
|
||||||
context['view'] = 'ra_detail'
|
|
||||||
context['edit'] = 'ra_edit'
|
|
||||||
context['review'] = 'ra_review'
|
|
||||||
context['perm'] = 'perms.RIGS.review_riskassessment'
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentReview(generic.View):
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
rpk = kwargs.get('pk')
|
|
||||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
|
||||||
with reversion.create_revision():
|
|
||||||
reversion.set_user(self.request.user)
|
|
||||||
ra.reviewed_by = self.request.user
|
|
||||||
ra.reviewed_at = timezone.now()
|
|
||||||
ra.save()
|
|
||||||
return HttpResponseRedirect(reverse_lazy('ra_list'))
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistDetail(generic.DetailView):
|
class EventChecklistDetail(generic.DetailView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'event_checklist_detail.html'
|
template_name = 'hs/event_checklist_detail.html'
|
||||||
|
|
||||||
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 <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
|
context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistEdit(generic.UpdateView):
|
class EventChecklistEdit(generic.UpdateView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'event_checklist_form.html'
|
template_name = 'hs/event_checklist_form.html'
|
||||||
form_class = forms.EventChecklistForm
|
form_class = forms.EventChecklistForm
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -126,7 +114,7 @@ class EventChecklistEdit(generic.UpdateView):
|
|||||||
ec.reviewed_by = None
|
ec.reviewed_by = None
|
||||||
ec.reviewed_at = None
|
ec.reviewed_at = None
|
||||||
ec.save()
|
ec.save()
|
||||||
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventChecklistEdit, self).get_context_data(**kwargs)
|
context = super(EventChecklistEdit, self).get_context_data(**kwargs)
|
||||||
@@ -134,19 +122,14 @@ class EventChecklistEdit(generic.UpdateView):
|
|||||||
ec = models.EventChecklist.objects.get(pk=pk)
|
ec = models.EventChecklist.objects.get(pk=pk)
|
||||||
context['event'] = ec.event
|
context['event'] = ec.event
|
||||||
context['edit'] = True
|
context['edit'] = True
|
||||||
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
|
context['page_title'] = f'Edit Event Checklist for Event {ec.event.display_id}'
|
||||||
form = context['form']
|
get_related(context['form'], context)
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
|
||||||
for field, model in form.related_models.items():
|
|
||||||
value = form[field].value()
|
|
||||||
if value is not None and value != '':
|
|
||||||
context[field] = model.objects.get(pk=value)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistCreate(generic.CreateView):
|
class EventChecklistCreate(HSCreateView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'event_checklist_form.html'
|
template_name = 'hs/event_checklist_form.html'
|
||||||
form_class = forms.EventChecklistForm
|
form_class = forms.EventChecklistForm
|
||||||
|
|
||||||
# From both business logic and programming POVs, RAs must exist before ECs!
|
# From both business logic and programming POVs, RAs must exist before ECs!
|
||||||
@@ -158,64 +141,74 @@ class EventChecklistCreate(generic.CreateView):
|
|||||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||||
|
|
||||||
if ra is None:
|
if ra is None:
|
||||||
messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event))
|
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
|
||||||
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
|
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
|
||||||
|
|
||||||
return super(EventChecklistCreate, self).get(self)
|
return super(EventChecklistCreate, self).get(self)
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
def get_success_url(self):
|
||||||
form = super(EventChecklistCreate, self).get_form(**kwargs)
|
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
||||||
epk = self.kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
form.instance.event = event
|
class PowerTestDetail(generic.DetailView):
|
||||||
return form
|
model = models.PowerTestRecord
|
||||||
|
template_name = 'hs/power_detail.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventChecklistCreate, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
epk = self.kwargs.get('pk')
|
context['page_title'] = f"Power Test Record for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
context['event'] = event
|
|
||||||
context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class PowerTestEdit(generic.UpdateView):
|
||||||
|
model = models.PowerTestRecord
|
||||||
|
template_name = 'hs/power_form.html'
|
||||||
|
form_class = forms.PowerTestRecordForm
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
ec = self.get_object()
|
||||||
|
ec.reviewed_by = None
|
||||||
|
ec.reviewed_at = None
|
||||||
class EventChecklistList(generic.ListView):
|
ec.save()
|
||||||
paginate_by = 20
|
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
||||||
model = models.EventChecklist
|
|
||||||
template_name = 'hs_object_list.html'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventChecklistList, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['title'] = 'Event Checklist'
|
pk = self.kwargs.get('pk')
|
||||||
context['view'] = 'ec_detail'
|
ec = models.PowerTestRecord.objects.get(pk=pk)
|
||||||
context['edit'] = 'ec_edit'
|
context['event'] = ec.event
|
||||||
context['review'] = 'ec_review'
|
context['edit'] = True
|
||||||
context['perm'] = 'perms.RIGS.review_eventchecklist'
|
context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}'
|
||||||
|
# get_related(context['form'], context)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistReview(generic.View):
|
class PowerTestCreate(HSCreateView):
|
||||||
|
model = models.PowerTestRecord
|
||||||
|
template_name = 'hs/power_form.html'
|
||||||
|
form_class = forms.PowerTestRecordForm
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
rpk = kwargs.get('pk')
|
epk = kwargs.get('pk')
|
||||||
ec = models.EventChecklist.objects.get(pk=rpk)
|
event = models.Event.objects.get(pk=epk)
|
||||||
with reversion.create_revision():
|
|
||||||
reversion.set_user(self.request.user)
|
# Check if RA exists
|
||||||
ec.reviewed_by = self.request.user
|
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||||
ec.reviewed_at = timezone.now()
|
|
||||||
ec.save()
|
if ra is None:
|
||||||
return HttpResponseRedirect(reverse_lazy('ec_list'))
|
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.')
|
||||||
|
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
|
||||||
|
|
||||||
|
return super().get(self)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('pt_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class HSList(generic.ListView):
|
class HSList(generic.ListView):
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
model = models.Event
|
model = models.Event
|
||||||
template_name = 'hs_list.html'
|
template_name = 'hs/hs_list.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||||
@@ -224,3 +217,74 @@ class HSList(generic.ListView):
|
|||||||
context = super(HSList, self).get_context_data(**kwargs)
|
context = super(HSList, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = 'H&S Overview'
|
context['page_title'] = 'H&S Overview'
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class RAPrint(PrintView):
|
||||||
|
model = models.RiskAssessment
|
||||||
|
template_name = 'hs/ra_print.xml'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventCheckIn(generic.CreateView, ModalURLMixin):
|
||||||
|
model = models.EventCheckIn
|
||||||
|
template_name = 'hs/eventcheckin_form.html'
|
||||||
|
form_class = forms.EventCheckInForm
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...!
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk'))
|
||||||
|
context['page_title'] = f'Check In to Event {context["event"].display_id}'
|
||||||
|
# get_related(context['form'], context)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventCheckInOverride(generic.CreateView):
|
||||||
|
model = models.EventCheckIn
|
||||||
|
template_name = 'hs/eventcheckin_form.html'
|
||||||
|
form_class = forms.EditCheckInForm
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk'))
|
||||||
|
context['page_title'] = f'Manually add Check In to Event {context["event"].display_id}'
|
||||||
|
context['manual'] = True
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventCheckInEdit(generic.UpdateView, ModalURLMixin):
|
||||||
|
model = models.EventCheckIn
|
||||||
|
template_name = 'hs/eventcheckin_form.html'
|
||||||
|
form_class = forms.EditCheckInForm
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
obj = self.get_object()
|
||||||
|
if not obj.person == self.request.user and not obj.event.mic == self.request.user:
|
||||||
|
return redirect(self.request.META.get('HTTP_REFERER', '/'))
|
||||||
|
return super().dispatch(request)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...!
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['event'] = self.object.event
|
||||||
|
context['page_title'] = f'Edit Check In for Event {context["event"].display_id}'
|
||||||
|
context['edit'] = True
|
||||||
|
# get_related(context['form'], context)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventCheckOut(generic.RedirectView):
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
checkin = self.request.user.current_event()
|
||||||
|
if checkin:
|
||||||
|
checkin.end_time = timezone.now()
|
||||||
|
checkin.save()
|
||||||
|
return self.request.META.get('HTTP_REFERER', '/')
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class CalendarICS(ICalFeed):
|
|||||||
title += item.name
|
title += item.name
|
||||||
|
|
||||||
# Add the status
|
# Add the status
|
||||||
title += ' (' + str(item.get_status_display()) + ')'
|
title += f' ({item.get_status_display()})'
|
||||||
|
|
||||||
return title
|
return title
|
||||||
|
|
||||||
@@ -101,9 +101,8 @@ class CalendarICS(ICalFeed):
|
|||||||
return item.earliest_time
|
return item.earliest_time
|
||||||
|
|
||||||
def item_end_datetime(self, item):
|
def item_end_datetime(self, item):
|
||||||
if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
|
# if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
|
||||||
return item.latest_time + datetime.timedelta(days=1)
|
# return item.latest_time + datetime.timedelta(days=1)
|
||||||
|
|
||||||
return item.latest_time
|
return item.latest_time
|
||||||
|
|
||||||
def item_location(self, item):
|
def item_location(self, item):
|
||||||
@@ -115,13 +114,13 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
tz = pytz.timezone(self.timezone)
|
tz = pytz.timezone(self.timezone)
|
||||||
|
|
||||||
desc = 'Rig ID = ' + str(item.pk) + '\n'
|
desc = f'Rig ID = {item.display_id}\n'
|
||||||
desc += 'Event = ' + item.name + '\n'
|
desc += f'Event = {item.name}\n'
|
||||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
||||||
if item.is_rig and item.person:
|
if item.is_rig and item.person:
|
||||||
desc += 'Client = ' + item.person.name + (
|
desc += 'Client = ' + item.person.name + (
|
||||||
(' for ' + item.organisation.name) if item.organisation else '') + '\n'
|
(' for ' + item.organisation.name) if item.organisation else '') + '\n'
|
||||||
desc += 'Status = ' + str(item.get_status_display()) + '\n'
|
desc += f'Status = {item.get_status_display()}\n'
|
||||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
@@ -140,23 +139,18 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.description:
|
if item.description:
|
||||||
desc += 'Event Description:\n' + item.description + '\n\n'
|
desc += f'Event Description:\n{item.description}\n\n'
|
||||||
# if item.notes: // Need to add proper keyholder checks before this gets put back
|
# if item.notes: // Need to add proper keyholder checks before this gets put back
|
||||||
# desc += 'Notes:\n'+item.notes+'\n\n'
|
# desc += 'Notes:\n'+item.notes+'\n\n'
|
||||||
|
|
||||||
base_url = "https://rigs.nottinghamtec.co.uk"
|
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
|
||||||
desc += 'URL = ' + base_url + str(item.get_absolute_url())
|
|
||||||
|
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
def item_link(self, item):
|
def item_link(self, item):
|
||||||
# Make a link to the event in the web interface
|
# Make a link to the event in the web interface
|
||||||
# base_url = "https://pyrigs.nottinghamtec.co.uk"
|
|
||||||
return item.get_absolute_url()
|
return item.get_absolute_url()
|
||||||
|
|
||||||
# def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though
|
|
||||||
# return ''
|
|
||||||
|
|
||||||
def item_updated(self, item): # some ical clients will display this
|
def item_updated(self, item): # some ical clients will display this
|
||||||
return item.last_edited_at
|
return item.last_edited_at
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import premailer
|
import premailer
|
||||||
import simplejson
|
import simplejson
|
||||||
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 import finders
|
from django.contrib.staticfiles import finders
|
||||||
@@ -24,10 +19,9 @@ from django.urls import reverse_lazy
|
|||||||
from django.utils import timezone
|
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 z3c.rml import rml2pdf
|
|
||||||
|
|
||||||
from PyRIGS import decorators
|
from PyRIGS import decorators
|
||||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin
|
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
|
|
||||||
__author__ = 'ghost'
|
__author__ = 'ghost'
|
||||||
@@ -98,11 +92,8 @@ class EventCreate(generic.CreateView):
|
|||||||
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
|
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
|
||||||
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
||||||
|
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
get_related(form, context)
|
||||||
for field, model in form.related_models.items():
|
|
||||||
value = form[field].value()
|
|
||||||
if value is not None and value != '':
|
|
||||||
context[field] = model.objects.get(pk=value)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -121,11 +112,7 @@ class EventUpdate(generic.UpdateView):
|
|||||||
|
|
||||||
form = context['form']
|
form = context['form']
|
||||||
|
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
get_related(form, context)
|
||||||
for field, model in form.related_models.items():
|
|
||||||
value = form[field].value()
|
|
||||||
if value is not None and value != '':
|
|
||||||
context[field] = model.objects.get(pk=value)
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -178,35 +165,16 @@ class EventDuplicate(EventUpdate):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventPrint(generic.View):
|
class EventPrint(PrintView):
|
||||||
def get(self, request, pk):
|
model = models.Event
|
||||||
object = get_object_or_404(models.Event, pk=pk)
|
template_name = 'event_print.xml'
|
||||||
template = get_template('event_print.xml')
|
append_terms = True
|
||||||
|
|
||||||
merger = PdfFileMerger()
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
context = {
|
context['quote'] = True
|
||||||
'object': object,
|
context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf"
|
||||||
'quote': True,
|
return context
|
||||||
'current_user': request.user,
|
|
||||||
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
|
|
||||||
}
|
|
||||||
|
|
||||||
rml = template.render(context)
|
|
||||||
buffer = rml2pdf.parseString(rml)
|
|
||||||
merger.append(PdfFileReader(buffer))
|
|
||||||
buffer.close()
|
|
||||||
|
|
||||||
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
|
||||||
merger.append(BytesIO(terms.read()))
|
|
||||||
|
|
||||||
merged = BytesIO()
|
|
||||||
merger.write(merged)
|
|
||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
|
||||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
|
||||||
response.write(merged.getvalue())
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class EventArchive(generic.ListView):
|
class EventArchive(generic.ListView):
|
||||||
@@ -216,7 +184,6 @@ class EventArchive(generic.ListView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
context['start'] = self.request.GET.get('start', None)
|
context['start'] = self.request.GET.get('start', None)
|
||||||
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
|
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
|
||||||
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
|
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
|
||||||
@@ -240,32 +207,17 @@ class EventArchive(generic.ListView):
|
|||||||
filter &= Q(start_date__gte=start)
|
filter &= Q(start_date__gte=start)
|
||||||
|
|
||||||
q = self.request.GET.get('q', "")
|
q = self.request.GET.get('q', "")
|
||||||
|
objects = self.model.objects.all()
|
||||||
|
|
||||||
if q != "":
|
if q:
|
||||||
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
|
objects = self.model.objects.search(q)
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
qfilter = qfilter | Q(pk=val)
|
|
||||||
except: # noqa not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if q[0] == "N":
|
|
||||||
val = int(q[1:])
|
|
||||||
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
filter &= qfilter
|
|
||||||
|
|
||||||
status = self.request.GET.getlist('status', "")
|
status = self.request.GET.getlist('status', "")
|
||||||
|
|
||||||
if len(status) > 0:
|
if len(status) > 0:
|
||||||
filter &= Q(status__in=status)
|
filter &= Q(status__in=status)
|
||||||
|
|
||||||
qs = self.model.objects.filter(filter).order_by('-start_date')
|
qs = objects.filter(filter).order_by('-start_date')
|
||||||
|
|
||||||
# Preselect related for efficiency
|
# Preselect related for efficiency
|
||||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||||
@@ -320,7 +272,7 @@ class EventAuthorise(generic.UpdateView):
|
|||||||
messages.add_message(self.request, messages.WARNING,
|
messages.add_message(self.request, messages.WARNING,
|
||||||
"This event has already been authorised, but the amount has changed. " +
|
"This event has already been authorised, but the amount has changed. " +
|
||||||
"Please check the amount and reauthorise.")
|
"Please check the amount and reauthorise.")
|
||||||
return super(EventAuthorise, self).get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
def get_form(self, **kwargs):
|
||||||
form = super().get_form(**kwargs)
|
form = super().get_form(**kwargs)
|
||||||
@@ -389,13 +341,13 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
context['to_name'] = event.organisation.name
|
context['to_name'] = event.organisation.name
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
msg = EmailMultiAlternatives(
|
||||||
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
|
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
|
||||||
get_template("eventauthorisation_client_request.txt").render(context),
|
get_template("email/eventauthorisation_client_request.txt").render(context),
|
||||||
to=[email],
|
to=[email],
|
||||||
reply_to=[self.request.user.email],
|
reply_to=[self.request.user.email],
|
||||||
)
|
)
|
||||||
css = finders.find('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("email/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')
|
||||||
|
|
||||||
@@ -405,7 +357,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
|
|
||||||
|
|
||||||
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||||
template_name = "eventauthorisation_client_request.html"
|
template_name = "email/eventauthorisation_client_request.html"
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
from RIGS.admin import AssociateAdmin
|
||||||
from assets import models as assets
|
from assets import models as assets
|
||||||
|
|
||||||
|
|
||||||
@@ -17,9 +17,13 @@ class AssetStatusAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
@admin.register(assets.Supplier)
|
@admin.register(assets.Supplier)
|
||||||
class SupplierAdmin(VersionAdmin):
|
class SupplierAdmin(AssociateAdmin):
|
||||||
list_display = ['id', 'name']
|
list_display = ['id', 'name']
|
||||||
ordering = ['id']
|
ordering = ['id']
|
||||||
|
merge_fields = ['name', 'phone', 'email', 'address', 'notes']
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super(VersionAdmin, self).get_queryset(request)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(assets.Asset)
|
@admin.register(assets.Asset)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class AssetSearchForm(forms.Form):
|
|||||||
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
||||||
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
||||||
is_cable = forms.BooleanField(required=False)
|
is_cable = forms.BooleanField(required=False)
|
||||||
|
cable_type = forms.ModelMultipleChoiceField(models.CableType.objects.all(), required=False)
|
||||||
date_acquired = forms.DateField(required=False)
|
date_acquired = forms.DateField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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.db import transaction
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
@@ -104,8 +105,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
prefix = random.choice(asset_prefixes)
|
prefix = random.choice(asset_prefixes)
|
||||||
asset_id = str(get_available_asset_id(wanted_prefix=prefix))
|
asset_id = get_available_asset_id(wanted_prefix=prefix)
|
||||||
asset_id = prefix + asset_id
|
|
||||||
asset = models.Asset(
|
asset = models.Asset(
|
||||||
asset_id=asset_id,
|
asset_id=asset_id,
|
||||||
description=random.choice(asset_description),
|
description=random.choice(asset_description),
|
||||||
@@ -125,5 +125,9 @@ class Command(BaseCommand):
|
|||||||
if i % 3 == 0:
|
if i % 3 == 0:
|
||||||
asset.purchased_from = random.choice(self.suppliers)
|
asset.purchased_from = random.choice(self.suppliers)
|
||||||
|
|
||||||
asset.clean()
|
with transaction.atomic():
|
||||||
asset.save()
|
try:
|
||||||
|
asset.clean()
|
||||||
|
asset.save()
|
||||||
|
except IntegrityError:
|
||||||
|
pass
|
||||||
|
|||||||
19
assets/migrations/0023_alter_asset_purchased_from.py
Normal file
19
assets/migrations/0023_alter_asset_purchased_from.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-02-14 15:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0022_alter_cabletype_unique_together'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='asset',
|
||||||
|
name='purchased_from',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.supplier'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
assets/migrations/0024_alter_asset_salvage_value.py
Normal file
18
assets/migrations/0024_alter_asset_salvage_value.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-02-14 23:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0023_alter_asset_purchased_from'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='asset',
|
||||||
|
name='salvage_value',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-05-26 09:22
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0024_alter_asset_salvage_value'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='asset',
|
||||||
|
old_name='salvage_value',
|
||||||
|
new_name='replacement_cost',
|
||||||
|
),
|
||||||
|
]
|
||||||
24
assets/migrations/0026_auto_20220526_1623.py
Normal file
24
assets/migrations/0026_auto_20220526_1623.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-05-26 15:23
|
||||||
|
|
||||||
|
import assets.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0025_rename_salvage_value_asset_replacement_cost'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='asset',
|
||||||
|
name='purchase_price',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='asset',
|
||||||
|
name='replacement_cost',
|
||||||
|
field=models.DecimalField(decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
assets/migrations/0027_asset_nickname.py
Normal file
18
assets/migrations/0027_asset_nickname.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2022-12-11 00:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0026_auto_20220526_1623'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='asset',
|
||||||
|
name='nickname',
|
||||||
|
field=models.CharField(blank=True, max_length=120),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,11 +2,12 @@ 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 import Q
|
||||||
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
|
||||||
|
|
||||||
from RIGS.models import Profile
|
from RIGS.models import Profile, ContactableManager
|
||||||
from versioning.versioning import RevisionMixin
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ class Supplier(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
notes = models.TextField(blank=True, default="")
|
notes = models.TextField(blank=True, default="")
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
@@ -88,23 +91,24 @@ class CableType(models.Model):
|
|||||||
return reverse('cable_type_detail', kwargs={'pk': self.pk})
|
return reverse('cable_type_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class AssetManager(models.Manager):
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query) | Q(nickname__icontains=query))
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def get_available_asset_id(wanted_prefix=""):
|
def get_available_asset_id(wanted_prefix=""):
|
||||||
sql = """
|
last_asset = Asset.objects.filter(asset_id_prefix=wanted_prefix).last()
|
||||||
SELECT a.asset_id_number+1
|
last_asset_id = last_asset.asset_id_number if last_asset else 0
|
||||||
FROM assets_asset a
|
return wanted_prefix + str(last_asset_id + 1)
|
||||||
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 validate_positive(value):
|
||||||
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
|
if value < 0:
|
||||||
"""
|
raise ValidationError("A price cannot be negative")
|
||||||
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
|
||||||
@@ -116,12 +120,13 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
|
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
|
||||||
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
|
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
|
||||||
serial_number = models.CharField(max_length=150, blank=True)
|
serial_number = models.CharField(max_length=150, blank=True)
|
||||||
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True, related_name="assets")
|
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets")
|
||||||
date_acquired = models.DateField()
|
date_acquired = models.DateField()
|
||||||
date_sold = models.DateField(blank=True, null=True)
|
date_sold = models.DateField(blank=True, null=True)
|
||||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||||
salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
nickname = models.CharField(max_length=120, blank=True)
|
||||||
|
|
||||||
# Audit
|
# Audit
|
||||||
last_audited_at = models.DateTimeField(blank=True, null=True)
|
last_audited_at = models.DateTimeField(blank=True, null=True)
|
||||||
@@ -142,6 +147,8 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
reversion_perm = 'assets.asset_finance'
|
reversion_perm = 'assets.asset_finance'
|
||||||
|
|
||||||
|
objects = AssetManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||||
permissions = [
|
permissions = [
|
||||||
@@ -165,12 +172,6 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
errdict["asset_id"] = [
|
errdict["asset_id"] = [
|
||||||
"An Asset ID can only consist of letters and numbers, with a final number"]
|
"An Asset ID can only consist of letters and numbers, with a final number"]
|
||||||
|
|
||||||
if self.purchase_price and self.purchase_price < 0:
|
|
||||||
errdict["purchase_price"] = ["A price cannot be negative"]
|
|
||||||
|
|
||||||
if self.salvage_value and self.salvage_value < 0:
|
|
||||||
errdict["salvage_value"] = ["A price cannot be negative"]
|
|
||||||
|
|
||||||
if self.is_cable:
|
if self.is_cable:
|
||||||
if not self.length or self.length <= 0:
|
if not self.length or self.length <= 0:
|
||||||
errdict["length"] = ["The length of a cable must be more than 0"]
|
errdict["length"] = ["The length of a cable must be more than 0"]
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
date = new Date();
|
date = new Date();
|
||||||
}
|
}
|
||||||
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
function setFieldValue(ID, CSA) {
|
function setFieldValue(ID, CSA) {
|
||||||
$('#' + String(ID)).val(CSA);
|
$('#' + String(ID)).val(CSA);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
function checkIfCableHidden() {
|
function checkIfCableHidden() {
|
||||||
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
||||||
@@ -39,16 +41,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-2">
|
||||||
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button>
|
<button class="btn btn-info" onclick="return setAcquired(true);" tabindex="-1">Today</button>
|
||||||
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button>
|
<button class="btn btn-warning" onclick="return setAcquired(false);" tabindex="-1">Unknown</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.salvage_value col="col-6" prepend="£" %}
|
{% include 'partials/form_field.html' with field=form.replacement_cost col="col-6" prepend="£" %}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
@@ -64,16 +66,16 @@
|
|||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
|
<button class="btn btn-danger" onclick="return setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
|
||||||
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
|
<button class="btn btn-success" onclick="return setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
|
||||||
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
|
<button class="btn btn-info" onclick="return setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
|
<button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
|
||||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
<button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,13 +75,13 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
|
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
|
||||||
<label for="category" class="sr-only">Category</label>
|
<label for="category" class="sr-only">Category</label>
|
||||||
{% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
|
{% render_field form.category|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
|
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
|
||||||
<label for="status" class="sr-only">Status</label>
|
<label for="status" class="sr-only">Status</label>
|
||||||
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
{% render_field form.status|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col mt-2">
|
<div class="col mt-2">
|
||||||
@@ -121,6 +121,7 @@
|
|||||||
</button></span>{%endfor%}</p>
|
</button></span>{%endfor%}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h3>{{ object_list.count }} assets</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col px-0">
|
<div class="col px-0">
|
||||||
{% include 'partials/asset_list_table.html' %}
|
{% include 'partials/asset_list_table.html' %}
|
||||||
|
|||||||
162
assets/templates/cable_list.html
Normal file
162
assets/templates/cable_list.html
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{% extends 'base_assets.html' %}
|
||||||
|
{% load button from filters %}
|
||||||
|
{% load ids_from_objects from asset_tags %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block preload_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/selects.js' %}" async></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script>
|
||||||
|
//Get querystring value
|
||||||
|
function getParameterByName(name) {
|
||||||
|
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||||
|
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||||
|
results = regex.exec(location.search);
|
||||||
|
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||||
|
}
|
||||||
|
//Function used to remove querystring
|
||||||
|
function removeQString(key) {
|
||||||
|
var urlValue=document.location.href;
|
||||||
|
|
||||||
|
//Get query string value
|
||||||
|
var searchUrl=location.search;
|
||||||
|
|
||||||
|
if(key!=="") {
|
||||||
|
oldValue = getParameterByName(key);
|
||||||
|
removeVal=key+"="+oldValue;
|
||||||
|
if(searchUrl.indexOf('?'+removeVal+'&')!== "-1") {
|
||||||
|
urlValue=urlValue.replace('?'+removeVal+'&','?');
|
||||||
|
}
|
||||||
|
else if(searchUrl.indexOf('&'+removeVal+'&')!== "-1") {
|
||||||
|
urlValue=urlValue.replace('&'+removeVal+'&','&');
|
||||||
|
}
|
||||||
|
else if(searchUrl.indexOf('?'+removeVal)!== "-1") {
|
||||||
|
urlValue=urlValue.replace('?'+removeVal,'');
|
||||||
|
}
|
||||||
|
else if(searchUrl.indexOf('&'+removeVal)!== "-1") {
|
||||||
|
urlValue=urlValue.replace('&'+removeVal,'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var searchUrl=location.search;
|
||||||
|
urlValue=urlValue.replace(searchUrl,'');
|
||||||
|
}
|
||||||
|
history.pushState({state:1, rand: Math.random()}, '', urlValue);
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>{{ object_list.count }} cables with a total length of {{ total_length|default:"0" }}m</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col px-0">
|
||||||
|
<form id="asset-search-form" method="GET">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
|
||||||
|
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
|
||||||
|
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
|
||||||
|
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row mt-2">
|
||||||
|
<div class="col">
|
||||||
|
<div id="category-group" class="form-group px-1">
|
||||||
|
<label for="category" class="sr-only">Category</label>
|
||||||
|
{% render_field form.category|attr:'multiple'|add_class:'selectpicker col-sm pl-0' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div id="status-group" class="form-group px-1">
|
||||||
|
<label for="status" class="sr-only">Status</label>
|
||||||
|
{% render_field form.status|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-group d-flex flex-nowrap">
|
||||||
|
<label for="cable_type" class="sr-only">Cable Type</label>
|
||||||
|
{% render_field form.cable_type|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Cable Type" data-header="Cable Type" data-actions-box="true" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="form-group d-flex flex-nowrap">
|
||||||
|
<label for="date_acquired" class="text-nowrap mt-auto">Date Acquired</label>
|
||||||
|
{% render_field form.date_acquired|add_class:'form-control mx-2' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto mr-auto">
|
||||||
|
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-2">
|
||||||
|
<div class="col text-right px-0">
|
||||||
|
{% button 'new' 'asset_create' style="width: 6em" %}
|
||||||
|
{% if object_list %}
|
||||||
|
<a class="btn btn-primary" href="{% url 'generate_labels' object_list|ids_from_objects %}"><span class="fas fa-barcode"></span> Generate Labels</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-2">
|
||||||
|
<div class="col bg-dark text-white rounded pt-3">
|
||||||
|
{# TODO Gotta be a cleaner way to do this... #}
|
||||||
|
<p><span class="ml-2">Active Filters: </span> {% for filter in category_filters %}<span class="badge badge-info mx-1 ">{{filter}}<button type="button" class="btn btn-link p-0 ml-1 align-baseline">
|
||||||
|
<span aria-hidden="true" class="fas fa-times" onclick="removeQString('category', '{{filter.id}}')"></span>
|
||||||
|
</button></span>{%endfor%}{% for filter in status_filters %}<span class="badge badge-info mx-1 ">{{filter}}<button type="button" class="btn btn-link p-0 ml-1 align-baseline">
|
||||||
|
<span aria-hidden="true" class="fas fa-times" onclick="removeQString('status', '{{filter.id}}')"></span>
|
||||||
|
</button></span>{%endfor%}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col px-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Asset ID</th>
|
||||||
|
<th scope="col">Description</th>
|
||||||
|
<th scope="col">Category</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">Length</th>
|
||||||
|
<th scope="col">Cable Type</th>
|
||||||
|
<th scope="col" class="d-none d-sm-table-cell">Quick Links</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="asset_table_body">
|
||||||
|
{% for item in object_list %}
|
||||||
|
<tr class="table-{{ item.status.display_class|default:'' }} assetRow">
|
||||||
|
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
|
||||||
|
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
|
||||||
|
<td class="assetCategory align-middle">{{ item.category }}</td>
|
||||||
|
<td class="assetStatus align-middle">{{ item.status }}</td>
|
||||||
|
<td style="background-color:{% if item.length == 20.0 %}#304486{% elif item.length == 10.0 %}green{%elif item.length == 5.0 %}red{% endif %} !important;">{{ item.length }}m</td>
|
||||||
|
<td>{{ item.cable_type }}</td>
|
||||||
|
<td class="d-none d-sm-table-cell">
|
||||||
|
{% include 'partials/asset_list_buttons.html' %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6">Nothing found</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,22 +12,18 @@
|
|||||||
</template>
|
</template>
|
||||||
<stylesheet>
|
<stylesheet>
|
||||||
<blockTableStyle id="table">
|
<blockTableStyle id="table">
|
||||||
<!-- show a grid: this also comes in handy for debugging your tables.-->
|
|
||||||
<lineStyle kind="GRID" colorName="black" thickness="1" start="0,0" stop="-1,-1" />
|
|
||||||
</blockTableStyle>
|
</blockTableStyle>
|
||||||
</stylesheet>
|
</stylesheet>
|
||||||
<story>
|
<story>
|
||||||
<blockTable style="table">
|
<blockTable style="table">
|
||||||
{% for i in images0 %}
|
{% for i in images0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="180" height="55" borderStrokeWidth="1"
|
||||||
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
borderStrokeColor="black"><image file="data:image/png;base64,{{image.1}}" x="0" y="0"
|
||||||
<td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
{% if image.0.csa >= 4 %}width="180" height="55"{% else %}width="130" height="38"{%endif%}/></illustration>{% endif %}{% endwith %}</td>
|
||||||
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
<td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="180" height="55"><image file="data:image/png;base64,{{image.1}}" x="0" y="0"
|
||||||
<td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
{% if image.0.csa >= 4 %}width="180" height="55"{% else %}width="130" height="38"{%endif%}/></illustration>{% endif %}{% endwith %}</td>
|
||||||
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
<td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="180" height="55"><image file="data:image/png;base64,{{image.1}}" x="0" y="0" {% if image.0.csa >= 4 %}width="180" height="55"{% else %}width="130" height="38"{%endif%}/></illustration>{% endif %}{% endwith %}</td>
|
||||||
<td>{% with images3|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
|
|
||||||
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</blockTable>
|
</blockTable>
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
<label for="{{ form.description.id_for_label }}">Description</label>
|
<label for="{{ form.description.id_for_label }}">Description</label>
|
||||||
{% render_field form.description|add_class:'form-control' value=object.description %}
|
{% render_field form.description|add_class:'form-control' value=object.description %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.nickname.id_for_label }}">Nickname</label>
|
||||||
|
{% render_field form.nickname|add_class:'form-control' value=object.nickname %}
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.category.id_for_label }}" >Category</label>
|
<label for="{{ form.category.id_for_label }}" >Category</label>
|
||||||
{% render_field form.category|add_class:'form-control'%}
|
{% render_field form.category|add_class:'form-control'%}
|
||||||
@@ -45,7 +49,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<dt>Asset ID</dt>
|
<dt>Asset ID</dt>
|
||||||
<dd>{{ object.asset_id }}</dd>
|
<dd>{{ object.asset_id }}</dd>
|
||||||
|
{% if object.nickname %}
|
||||||
|
<dt>Nickname</dt>
|
||||||
|
<dd>"{{ object.nickname }}"</dd>
|
||||||
|
{% endif %}
|
||||||
<dt>Description</dt>
|
<dt>Description</dt>
|
||||||
<dd>{{ object.description }}</dd>
|
<dd>{{ object.description }}</dd>
|
||||||
|
|
||||||
|
|||||||
14
assets/templates/partials/asset_list_buttons.html
Normal file
14
assets/templates/partials/asset_list_buttons.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% load button from filters %}
|
||||||
|
{% if audit %}
|
||||||
|
<a type="button" class="btn btn-info btn-sm modal-href" href="{% url 'asset_audit' item.asset_id %}"><i class="fas fa-certificate"></i> Audit</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% button 'view' url='asset_detail' pk=item.asset_id clazz="btn-sm" %}
|
||||||
|
{% if perms.assets.change_asset %}
|
||||||
|
{% 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" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -18,19 +18,7 @@
|
|||||||
<td class="assetCategory align-middle">{{ item.category }}</td>
|
<td class="assetCategory align-middle">{{ item.category }}</td>
|
||||||
<td class="assetStatus align-middle">{{ item.status }}</td>
|
<td class="assetStatus align-middle">{{ item.status }}</td>
|
||||||
<td class="d-none d-sm-table-cell">
|
<td class="d-none d-sm-table-cell">
|
||||||
{% if audit %}
|
{% include 'partials/asset_list_buttons.html' %}
|
||||||
<a type="button" class="btn btn-info btn-sm modal-href" href="{% url 'asset_audit' item.asset_id %}"><i class="fas fa-certificate"></i> Audit</a>
|
|
||||||
{% else %}
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
{% button 'view' url='asset_detail' pk=item.asset_id clazz="btn-sm" %}
|
|
||||||
{% if perms.assets.change_asset %}
|
|
||||||
{% 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" %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load title_spaced from filters %}
|
{% load title_spaced from filters %}
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<label for="{{ field.id_for_label }}" {% if col %}class="col-2 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
||||||
{% if append or prepend %}
|
{% if append or prepend %}
|
||||||
<div class="input-group {{col}}">
|
<div class="input-group {{col}}">
|
||||||
{% if prepend %}
|
{% if prepend %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{% if create or edit or duplicate %}
|
{% if create or edit or duplicate %}
|
||||||
<div class="form-group" id="parent-group">
|
<div class="form-group" id="parent-group">
|
||||||
<label for="selectpicker">Set Parent</label>
|
<label for="selectpicker">Set Parent</label>
|
||||||
<select name="parent" id="parent_id" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='asset' %}?fields=asset_id,description">
|
<select name="parent" id="parent_id" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='asset' %}?fields=asset_id,description">
|
||||||
{% if object.parent %}
|
{% if object.parent %}
|
||||||
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
|
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||||
{% if object.purchased_from %}
|
{% if object.purchased_from %}
|
||||||
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label>
|
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||||
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
|
{% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
<dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</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>Replacement Cost</dt>
|
||||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
<dd>£{{ object.replacement_cost|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 %}
|
||||||
|
|||||||
@@ -18,18 +18,23 @@ def status(db):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_cable(db, category, status):
|
def cable_type(db):
|
||||||
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
|
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_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_type
|
||||||
yield cable
|
|
||||||
connector.delete()
|
connector.delete()
|
||||||
cable_type.delete()
|
cable_type.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_cable(db, category, status, cable_type):
|
||||||
|
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", replacement_cost=50)
|
||||||
|
yield cable
|
||||||
cable.delete()
|
cable.delete()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_asset(db, category, status):
|
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))
|
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
|
||||||
yield asset
|
yield asset
|
||||||
asset.delete()
|
asset.delete()
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class AssetForm(FormPage):
|
|||||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||||
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||||
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
||||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||||
@@ -221,7 +221,7 @@ class AssetAuditList(AssetList):
|
|||||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||||
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
||||||
|
|||||||
@@ -94,6 +94,55 @@ class TestAssetList(AutoLoginTest):
|
|||||||
self.assertEqual("10", asset_ids[1])
|
self.assertEqual("10", asset_ids[1])
|
||||||
|
|
||||||
|
|
||||||
|
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):
|
||||||
|
page = pages.AssetCreate(logged_in_browser.driver, live_server.url).open()
|
||||||
|
wait = WebDriverWait(logged_in_browser.driver, 20)
|
||||||
|
page.description = str(cable_type)
|
||||||
|
page.category = category.name
|
||||||
|
page.status = status.name
|
||||||
|
page.serial_number = "MELON-MELON-MELON"
|
||||||
|
page.comments = "You might need that"
|
||||||
|
page.replacement_cost = "666"
|
||||||
|
page.is_cable = True
|
||||||
|
|
||||||
|
assert logged_in_browser.driver.find_element(By.ID, 'cable-table').is_displayed()
|
||||||
|
wait.until(animation_is_finished())
|
||||||
|
page.cable_type = str(cable_type)
|
||||||
|
page.length = 10
|
||||||
|
page.csa = "1.5"
|
||||||
|
|
||||||
|
page.submit()
|
||||||
|
assert page.success
|
||||||
|
|
||||||
|
|
||||||
|
def test_asset_edit(logged_in_browser, admin_user, live_server, test_asset):
|
||||||
|
page = pages.AssetEdit(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open()
|
||||||
|
|
||||||
|
assert logged_in_browser.driver.find_element(By.ID, 'id_asset_id').get_attribute('readonly') is not None
|
||||||
|
|
||||||
|
new_description = "Big Shelf"
|
||||||
|
page.description = new_description
|
||||||
|
|
||||||
|
page.submit()
|
||||||
|
assert page.success
|
||||||
|
|
||||||
|
assert models.Asset.objects.get(asset_id=test_asset.asset_id).description == new_description
|
||||||
|
|
||||||
|
|
||||||
|
def test_asset_duplicate(logged_in_browser, admin_user, live_server, test_asset):
|
||||||
|
page = pages.AssetDuplicate(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open()
|
||||||
|
|
||||||
|
assert test_asset.asset_id != page.asset_id
|
||||||
|
assert test_asset.description == page.description
|
||||||
|
assert test_asset.status.name == page.status
|
||||||
|
assert test_asset.category.name == page.category
|
||||||
|
assert test_asset.date_acquired == page.date_acquired.date()
|
||||||
|
|
||||||
|
page.submit()
|
||||||
|
assert page.success
|
||||||
|
assert models.Asset.objects.last().description == test_asset.description
|
||||||
|
|
||||||
|
|
||||||
@screenshot_failure_cls
|
@screenshot_failure_cls
|
||||||
class TestAssetForm(AutoLoginTest):
|
class TestAssetForm(AutoLoginTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -130,7 +179,7 @@ class TestAssetForm(AutoLoginTest):
|
|||||||
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.purchase_price = "12.99"
|
||||||
self.page.salvage_value = "99.12"
|
self.page.replacement_cost = "99.12"
|
||||||
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
|
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)
|
||||||
@@ -146,7 +195,7 @@ class TestAssetForm(AutoLoginTest):
|
|||||||
# 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()
|
||||||
|
|
||||||
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
|
self.assertFalse(self.driver.find_element(By.ID, 'cable-table').is_displayed())
|
||||||
|
|
||||||
self.page.submit()
|
self.page.submit()
|
||||||
self.assertTrue(self.page.success)
|
self.assertTrue(self.page.success)
|
||||||
@@ -160,50 +209,6 @@ class TestAssetForm(AutoLoginTest):
|
|||||||
# This one is important as it defaults to today's date
|
# This one is important as it defaults to today's date
|
||||||
self.assertEqual(asset.date_acquired, acquired)
|
self.assertEqual(asset.date_acquired, acquired)
|
||||||
|
|
||||||
def test_cable_create(self):
|
|
||||||
self.page.description = "IEC -> IEC"
|
|
||||||
self.page.category = "Health & Safety"
|
|
||||||
self.page.status = "O.K."
|
|
||||||
self.page.serial_number = "MELON-MELON-MELON"
|
|
||||||
self.page.comments = "You might need that"
|
|
||||||
self.page.is_cable = True
|
|
||||||
|
|
||||||
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
|
|
||||||
self.wait.until(animation_is_finished())
|
|
||||||
self.page.cable_type = "IEC → IEC"
|
|
||||||
self.page.socket = "IEC"
|
|
||||||
self.page.length = 10
|
|
||||||
self.page.csa = "1.5"
|
|
||||||
|
|
||||||
self.page.submit()
|
|
||||||
self.assertTrue(self.page.success)
|
|
||||||
|
|
||||||
def test_asset_edit(self):
|
|
||||||
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
|
||||||
|
|
||||||
self.assertIsNotNone(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly'))
|
|
||||||
|
|
||||||
new_description = "Big Shelf"
|
|
||||||
self.page.description = new_description
|
|
||||||
|
|
||||||
self.page.submit()
|
|
||||||
self.assertTrue(self.page.success)
|
|
||||||
|
|
||||||
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
|
|
||||||
|
|
||||||
def test_asset_duplicate(self):
|
|
||||||
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
|
||||||
|
|
||||||
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
|
|
||||||
self.assertEqual(self.parent.description, self.page.description)
|
|
||||||
self.assertEqual(self.parent.status.name, self.page.status)
|
|
||||||
self.assertEqual(self.parent.category.name, self.page.category)
|
|
||||||
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
|
|
||||||
|
|
||||||
self.page.submit()
|
|
||||||
self.assertTrue(self.page.success)
|
|
||||||
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
|
|
||||||
|
|
||||||
|
|
||||||
@screenshot_failure_cls
|
@screenshot_failure_cls
|
||||||
class TestSupplierList(AutoLoginTest):
|
class TestSupplierList(AutoLoginTest):
|
||||||
@@ -283,6 +288,28 @@ def test_audit_search(logged_in_browser, live_server, test_asset):
|
|||||||
assert logged_in_browser.is_text_present("Asset with that ID does not exist!")
|
assert logged_in_browser.is_text_present("Asset with that ID does not exist!")
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_success(logged_in_browser, admin_user, live_server, test_asset):
|
||||||
|
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
|
||||||
|
wait = WebDriverWait(logged_in_browser.driver, 20)
|
||||||
|
page.set_query(test_asset.asset_id)
|
||||||
|
page.search()
|
||||||
|
wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||||
|
# Now do it properly
|
||||||
|
page.modal.description = new_desc = "A BIG hammer"
|
||||||
|
page.modal.submit()
|
||||||
|
logged_in_browser.driver.implicitly_wait(4)
|
||||||
|
wait.until(animation_is_finished())
|
||||||
|
submit_time = timezone.now()
|
||||||
|
# Check data is correct
|
||||||
|
test_asset.refresh_from_db()
|
||||||
|
assert test_asset.description in new_desc
|
||||||
|
# Make sure audit 'log' was filled out
|
||||||
|
assert admin_user.initials == test_asset.last_audited_by.initials
|
||||||
|
assert_times_almost_equal(submit_time, test_asset.last_audited_at)
|
||||||
|
# Check we've removed it from the 'needing audit' list
|
||||||
|
assert test_asset.asset_id not in page.assets
|
||||||
|
|
||||||
|
|
||||||
@screenshot_failure_cls
|
@screenshot_failure_cls
|
||||||
class TestAssetAudit(AutoLoginTest):
|
class TestAssetAudit(AutoLoginTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -293,14 +320,14 @@ class TestAssetAudit(AutoLoginTest):
|
|||||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||||
voltage_rating=40, num_pins=13)
|
voltage_rating=40, num_pins=13)
|
||||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
||||||
category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
|
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
|
||||||
category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
|
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
|
||||||
date_acquired=datetime.date(2020, 2, 1))
|
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||||
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
|
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
|
||||||
category=self.category,
|
category=self.category,
|
||||||
date_acquired=datetime.date(2020, 2, 1))
|
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||||
self.wait = WebDriverWait(self.driver, 20)
|
self.wait = WebDriverWait(self.driver, 20)
|
||||||
|
|
||||||
@@ -316,25 +343,6 @@ class TestAssetAudit(AutoLoginTest):
|
|||||||
self.driver.implicitly_wait(4)
|
self.driver.implicitly_wait(4)
|
||||||
self.assertIn("This field is required.", self.page.modal.errors["Description"])
|
self.assertIn("This field is required.", self.page.modal.errors["Description"])
|
||||||
|
|
||||||
def test_audit_success(self):
|
|
||||||
self.page.set_query(self.asset.asset_id)
|
|
||||||
self.page.search()
|
|
||||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
|
||||||
# Now do it properly
|
|
||||||
self.page.modal.description = new_desc = "A BIG hammer"
|
|
||||||
self.page.modal.submit()
|
|
||||||
self.driver.implicitly_wait(4)
|
|
||||||
self.wait.until(animation_is_finished())
|
|
||||||
submit_time = timezone.now()
|
|
||||||
# Check data is correct
|
|
||||||
self.asset.refresh_from_db()
|
|
||||||
self.assertEqual(self.asset.description, new_desc)
|
|
||||||
# Make sure audit 'log' was filled out
|
|
||||||
self.assertEqual(self.profile.initials, self.asset.last_audited_by.initials)
|
|
||||||
assert_times_almost_equal(submit_time, self.asset.last_audited_at)
|
|
||||||
# Check we've removed it from the 'needing audit' list
|
|
||||||
self.assertNotIn(self.asset.asset_id, self.page.assets)
|
|
||||||
|
|
||||||
def test_audit_list(self):
|
def test_audit_list(self):
|
||||||
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
|
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
|
||||||
asset_row = self.page.assets[0]
|
asset_row = self.page.assets[0]
|
||||||
@@ -342,7 +350,7 @@ class TestAssetAudit(AutoLoginTest):
|
|||||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||||
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
||||||
self.page.modal.close()
|
self.page.modal.close()
|
||||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
self.assertFalse(self.driver.find_element(By.ID, 'modal').is_displayed())
|
||||||
# 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
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def test_oembed(client, test_asset):
|
|||||||
|
|
||||||
|
|
||||||
def test_asset_create(admin_client):
|
def test_asset_create(admin_client):
|
||||||
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'replacement_cost': '-30'})
|
||||||
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||||
assert_asset_form_errors(response)
|
assert_asset_form_errors(response)
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ def test_cable_create(admin_client):
|
|||||||
|
|
||||||
def test_asset_edit(admin_client, test_asset):
|
def test_asset_edit(admin_client, test_asset):
|
||||||
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
|
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': ""})
|
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'replacement_cost': '-50', 'description': "", 'status': "", 'category': ""})
|
||||||
assert_asset_form_errors(response)
|
assert_asset_form_errors(response)
|
||||||
|
|
||||||
|
|
||||||
@@ -127,4 +127,4 @@ def assert_asset_form_errors(response):
|
|||||||
assertFormError(response, 'form', 'category', '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', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||||
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||||
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
assertFormError(response, 'form', 'replacement_cost', 'A price cannot be negative')
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ urlpatterns = [
|
|||||||
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
|
path('asset/id/<asset: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/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
|
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
|
||||||
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
|
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
|
||||||
|
|
||||||
|
path('cables/list/', login_required(views.CableList.as_view()), name='cable_list'),
|
||||||
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'),
|
||||||
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
|
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from io import BytesIO
|
|||||||
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
|
||||||
from django.db.models import Q
|
from django.db.models import Q, Sum
|
||||||
from django.http import Http404, HttpResponse, JsonResponse
|
from django.http import Http404, HttpResponse, JsonResponse
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -17,7 +17,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
|
||||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||||
from barcode import Code39
|
from barcode import Code39
|
||||||
from barcode.writer import ImageWriter
|
from barcode.writer import ImageWriter
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
@@ -25,14 +25,12 @@ from z3c.rml import rml2pdf
|
|||||||
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
|
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
|
||||||
is_ajax, OEmbedView
|
is_ajax, OEmbedView
|
||||||
from assets import forms, models
|
from assets import forms, models
|
||||||
from assets.models import get_available_asset_id
|
|
||||||
|
|
||||||
|
|
||||||
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'
|
||||||
paginate_by = 40
|
paginate_by = 40
|
||||||
ordering = ['-pk']
|
|
||||||
hide_hidden_status = True
|
hide_hidden_status = True
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
@@ -50,13 +48,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
|||||||
|
|
||||||
# TODO Feedback to user when search fails
|
# TODO Feedback to user when search fails
|
||||||
query_string = form.cleaned_data['q'] or ""
|
query_string = form.cleaned_data['q'] or ""
|
||||||
if len(query_string) == 0:
|
queryset = models.Asset.objects.search(query=query_string)
|
||||||
queryset = self.model.objects.all()
|
|
||||||
elif len(query_string) >= 3:
|
|
||||||
queryset = self.model.objects.filter(
|
|
||||||
Q(asset_id__exact=query_string.upper()) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
|
|
||||||
else:
|
|
||||||
queryset = self.model.objects.filter(Q(asset_id__exact=query_string.upper()))
|
|
||||||
|
|
||||||
if form.cleaned_data['is_cable']:
|
if form.cleaned_data['is_cable']:
|
||||||
queryset = queryset.filter(is_cable=True)
|
queryset = queryset.filter(is_cable=True)
|
||||||
@@ -87,6 +79,25 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CableList(AssetList):
|
||||||
|
template_name = 'cable_list.html'
|
||||||
|
paginator = None
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset().filter(is_cable=True)
|
||||||
|
|
||||||
|
if self.form.cleaned_data['cable_type']:
|
||||||
|
queryset = queryset.filter(cable_type__in=self.form.cleaned_data['cable_type'])
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["page_title"] = "Cable List"
|
||||||
|
context["total_length"] = self.get_queryset().aggregate(Sum('length'))['length__sum']
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class AssetIDUrlMixin:
|
class AssetIDUrlMixin:
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
pk = self.kwargs.get(self.pk_url_kwarg)
|
pk = self.kwargs.get(self.pk_url_kwarg)
|
||||||
@@ -140,7 +151,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"] = get_available_asset_id()
|
initial["asset_id"] = models.get_available_asset_id()
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -155,6 +166,11 @@ class DuplicateMixin:
|
|||||||
|
|
||||||
|
|
||||||
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
|
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
|
||||||
|
def get_initial(self, *args, **kwargs):
|
||||||
|
initial = super().get_initial(*args, **kwargs)
|
||||||
|
initial["asset_id"] = models.get_available_asset_id()
|
||||||
|
return initial
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["create"] = None
|
context["create"] = None
|
||||||
@@ -176,6 +192,7 @@ class AssetOEmbed(OEmbedView):
|
|||||||
|
|
||||||
class AssetAuditList(AssetList):
|
class AssetAuditList(AssetList):
|
||||||
template_name = 'asset_audit_list.html'
|
template_name = 'asset_audit_list.html'
|
||||||
|
hide_hidden_status = True
|
||||||
|
|
||||||
# TODO Refresh this when the modal is submitted
|
# TODO Refresh this when the modal is submitted
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -262,7 +279,7 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin):
|
|||||||
form_class = forms.SupplierForm
|
form_class = forms.SupplierForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(SupplierUpdate, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
if is_ajax(self.request):
|
if is_ajax(self.request):
|
||||||
context['override'] = "base_ajax.html"
|
context['override'] = "base_ajax.html"
|
||||||
else:
|
else:
|
||||||
@@ -306,7 +323,6 @@ class CableTypeCreate(generic.CreateView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["create"] = True
|
context["create"] = True
|
||||||
context["page_title"] = "Create Cable Type"
|
context["page_title"] = "Create Cable Type"
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -322,7 +338,6 @@ class CableTypeUpdate(generic.UpdateView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["edit"] = True
|
context["edit"] = True
|
||||||
context["page_title"] = f"Edit Cable Type {self.object}"
|
context["page_title"] = f"Edit Cable Type {self.object}"
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -333,7 +348,9 @@ def generate_label(pk):
|
|||||||
black = (0, 0, 0)
|
black = (0, 0, 0)
|
||||||
white = (255, 255, 255)
|
white = (255, 255, 255)
|
||||||
size = (700, 200)
|
size = (700, 200)
|
||||||
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
|
font_size = 22
|
||||||
|
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", font_size)
|
||||||
|
heavy_font = ImageFont.truetype("static/fonts/OpenSans-Bold.tff", font_size + 13)
|
||||||
obj = get_object_or_404(models.Asset, asset_id=pk)
|
obj = get_object_or_404(models.Asset, asset_id=pk)
|
||||||
|
|
||||||
asset_id = f"Asset: {obj.asset_id}"
|
asset_id = f"Asset: {obj.asset_id}"
|
||||||
@@ -342,22 +359,25 @@ def generate_label(pk):
|
|||||||
csa = f"CSA: {obj.csa}mm²"
|
csa = f"CSA: {obj.csa}mm²"
|
||||||
|
|
||||||
image = Image.new("RGB", size, white)
|
image = Image.new("RGB", size, white)
|
||||||
|
image = ImageOps.expand(image, border=(5, 5, 5, 5), fill=black)
|
||||||
logo = Image.open("static/imgs/square_logo.png")
|
logo = Image.open("static/imgs/square_logo.png")
|
||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
draw.text((210, 140), asset_id, fill=black, font=font)
|
draw.text((300, 0), asset_id, fill=black, font=heavy_font)
|
||||||
if obj.is_cable:
|
if obj.is_cable:
|
||||||
draw.text((210, 170), length, fill=black, font=font)
|
y = 140
|
||||||
draw.text((360, 170), csa, fill=black, font=font)
|
draw.text((210, y), length, fill=black, font=font)
|
||||||
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
|
if obj.csa:
|
||||||
|
draw.text((365, y), csa, fill=black, font=font)
|
||||||
|
draw.text((210, size[1] - font_size - 8), "TEC PA & Lighting (0115) 84 68720", fill=black, font=font)
|
||||||
|
|
||||||
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
|
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
|
||||||
|
|
||||||
logo_size = (200, 200)
|
logo_size = (200, 200)
|
||||||
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
|
image.paste(logo.resize(logo_size, Image.ANTIALIAS), box=(5, 5))
|
||||||
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
|
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
|
||||||
width, height = barcode_image.size
|
width, height = barcode_image.size
|
||||||
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
|
image.paste(barcode_image.crop((0, 0, width, 100)), (int(((size[0] + logo_size[0]) - width) / 2), 40))
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@@ -386,14 +406,16 @@ class GenerateLabels(generic.View):
|
|||||||
|
|
||||||
base64_encoded_result_bytes = base64.b64encode(img_bytes)
|
base64_encoded_result_bytes = base64.b64encode(img_bytes)
|
||||||
base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii')
|
base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii')
|
||||||
images.append(base64_encoded_result_str)
|
images.append((get_object_or_404(models.Asset, asset_id=asset_id), base64_encoded_result_str))
|
||||||
|
|
||||||
|
name = f"Asset Label Sheet generated at {timezone.now()}"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'images0': images[::4],
|
'images0': images[::3],
|
||||||
'images1': images[1::4],
|
'images1': images[1::3],
|
||||||
'images2': images[2::4],
|
'images2': images[2::3],
|
||||||
'images3': images[3::4],
|
# 'images3': images[3::4],
|
||||||
'filename': "Asset Label Sheet generated at {}".format(timezone.now())
|
'filename': name
|
||||||
}
|
}
|
||||||
merger = PdfFileMerger()
|
merger = PdfFileMerger()
|
||||||
|
|
||||||
@@ -405,6 +427,6 @@ class GenerateLabels(generic.View):
|
|||||||
merged = BytesIO()
|
merged = BytesIO()
|
||||||
merger.write(merged)
|
merger.write(merged)
|
||||||
|
|
||||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
response['Content-Disposition'] = f'filename="{name}"'
|
||||||
response.write(merged.getvalue())
|
response.write(merged.getvalue())
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ def admin_user(admin_user):
|
|||||||
admin_user.first_name = "Event"
|
admin_user.first_name = "Event"
|
||||||
admin_user.last_name = "Test"
|
admin_user.last_name = "Test"
|
||||||
admin_user.initials = "ETU"
|
admin_user.initials = "ETU"
|
||||||
|
admin_user.is_approved = True
|
||||||
|
admin_user.is_supervisor = True
|
||||||
admin_user.save()
|
admin_user.save()
|
||||||
return admin_user
|
return admin_user
|
||||||
|
|
||||||
|
|||||||
7521
package-lock.json
generated
7521
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
|||||||
"autocompleter": "^6.1.2",
|
"autocompleter": "^6.1.2",
|
||||||
"autoprefixer": "^10.4.0",
|
"autoprefixer": "^10.4.0",
|
||||||
"bootstrap": "^4.6.1",
|
"bootstrap": "^4.6.1",
|
||||||
"bootstrap-select": "^1.13.17",
|
"bootstrap-select": "^1.13.18",
|
||||||
"clipboard": "^2.0.8",
|
"clipboard": "^2.0.8",
|
||||||
"cssnano": "^5.0.13",
|
"cssnano": "^5.0.13",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
"html5sortable": "^0.13.3",
|
"html5sortable": "^0.13.3",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"konami": "^1.6.3",
|
"konami": "^1.6.3",
|
||||||
"moment": "^2.27.0",
|
"moment": "^2.29.4",
|
||||||
"node-sass": "^7.0.0",
|
"node-sass": "^7.0.3",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"uglify-js": "^3.14.5"
|
"uglify-js": "^3.14.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browser-sync": "^2.27.7"
|
"browser-sync": "^2.27.11"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gulp": "gulp",
|
"gulp": "gulp",
|
||||||
|
|||||||
@@ -47,14 +47,16 @@ function initPicker(obj) {
|
|||||||
//log: 3,
|
//log: 3,
|
||||||
preprocessData: function (data) {
|
preprocessData: function (data) {
|
||||||
var i, l = data.length, array = [];
|
var i, l = data.length, array = [];
|
||||||
array.push({
|
if (!obj.data('noclear')) {
|
||||||
text: clearSelectionLabel,
|
array.push({
|
||||||
value: '',
|
text: clearSelectionLabel,
|
||||||
data:{
|
value: '',
|
||||||
update_url: '',
|
data:{
|
||||||
subtext:''
|
update_url: '',
|
||||||
}
|
subtext:''
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (l) {
|
if (l) {
|
||||||
for(i = 0; i < l; i++){
|
for(i = 0; i < l; i++){
|
||||||
@@ -71,11 +73,13 @@ function initPicker(obj) {
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
//console.log(obj.data);
|
||||||
obj.prepend($("<option></option>")
|
if (!obj.data('noclear')) {
|
||||||
.attr("value",'')
|
obj.prepend($("<option></option>")
|
||||||
.text(clearSelectionLabel)
|
.attr("value",'')
|
||||||
.data('update_url','')); //Add "clear selection" option
|
.text(clearSelectionLabel)
|
||||||
|
.data('update_url','')); //Add "clear selection" option
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
||||||
|
|||||||
@@ -77,13 +77,13 @@
|
|||||||
border-collapse: separate !important;
|
border-collapse: separate !important;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
}
|
}
|
||||||
.table tr th {
|
#event_table tr th {
|
||||||
border-right: 0 !important;
|
border-right: 0 !important;
|
||||||
}
|
}
|
||||||
.table tr td {
|
#event_table tr td {
|
||||||
border-left: 0 !important;
|
border-left: 0 !important;
|
||||||
}
|
}
|
||||||
.table tr td:not(:last-child) {
|
#event_table tr td:not(:last-child) {
|
||||||
border-right: 0 !important;
|
border-right: 0 !important;
|
||||||
}
|
}
|
||||||
@each $color, $value in $theme-colors {
|
@each $color, $value in $theme-colors {
|
||||||
|
|||||||
@@ -33,6 +33,25 @@ $fa-font-path: '/static/fonts';
|
|||||||
@import "node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
@import "node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
@import "node_modules/@fortawesome/fontawesome-free/scss/solid";
|
@import "node_modules/@fortawesome/fontawesome-free/scss/solid";
|
||||||
|
|
||||||
|
html {
|
||||||
|
--brand: #3F58AA;
|
||||||
|
scrollbar-color: #3F58AA Canvas !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root { accent-color: var(--brand); }
|
||||||
|
:focus-visible { outline-color: var(--brand); }
|
||||||
|
::selection { background-color: var(--brand); }
|
||||||
|
::marker { color: var(--brand); }
|
||||||
|
|
||||||
|
:is(
|
||||||
|
::-webkit-calendar-picker-indicator,
|
||||||
|
::-webkit-clear-button,
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button
|
||||||
|
) {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and
|
@media screen and
|
||||||
(prefers-reduced-motion: reduce),
|
(prefers-reduced-motion: reduce),
|
||||||
(update: slow) {
|
(update: slow) {
|
||||||
@@ -175,7 +194,7 @@ svg {
|
|||||||
|
|
||||||
span.fas {
|
span.fas {
|
||||||
padding-left: 0.1em !important;
|
padding-left: 0.1em !important;
|
||||||
padding-right: 0.1em !important;
|
padding-right: 0.3em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.embedded {
|
html.embedded {
|
||||||
@@ -252,3 +271,13 @@ html.embedded {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card, .card-header, .btn, input, select, .CodeMirror, .editor-toolbar, .card-img-top {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-top-left-radius: 0 !important;
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
||||||
|
padding-right: 1rem !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
<p>The following objects will be merged. Please select the 'master' record which you would like to keep. Other records will have associated events moved to the 'master' copy, and then will be deleted.</p>
|
<p>The following objects will be merged. Please select the 'master' record which you would like to keep. This may take some time.</p>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
{% for form in forms %}
|
{% for form in forms %}
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<th>{{ field.label }}</th>
|
<th>{{ field.label }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="radio" name="master" value="{{form.instance.pk|unlocalize}}"></td>
|
<td><input type="radio" name="master" value="{{form.instance.pk|unlocalize}}"></td>
|
||||||
<td>{{form.instance.pk}}</td>
|
<td>{{form.instance.pk}}</td>
|
||||||
@@ -37,4 +37,4 @@
|
|||||||
<input type="submit" value="Merge them" />
|
<input type="submit" value="Merge them" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
<body>
|
<body>
|
||||||
<a class="skip-link" href='#main'>Skip to content</a>
|
<a class="skip-link" href='#main'>Skip to content</a>
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
|
{% if request.user.current_event %}
|
||||||
|
<div class="bg-primary d-flex justify-content-between align-items-center"><span class="ml-2">You are currently checked in to <a href="{{request.user.current_event.event.get_absolute_url}}" class="text-white">{{request.user.current_event.event}}</a></span><a href="{% url 'event_checkout'%}" class="btn btn-warning">Check Out</a></div>
|
||||||
|
{% endif %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
|
<a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
{% load nice_errors from filters %}
|
{% load nice_errors from filters %}
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<div class="alert alert-danger alert-dismissable">
|
<div class="alert alert-danger mb-0">
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
|
||||||
<dl>
|
<dl>
|
||||||
{% with form|nice_errors as qq %}
|
{% with form|nice_errors as qq %}
|
||||||
{% for error_name,desc in qq.items %}
|
{% for error_name,desc in qq.items %}
|
||||||
<span class="row">
|
<span class="row">
|
||||||
<dt class="col-4">{{error_name}}</dt>
|
<dt class="col-3">{{error_name}}</dt>
|
||||||
<dd class="col-8">{{desc}}</dd>
|
<dd class="col-9">{{desc}}</dd>
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user