Compare commits
52 Commits
subhire
...
f3409d0680
| Author | SHA1 | Date | |
|---|---|---|---|
| f3409d0680 | |||
| 1a30a418b1 | |||
| 3aeafde96e | |||
| f4a163f63c | |||
| e14e250896 | |||
| 59a9fd5bb4 | |||
| 925498be02 | |||
| 6c9e360927 | |||
| c02e2e6bbf | |||
| be5aa892f0 | |||
| 23ac9fb62a | |||
| 7f05468483 | |||
| 5a36e33bf0 | |||
| b3ceed777e | |||
| a7119599ca | |||
| 2396e27943 | |||
| 8204fdae1f | |||
| 0b0043f6f7 | |||
| 5a8011a8e3 | |||
| 7062ccd5f8 | |||
| 9b525759f4 | |||
| a9b034255e | |||
| 6676183443 | |||
| 3f93cebf41 | |||
| 603e919ad0 | |||
| a0b70a3cac | |||
| a11e32252f | |||
| e48e016cb9 | |||
| ef1d9868da | |||
| 788fb3efe6 | |||
| 4f912932ca | |||
| 0598612c15 | |||
| 656f9fdd25 | |||
| ccda38918c | |||
| a1edf80dd0 | |||
| 83fe526cbd | |||
| 1d63bd940d | |||
| c090163f40 | |||
| baa3b2c9c6 | |||
| 462a16ec42 | |||
| 6cb3d1855a | |||
| 9279131edf | |||
| 3853ad0871 | |||
| 7eea868575 | |||
| fc6e66c7f5 | |||
| 01ed05ecd9 | |||
| 20e5d25130 | |||
| 11db880ac3 | |||
| 87caab6c8e | |||
| d79366d2e6 | |||
| 10f2152d8b | |||
| 0154ecb6d8 |
@@ -1,5 +1,3 @@
|
|||||||
[run]
|
[run]
|
||||||
omit = */migrations/*
|
plugins = django_coverage_plugin
|
||||||
*/tests/*
|
omit = */migrations/*, */tests/*
|
||||||
*/site-packages/*
|
|
||||||
*/distutils/*
|
|
||||||
|
|||||||
151
.github/workflows/combine-prs.yml
vendored
@@ -1,151 +0,0 @@
|
|||||||
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
@@ -1,14 +0,0 @@
|
|||||||
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"
|
|
||||||
36
.github/workflows/django.yml
vendored
@@ -13,33 +13,37 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
cache: 'pipenv'
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
python3 -m pip install --upgrade pip pipenv
|
|
||||||
pipenv install -d
|
|
||||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
|
||||||
- name: Cache Static Files
|
- name: Cache Static Files
|
||||||
id: static-cache
|
id: static-cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: 'pipeline/built_assets'
|
path: 'static/'
|
||||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||||
- uses: bahmutov/npm-install@v1
|
- uses: bahmutov/npm-install@v1
|
||||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
if: steps.static-cache.outputs.cache-hit != 'true'
|
||||||
- run: node node_modules/gulp/bin/gulp build
|
- run: node node_modules/gulp/bin/gulp build
|
||||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
if: steps.static-cache.outputs.cache-hit != 'true'
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Cache python deps
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.pythonLocation }}
|
||||||
|
key: ${{ env.pythonLocation }}-${{ hashFiles('Pipfile.lock') }}
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pipenv
|
||||||
|
pipenv install -d
|
||||||
- name: Basic Checks
|
- name: Basic Checks
|
||||||
run: |
|
run: |
|
||||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
pipenv run pycodestyle . --exclude=migrations,node_modules
|
||||||
pipenv run python3 manage.py check
|
pipenv run python manage.py check
|
||||||
pipenv run python3 manage.py makemigrations --check --dry-run
|
pipenv run python manage.py makemigrations --check --dry-run
|
||||||
pipenv run python3 manage.py collectstatic --noinput
|
pipenv run python 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
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -26,7 +26,6 @@ var/
|
|||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
node_modules/
|
node_modules/
|
||||||
data/
|
|
||||||
|
|
||||||
# Continer extras
|
# Continer extras
|
||||||
.vagrant
|
.vagrant
|
||||||
|
|||||||
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.6
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ADD . /app
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt && \
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
46
Pipfile
@@ -11,6 +11,7 @@ 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"
|
||||||
@@ -18,47 +19,53 @@ cssselect = "~=1.1.0"
|
|||||||
cssutils = "~=1.0.2"
|
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.1.5"
|
||||||
django-debug-toolbar = "~=3.2"
|
django-debug-toolbar = "~=3.2"
|
||||||
django-filter = "~=2.4.0"
|
django-filter = "~=2.4.0"
|
||||||
django-ical = "~=1.8.3"
|
django-ical = "~=1.7.1"
|
||||||
|
django-recaptcha = "~=2.0.6"
|
||||||
|
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 = "*"
|
envparse = "~=0.2.0"
|
||||||
gunicorn = "~=20.0.4"
|
gunicorn = "~=20.0.4"
|
||||||
icalendar = "~=4.0.7"
|
icalendar = "~=4.0.7"
|
||||||
idna = "~=2.10"
|
idna = "~=2.10"
|
||||||
|
importlib-metadata = "~=3.4.0"
|
||||||
|
lxml = "~=4.6.2"
|
||||||
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.3.0"
|
Pillow = "~=8.1.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.27.5"
|
PyPDF2 = "~=1.26.0"
|
||||||
PyPOM = "~=2.2.4"
|
PyPOM = "~=2.2.0"
|
||||||
python-dateutil = "~=2.8.1"
|
python-dateutil = "~=2.8.1"
|
||||||
pytoml = "~=0.1.21"
|
pytoml = "~=0.1.21"
|
||||||
pytz = "~=2020.5"
|
pytz = "~=2020.5"
|
||||||
reportlab = "*"
|
reportlab = "~=3.5.59"
|
||||||
requests = "~=2.25.1"
|
requests = "~=2.25.1"
|
||||||
retrying = "~=1.3.3"
|
retrying = "~=1.3.3"
|
||||||
simplejson = "~=3.17.2"
|
simplejson = "~=3.17.2"
|
||||||
six = "~=1.15.0"
|
six = "~=1.15.0"
|
||||||
soupsieve = "~=2.1"
|
soupsieve = "~=2.1"
|
||||||
sqlparse = "~=0.4.2"
|
sqlparse = "~=0.4.1"
|
||||||
static3 = "~=0.7.0"
|
static3 = "~=0.7.0"
|
||||||
svg2rlg = "~=0.3"
|
svg2rlg = "~=0.3"
|
||||||
tini = "~=3.0.1"
|
tini = "~=3.0.1"
|
||||||
tornado = "~=6.1"
|
tornado = "~=6.1"
|
||||||
urllib3 = "~=1.26.5"
|
urllib3 = "~=1.26.2"
|
||||||
whitenoise = "~=5.2.0"
|
whitenoise = "~=5.2.0"
|
||||||
yolk = "~=0.4.3"
|
yolk = "~=0.4.3"
|
||||||
|
"z3c.rml" = "~=4.1.2"
|
||||||
zipp = "~=3.4.0"
|
zipp = "~=3.4.0"
|
||||||
"zope.component" = "~=4.6.2"
|
"zope.component" = "~=4.6.2"
|
||||||
"zope.deferredimport" = "~=4.3.1"
|
"zope.deferredimport" = "~=4.3.1"
|
||||||
@@ -70,30 +77,19 @@ zipp = "~=3.4.0"
|
|||||||
"zope.schema" = "~=6.0.1"
|
"zope.schema" = "~=6.0.1"
|
||||||
sentry-sdk = "*"
|
sentry-sdk = "*"
|
||||||
diff-match-patch = "*"
|
diff-match-patch = "*"
|
||||||
python-barcode = "*"
|
|
||||||
django-hCaptcha = "*"
|
|
||||||
importlib-metadata = "*"
|
|
||||||
django-hcaptcha = "*"
|
|
||||||
"z3c.rml" = "*"
|
|
||||||
django-queryable-properties = "*"
|
|
||||||
django-mass-edit = "*"
|
|
||||||
selenium = "~=3.141.0"
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pycodestyle = "~=2.9.1"
|
selenium = "~=3.141.0"
|
||||||
|
pycodestyle = "*"
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
django-coverage-plugin = "*"
|
django-coverage-plugin = "*"
|
||||||
pytest-cov = "*"
|
pytest-cov = "*"
|
||||||
pytest-django = "*"
|
pytest-django = "*"
|
||||||
|
pytest-xdist = {extras = ["psutil"], version = "==2.2.0"}
|
||||||
pluggy = "*"
|
pluggy = "*"
|
||||||
pytest-splinter = "*"
|
pytest-splinter = "*"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-reverse = "*"
|
pypom = {extras = ["splinter"], version = "*"}
|
||||||
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
|
||||||
PyPOM = {extras = [ "splinter",], version = "*"}
|
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.10"
|
python_version = "3.9"
|
||||||
|
|
||||||
[pipenv]
|
|
||||||
allow_prereleases = true
|
|
||||||
|
|||||||
1166
Pipfile.lock
generated
@@ -9,8 +9,9 @@ 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'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
|
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
|
||||||
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
|
reverse(oembed_view, kwargs=kwargs))
|
||||||
|
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||||
resp = render(request, 'login_redirect.html', context=context)
|
resp = render(request, 'login_redirect.html', context=context)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@@ -24,7 +25,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(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
|
return HttpResponseRedirect('%s?%s=%s' % (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__
|
||||||
@@ -54,7 +55,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(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
|
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||||
else:
|
else:
|
||||||
resp = render(request, '403.html')
|
resp = render(request, '403.html')
|
||||||
resp.status_code = 403
|
resp.status_code = 403
|
||||||
|
|||||||
@@ -9,25 +9,29 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
from envparse import env
|
from envparse import env
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
|
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
|
||||||
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = env('DEBUG', cast=bool, default=True)
|
DEBUG = env('DEBUG', cast=bool, default=True)
|
||||||
STAGING = env('STAGING', cast=bool, default=False)
|
STAGING = env('STAGING', cast=bool, default=False)
|
||||||
CI = env('CI', cast=bool, default=False)
|
CI = env('CI', cast=bool, default=False)
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||||
CSRF_TRUSTED_ORIGINS = []
|
|
||||||
|
|
||||||
if STAGING:
|
if STAGING:
|
||||||
ALLOWED_HOSTS.append('.herokuapp.com')
|
ALLOWED_HOSTS.append('.herokuapp.com')
|
||||||
@@ -36,7 +40,6 @@ if DEBUG:
|
|||||||
ALLOWED_HOSTS.append('localhost')
|
ALLOWED_HOSTS.append('localhost')
|
||||||
ALLOWED_HOSTS.append('example.com')
|
ALLOWED_HOSTS.append('example.com')
|
||||||
ALLOWED_HOSTS.append('127.0.0.1')
|
ALLOWED_HOSTS.append('127.0.0.1')
|
||||||
CSRF_TRUSTED_ORIGINS.append('.preview.app.github.dev')
|
|
||||||
|
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
@@ -44,9 +47,8 @@ if not DEBUG:
|
|||||||
|
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
||||||
DOMAIN = env('DOMAIN', default='example.com')
|
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
||||||
|
('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'))
|
||||||
|
|
||||||
@@ -64,14 +66,12 @@ INSTALLED_APPS = (
|
|||||||
'users',
|
'users',
|
||||||
'RIGS',
|
'RIGS',
|
||||||
'assets',
|
'assets',
|
||||||
'training',
|
|
||||||
|
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
'registration',
|
'registration',
|
||||||
'reversion',
|
'reversion',
|
||||||
|
'captcha',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'hcaptcha',
|
|
||||||
'massadmin',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
@@ -97,7 +97,7 @@ WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': str(BASE_DIR / 'db.sqlite3'),
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,9 +191,12 @@ LOGOUT_URL = '/user/logout/'
|
|||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
# CAPTCHA settings
|
# reCAPTCHA settings
|
||||||
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
|
RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
|
||||||
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
|
RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
|
||||||
|
NOCAPTCHA = True
|
||||||
|
|
||||||
|
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAILER_TEST = False
|
EMAILER_TEST = False
|
||||||
@@ -232,16 +235,19 @@ DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = str(BASE_DIR / 'static/')
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||||
|
STATIC_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, 'static/'),
|
||||||
|
]
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
str(BASE_DIR / 'pipeline/built_assets'),
|
os.path.join(BASE_DIR, 'pipeline/built_assets'),
|
||||||
]
|
]
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [
|
'DIRS': [
|
||||||
BASE_DIR / 'templates'
|
os.path.join(BASE_DIR, 'templates')
|
||||||
],
|
],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
@@ -255,7 +261,7 @@ TEMPLATES = [
|
|||||||
"django.template.context_processors.request",
|
"django.template.context_processors.request",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
'debug': DEBUG
|
'debug': DEBUG or CI
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -264,5 +270,3 @@ USE_GRAVATAR = True
|
|||||||
|
|
||||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||||
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class BootstrapSelectElement(Region):
|
|||||||
return [self.BootstrapSelectOption(self, i) for i in options]
|
return [self.BootstrapSelectOption(self, i) for i in options]
|
||||||
|
|
||||||
def set_option(self, name, selected):
|
def set_option(self, name, selected):
|
||||||
options = [x for x in self.options if x.name == name]
|
options = list((x for x in self.options if x.name == name))
|
||||||
assert len(options) == 1
|
assert len(options) == 1
|
||||||
options[0].set_selected(selected)
|
options[0].set_selected(selected)
|
||||||
|
|
||||||
@@ -117,15 +117,6 @@ class TextBox(Region):
|
|||||||
self.root.send_keys(value)
|
self.root.send_keys(value)
|
||||||
|
|
||||||
|
|
||||||
class SimpleMDETextArea(Region):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
|
|
||||||
|
|
||||||
|
|
||||||
class CheckBox(Region):
|
class CheckBox(Region):
|
||||||
def toggle(self):
|
def toggle(self):
|
||||||
self.root.click()
|
self.root.click()
|
||||||
@@ -145,7 +136,7 @@ class RadioSelect(Region): # Currently only works for yes/no radio selects
|
|||||||
value = "0"
|
value = "0"
|
||||||
else:
|
else:
|
||||||
value = "1"
|
value = "1"
|
||||||
self.find_element(By.XPATH, f"//label[@for='{self.root.get_attribute('id')}_{value}']").click()
|
self.find_element(By.XPATH, "//label[@for='{}_{}']".format(self.root.get_attribute("id"), value)).click()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
|
|||||||
@@ -8,14 +8,18 @@ from pytest_django.asserts import assertRedirects, assertContains, assertNotCont
|
|||||||
from pytest_django.asserts import assertTemplateUsed, assertInHTML
|
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
|
||||||
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
|
||||||
|
import pytest
|
||||||
|
from django.core.management import call_command
|
||||||
from django.template.defaultfilters import striptags
|
from django.template.defaultfilters import striptags
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
|
|
||||||
from django.test import TestCase, TransactionTestCase
|
from RIGS.models import Event
|
||||||
|
from assets.models import Asset
|
||||||
|
from django.db import connection
|
||||||
|
from django.test import TestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +49,7 @@ def get_request_url(url):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
|
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
|
||||||
'deleteSampleData', 'generateSampleTrainingData', 'generate_sample_training_users'])
|
'deleteSampleData'])
|
||||||
def test_production_exception(command):
|
def test_production_exception(command):
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
with pytest.raises(CommandError, match=".*production"):
|
with pytest.raises(CommandError, match=".*production"):
|
||||||
@@ -63,84 +67,79 @@ class TestSampleDataGenerator(TestCase):
|
|||||||
assert Event.objects.all().count() == 0
|
assert Event.objects.all().count() == 0
|
||||||
|
|
||||||
|
|
||||||
@override_settings(DEBUG=True)
|
class TestSampleDataGenerator(TestCase):
|
||||||
@pytest.mark.skip(reason="broken")
|
@override_settings(DEBUG=True)
|
||||||
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
|
def setUp(self):
|
||||||
call_command('generateSampleData')
|
call_command('generateSampleData')
|
||||||
for url in find_urls_recursive(urls.urlpatterns):
|
|
||||||
request_url = get_request_url(url)
|
def test_unauthenticated(self): # Nothing should be available to the unauthenticated
|
||||||
if request_url and 'user' not in request_url: # User module is full of edge cases
|
for url in find_urls_recursive(urls.urlpatterns):
|
||||||
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
|
request_url = get_request_url(url)
|
||||||
assertContains(response, 'Login')
|
if request_url and 'user' not in request_url: # User module is full of edge cases
|
||||||
if 'application/json+oembed' in response.content.decode():
|
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||||
assertTemplateUsed(response, 'login_redirect.html')
|
assertContains(response, 'Login')
|
||||||
else:
|
if 'application/json+oembed' in response.content.decode():
|
||||||
if "embed" in str(url):
|
assertTemplateUsed(response, 'login_redirect.html')
|
||||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
|
||||||
else:
|
else:
|
||||||
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
if "embed" in str(url):
|
||||||
assertRedirects(response, expected_url)
|
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||||
call_command('deleteSampleData')
|
else:
|
||||||
|
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
||||||
|
assertRedirects(response, expected_url)
|
||||||
|
|
||||||
|
def test_page_titles(self):
|
||||||
|
assert self.client.login(username='superuser', password='superuser')
|
||||||
|
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
|
||||||
|
request_url = get_request_url(url)
|
||||||
|
response = self.client.get(request_url)
|
||||||
|
if hasattr(response, "context_data") and "page_title" in response.context_data:
|
||||||
|
expected_title = striptags(response.context_data["page_title"])
|
||||||
|
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title),
|
||||||
|
response.content.decode())
|
||||||
|
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
@override_settings(DEBUG=True)
|
def test_basic_access(self):
|
||||||
@pytest.mark.skip(reason="broken")
|
assert self.client.login(username="basic", password="basic")
|
||||||
def test_basic_access(client):
|
|
||||||
call_command('generateSampleData')
|
|
||||||
assert client.login(username="basic", password="basic")
|
|
||||||
|
|
||||||
url = reverse('asset_list')
|
url = reverse('asset_list')
|
||||||
response = client.get(url)
|
response = self.client.get(url)
|
||||||
# Check edit and duplicate buttons NOT shown in list
|
# Check edit and duplicate buttons NOT shown in list
|
||||||
assertNotContains(response, 'Edit')
|
assertNotContains(response, 'Edit')
|
||||||
assertNotContains(response,
|
assertNotContains(response,
|
||||||
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
|
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
|
||||||
|
|
||||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
||||||
response = client.get(url)
|
response = self.client.get(url)
|
||||||
assertNotContains(response, 'Purchase Details')
|
assertNotContains(response, 'Purchase Details')
|
||||||
assertNotContains(response, 'View Revision History')
|
assertNotContains(response, 'View Revision History')
|
||||||
|
|
||||||
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
|
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
|
||||||
for url_name in urlz:
|
for url_name in urlz:
|
||||||
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
|
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
|
||||||
response = client.get(request_url, follow=True)
|
response = self.client.get(request_url, follow=True)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
request_url = reverse('supplier_create')
|
||||||
|
response = self.client.get(request_url, follow=True)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
request_url = reverse('supplier_create')
|
request_url = reverse('supplier_update', kwargs={'pk': 1})
|
||||||
response = client.get(request_url, follow=True)
|
response = self.client.get(request_url, follow=True)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
request_url = reverse('supplier_update', kwargs={'pk': 1})
|
def test_keyholder_access(self):
|
||||||
response = client.get(request_url, follow=True)
|
assert self.client.login(username="keyholder", password="keyholder")
|
||||||
assert response.status_code == 403
|
|
||||||
client.logout()
|
|
||||||
call_command('deleteSampleData')
|
|
||||||
|
|
||||||
|
url = reverse('asset_list')
|
||||||
|
response = self.client.get(url)
|
||||||
|
# Check edit and duplicate buttons shown in list
|
||||||
|
assertContains(response, 'Edit')
|
||||||
|
assertContains(response, 'Duplicate')
|
||||||
|
|
||||||
@override_settings(DEBUG=True)
|
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
||||||
@pytest.mark.skip(reason="broken")
|
response = self.client.get(url)
|
||||||
def test_keyholder_access(client):
|
assertContains(response, 'Purchase Details')
|
||||||
call_command('generateSampleData')
|
assertContains(response, 'View Revision History')
|
||||||
assert client.login(username="keyholder", password="keyholder")
|
self.client.logout()
|
||||||
|
|
||||||
url = reverse('asset_list')
|
|
||||||
response = client.get(url)
|
|
||||||
# Check edit and duplicate buttons shown in list
|
|
||||||
assertContains(response, 'Edit')
|
|
||||||
assertContains(response, 'Duplicate')
|
|
||||||
|
|
||||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
|
||||||
response = client.get(url)
|
|
||||||
assertContains(response, 'Purchase Details')
|
|
||||||
assertContains(response, 'View Revision History')
|
|
||||||
client.logout()
|
|
||||||
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)
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ urlpatterns = [
|
|||||||
path('', include('versioning.urls')),
|
path('', include('versioning.urls')),
|
||||||
path('', include('RIGS.urls')),
|
path('', include('RIGS.urls')),
|
||||||
path('assets/', include('assets.urls')),
|
path('assets/', include('assets.urls')),
|
||||||
path('training/', include('training.urls')),
|
|
||||||
|
|
||||||
path('', login_required(views.Index.as_view()), name='index'),
|
path('', login_required(views.Index.as_view()), name='index'),
|
||||||
|
|
||||||
@@ -23,21 +22,18 @@ 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")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += staticfiles_urlpatterns()
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
urlpatterns += [
|
urlpatterns = [
|
||||||
path('__debug__/', include(debug_toolbar.urls)),
|
path('__debug__/', include(debug_toolbar.urls)),
|
||||||
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
|
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
|
||||||
]
|
] + urlpatterns
|
||||||
|
|||||||
192
PyRIGS/views.py
@@ -1,52 +1,32 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import operator
|
import operator
|
||||||
import re
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from itertools import chain
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
import simplejson
|
||||||
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
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from 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
|
||||||
from training import models as training_models
|
|
||||||
|
|
||||||
|
|
||||||
def is_ajax(request):
|
def is_ajax(request):
|
||||||
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
||||||
|
|
||||||
|
|
||||||
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(Index, self).get_context_data(**kwargs)
|
||||||
context['rig_count'] = models.Event.objects.rig_count()
|
context['rig_count'] = models.Event.objects.rig_count()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -58,9 +38,7 @@ class SecureAPIRequest(generic.View):
|
|||||||
'organisation': models.Organisation,
|
'organisation': models.Organisation,
|
||||||
'profile': models.Profile,
|
'profile': models.Profile,
|
||||||
'event': models.Event,
|
'event': models.Event,
|
||||||
'asset': asset_models.Asset,
|
'supplier': asset_models.Supplier
|
||||||
'supplier': asset_models.Supplier,
|
|
||||||
'training_item': training_models.TrainingItem,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
perms = {
|
perms = {
|
||||||
@@ -69,9 +47,7 @@ class SecureAPIRequest(generic.View):
|
|||||||
'organisation': 'RIGS.view_organisation',
|
'organisation': 'RIGS.view_organisation',
|
||||||
'profile': 'RIGS.view_profile',
|
'profile': 'RIGS.view_profile',
|
||||||
'event': None,
|
'event': None,
|
||||||
'asset': None,
|
'supplier': None
|
||||||
'supplier': None,
|
|
||||||
'training_item': None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -99,9 +75,6 @@ class SecureAPIRequest(generic.View):
|
|||||||
fields = request.GET.get('fields', None)
|
fields = request.GET.get('fields', None)
|
||||||
if fields:
|
if fields:
|
||||||
fields = fields.split(",")
|
fields = fields.split(",")
|
||||||
filters = request.GET.get('filters', [])
|
|
||||||
if filters:
|
|
||||||
filters = filters.split(",")
|
|
||||||
|
|
||||||
# Supply data for one record
|
# Supply data for one record
|
||||||
if pk:
|
if pk:
|
||||||
@@ -122,13 +95,8 @@ class SecureAPIRequest(generic.View):
|
|||||||
for field in fields:
|
for field in fields:
|
||||||
q = Q(**{field + "__icontains": part})
|
q = Q(**{field + "__icontains": part})
|
||||||
qs.append(q)
|
qs.append(q)
|
||||||
|
|
||||||
queries.append(reduce(operator.or_, qs))
|
queries.append(reduce(operator.or_, qs))
|
||||||
|
|
||||||
for f in filters:
|
|
||||||
q = Q(**{f: True})
|
|
||||||
queries.append(q)
|
|
||||||
|
|
||||||
# Build the data response list
|
# Build the data response list
|
||||||
results = []
|
results = []
|
||||||
query = reduce(operator.and_, queries)
|
query = reduce(operator.and_, queries)
|
||||||
@@ -140,13 +108,14 @@ 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(f"{model}_update", kwargs={'pk': o.pk})
|
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
pass
|
pass
|
||||||
results.append(data)
|
results.append(data)
|
||||||
|
|
||||||
# return a data response
|
# return a data response
|
||||||
return JsonResponse(results, safe=False)
|
json = simplejson.dumps(results)
|
||||||
|
return HttpResponse(json, content_type="application/json") # Always json
|
||||||
|
|
||||||
start = request.GET.get('start', None)
|
start = request.GET.get('start', None)
|
||||||
end = request.GET.get('end', None)
|
end = request.GET.get('end', None)
|
||||||
@@ -171,7 +140,8 @@ class SecureAPIRequest(generic.View):
|
|||||||
}
|
}
|
||||||
|
|
||||||
results.append(data)
|
results.append(data)
|
||||||
return JsonResponse(results, safe=False)
|
json = simplejson.dumps(results)
|
||||||
|
return HttpResponse(json, content_type="application/json") # Always json
|
||||||
|
|
||||||
return HttpResponse(model)
|
return HttpResponse(model)
|
||||||
|
|
||||||
@@ -195,14 +165,27 @@ class GenericListView(generic.ListView):
|
|||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(GenericListView, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = self.model.__name__ + "s"
|
context['page_title'] = self.model.__name__ + "s"
|
||||||
if is_ajax(self.request):
|
if is_ajax(self.request):
|
||||||
context['override'] = "base_ajax.html"
|
context['override'] = "base_ajax.html"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
|
q = 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 != "":
|
||||||
@@ -214,8 +197,8 @@ class GenericDetailView(generic.DetailView):
|
|||||||
template_name = "generic_detail.html"
|
template_name = "generic_detail.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(GenericDetailView, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = f"{self.model.__name__} | {self.object.name}"
|
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
|
||||||
if is_ajax(self.request):
|
if is_ajax(self.request):
|
||||||
context['override'] = "base_ajax.html"
|
context['override'] = "base_ajax.html"
|
||||||
return context
|
return context
|
||||||
@@ -225,8 +208,8 @@ class GenericUpdateView(generic.UpdateView):
|
|||||||
template_name = "generic_form.html"
|
template_name = "generic_form.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(GenericUpdateView, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = f"Edit {self.model.__name__}"
|
context['page_title'] = "Edit {}".format(self.model.__name__)
|
||||||
if is_ajax(self.request):
|
if is_ajax(self.request):
|
||||||
context['override'] = "base_ajax.html"
|
context['override'] = "base_ajax.html"
|
||||||
return context
|
return context
|
||||||
@@ -236,60 +219,13 @@ class GenericCreateView(generic.CreateView):
|
|||||||
template_name = "generic_form.html"
|
template_name = "generic_form.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(GenericCreateView, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = f"Create {self.model.__name__}"
|
context['page_title'] = "Create {}".format(self.model.__name__)
|
||||||
if is_ajax(self.request):
|
if is_ajax(self.request):
|
||||||
context['override'] = "base_ajax.html"
|
context['override'] = "base_ajax.html"
|
||||||
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'
|
||||||
|
|
||||||
@@ -309,72 +245,14 @@ class CloseModal(generic.TemplateView):
|
|||||||
class OEmbedView(generic.View):
|
class OEmbedView(generic.View):
|
||||||
def get(self, request, pk=None):
|
def get(self, request, pk=None):
|
||||||
embed_url = reverse(self.url_name, args=[pk])
|
embed_url = reverse(self.url_name, args=[pk])
|
||||||
full_url = f"{request.scheme}://{request.META['HTTP_HOST']}{embed_url}"
|
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'html': f'<iframe src="{full_url}" frameborder="0" width="100%" height="250"></iframe>',
|
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'type': 'rich',
|
'type': 'rich',
|
||||||
'height': '250'
|
'height': '250'
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(data)
|
json = simplejson.JSONEncoderForHTML().encode(data)
|
||||||
|
return HttpResponse(json, content_type="application/json")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'RIGS.apps.RIGSAppConfig'
|
||||||
|
|||||||
195
RIGS/admin.py
@@ -8,153 +8,30 @@ 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
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
from users import forms as user_forms
|
from users import forms as user_forms
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
admin.site.register(models.VatRate, VersionAdmin)
|
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)
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
def approve_user(modeladmin, request, queryset):
|
||||||
def merge_model_instances(primary_object, alias_objects):
|
queryset.update(is_approved=True)
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class AssociateAdmin(VersionAdmin):
|
approve_user.short_description = "Approve selected users"
|
||||||
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, AssociateAdmin):
|
class ProfileAdmin(UserAdmin):
|
||||||
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
|
# Don't know how to add 'is_approved' whilst preserving the default list...
|
||||||
list_display_links = ['username']
|
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
(_('Personal info'), {
|
(_('Personal info'), {
|
||||||
@@ -172,12 +49,62 @@ class ProfileAdmin(UserAdmin, AssociateAdmin):
|
|||||||
)
|
)
|
||||||
form = user_forms.ProfileChangeForm
|
form = user_forms.ProfileChangeForm
|
||||||
add_form = user_forms.ProfileCreationForm
|
add_form = user_forms.ProfileCreationForm
|
||||||
actions = ['approve_user', 'merge']
|
actions = [approve_user]
|
||||||
|
|
||||||
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
|
|
||||||
|
|
||||||
def approve_user(modeladmin, request, queryset):
|
class AssociateAdmin(VersionAdmin):
|
||||||
queryset.update(is_approved=True)
|
list_display = ('id', 'name', 'number_of_events')
|
||||||
|
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)
|
||||||
|
|||||||
@@ -24,17 +24,29 @@ class InvoiceIndex(generic.ListView):
|
|||||||
template_name = 'invoice_list.html'
|
template_name = 'invoice_list.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(InvoiceIndex, self).get_context_data(**kwargs)
|
||||||
total = 0
|
total = 0
|
||||||
for i in context['object_list']:
|
for i in context['object_list']:
|
||||||
total += i.balance
|
total += i.balance
|
||||||
event_count = len(list(context['object_list']))
|
context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total)
|
||||||
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
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.model.objects.outstanding_invoices()
|
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||||
|
sql = "SELECT * FROM " \
|
||||||
|
"(SELECT " \
|
||||||
|
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||||
|
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||||
|
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||||
|
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||||
|
"AS sub " \
|
||||||
|
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||||
|
"ORDER BY invoice_date"
|
||||||
|
|
||||||
|
query = self.model.objects.raw(sql)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
class InvoiceDetail(generic.DetailView):
|
class InvoiceDetail(generic.DetailView):
|
||||||
@@ -42,15 +54,8 @@ class InvoiceDetail(generic.DetailView):
|
|||||||
template_name = 'invoice_detail.html'
|
template_name = 'invoice_detail.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(InvoiceDetail, self).get_context_data(**kwargs)
|
||||||
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
|
context['page_title'] = "Invoice {} ({})".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
|
||||||
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})"
|
|
||||||
if self.object.void:
|
|
||||||
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
|
|
||||||
elif self.object.is_closed:
|
|
||||||
context['page_title'] += "<span class='badge badge-success float-right'>PAID</span>"
|
|
||||||
else:
|
|
||||||
context['page_title'] += "<span class='badge badge-info float-right'>OUTSTANDING</span>"
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -60,14 +65,11 @@ 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': filename
|
'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
rml = template.render(context)
|
rml = template.render(context)
|
||||||
@@ -77,7 +79,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'] = f'filename="{filename}"'
|
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
||||||
response.write(pdfData)
|
response.write(pdfData)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -122,13 +124,38 @@ class InvoiceArchive(generic.ListView):
|
|||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(InvoiceArchive, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = "Invoice Archive"
|
context['page_title'] = "Invoice Archive"
|
||||||
context['description'] = "This page displays all invoices: outstanding, paid, and void"
|
context['description'] = "This page displays all invoices: outstanding, paid, and void"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date')
|
q = self.request.GET.get('q', "")
|
||||||
|
|
||||||
|
filter = Q(event__name__icontains=q)
|
||||||
|
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(q)
|
||||||
|
filter = filter | Q(pk=val)
|
||||||
|
filter = filter | Q(event__pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if q[0] == "N":
|
||||||
|
val = int(q[1:])
|
||||||
|
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
||||||
|
elif q[0] == "#":
|
||||||
|
val = int(q[1:])
|
||||||
|
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
|
||||||
|
|
||||||
|
return object_list
|
||||||
|
|
||||||
|
|
||||||
class InvoiceWaiting(generic.ListView):
|
class InvoiceWaiting(generic.ListView):
|
||||||
@@ -142,11 +169,28 @@ 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'] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})"
|
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.model.objects.waiting_invoices()
|
return self.get_objects()
|
||||||
|
|
||||||
|
def get_objects(self):
|
||||||
|
# TODO find a way to select items
|
||||||
|
events = self.model.objects.filter(
|
||||||
|
(
|
||||||
|
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||||
|
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||||
|
) & Q(invoice__isnull=True) & # Has not already been invoiced
|
||||||
|
Q(is_rig=True) # Is a rig (not non-rig)
|
||||||
|
|
||||||
|
).order_by('start_date') \
|
||||||
|
.select_related('person',
|
||||||
|
'organisation',
|
||||||
|
'venue', 'mic') \
|
||||||
|
.prefetch_related('items')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
class InvoiceEvent(generic.View):
|
class InvoiceEvent(generic.View):
|
||||||
@@ -176,7 +220,7 @@ class PaymentCreate(generic.CreateView):
|
|||||||
template_name = 'payment_form.html'
|
template_name = 'payment_form.html'
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super().get_initial()
|
initial = super(generic.CreateView, self).get_initial()
|
||||||
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
|
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
|
||||||
if invoicepk is None:
|
if invoicepk is None:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
@@ -8,7 +8,6 @@ from django.utils import timezone
|
|||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
from training.models import TrainingLevel
|
|
||||||
|
|
||||||
# Override the django form defaults to use the HTML date/time/datetime UI elements
|
# Override the django form defaults to use the HTML date/time/datetime UI elements
|
||||||
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
||||||
@@ -97,10 +96,10 @@ class EventForm(forms.ModelForm):
|
|||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
'You haven\'t provided any client contact details. Please add a person or organisation.',
|
'You haven\'t provided any client contact details. Please add a person or organisation.',
|
||||||
code='contact')
|
code='contact')
|
||||||
return super().clean()
|
return super(EventForm, self).clean()
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
m = super().save(commit=False)
|
m = super(EventForm, self).save(commit=False)
|
||||||
|
|
||||||
if (commit):
|
if (commit):
|
||||||
m.save()
|
m.save()
|
||||||
@@ -124,22 +123,6 @@ class EventForm(forms.ModelForm):
|
|||||||
'purchase_order', 'collector']
|
'purchase_order', 'collector']
|
||||||
|
|
||||||
|
|
||||||
class SubhireForm(forms.ModelForm):
|
|
||||||
related_models = {
|
|
||||||
'person': models.Person,
|
|
||||||
'organisation': models.Organisation,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
|
||||||
self.fields['end_date'].widget.format = '%Y-%m-%d'
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Subhire
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||||
tos = forms.BooleanField(required=True, label="Terms of hire")
|
tos = forms.BooleanField(required=True, label="Terms of hire")
|
||||||
name = forms.CharField(label="Your Name")
|
name = forms.CharField(label="Your Name")
|
||||||
@@ -155,7 +138,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
|
|||||||
|
|
||||||
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
|
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs)
|
||||||
self.fields['uni_id'].required = True
|
self.fields['uni_id'].required = True
|
||||||
self.fields['account_code'].required = True
|
self.fields['account_code'].required = True
|
||||||
|
|
||||||
@@ -169,12 +152,8 @@ 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(EventRiskAssessmentForm, self).__init__(*args, **kwargs)
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
if str(name) == 'supervisor_consulted':
|
if str(name) == 'supervisor_consulted':
|
||||||
field.widget = forms.CheckboxInput()
|
field.widget = forms.CheckboxInput()
|
||||||
@@ -185,16 +164,13 @@ class EventRiskAssessmentForm(forms.ModelForm):
|
|||||||
], attrs={'class': 'custom-control-input', 'required': 'true'})
|
], attrs={'class': 'custom-control-input', 'required': 'true'})
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.cleaned_data.get('big_power'):
|
|
||||||
if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
|
|
||||||
self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"))
|
|
||||||
# Check expected values
|
# Check expected values
|
||||||
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(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
|
unexpected_values.append("<li>{}</li>".format(self._meta.model._meta.get_field(field).help_text))
|
||||||
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(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
|
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')
|
||||||
return super(EventRiskAssessmentForm, self).clean()
|
return super(EventRiskAssessmentForm, self).clean()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -205,7 +181,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
|
|||||||
|
|
||||||
class EventChecklistForm(forms.ModelForm):
|
class EventChecklistForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super(EventChecklistForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['date'].widget.format = '%Y-%m-%d'
|
self.fields['date'].widget.format = '%Y-%m-%d'
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
if field.__class__ == forms.NullBooleanField:
|
if field.__class__ == forms.NullBooleanField:
|
||||||
@@ -233,7 +209,7 @@ class EventChecklistForm(forms.ModelForm):
|
|||||||
for key in vehicles:
|
for key in vehicles:
|
||||||
pk = int(key.split('_')[1])
|
pk = int(key.split('_')[1])
|
||||||
driver_key = 'driver_' + str(pk)
|
driver_key = 'driver_' + str(pk)
|
||||||
if (self.data[driver_key] == ''):
|
if(self.data[driver_key] == ''):
|
||||||
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@@ -255,9 +231,9 @@ class EventChecklistForm(forms.ModelForm):
|
|||||||
pk = int(key.split('_')[1])
|
pk = int(key.split('_')[1])
|
||||||
|
|
||||||
for field in other_fields:
|
for field in other_fields:
|
||||||
value = self.data[f'{field}_{pk}']
|
value = self.data['{}_{}'.format(field, pk)]
|
||||||
if value == '':
|
if value == '':
|
||||||
raise forms.ValidationError(f'Add a {field} to crewmember {pk}', code=f'{field}_mismatch')
|
raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item = models.EventChecklistCrew.objects.get(pk=pk)
|
item = models.EventChecklistCrew.objects.get(pk=pk)
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ 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
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentCreate(generic.CreateView):
|
class EventRiskAssessmentCreate(generic.CreateView):
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
template_name = 'hs/risk_assessment_form.html'
|
template_name = 'risk_assessment_form.html'
|
||||||
form_class = forms.EventRiskAssessmentForm
|
form_class = forms.EventRiskAssessmentForm
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
@@ -39,8 +37,7 @@ class EventRiskAssessmentCreate(generic.CreateView):
|
|||||||
epk = self.kwargs.get('pk')
|
epk = self.kwargs.get('pk')
|
||||||
event = models.Event.objects.get(pk=epk)
|
event = models.Event.objects.get(pk=epk)
|
||||||
context['event'] = event
|
context['event'] = event
|
||||||
context['page_title'] = f'Create Risk Assessment for Event {event.display_id}'
|
context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id)
|
||||||
get_related(context['form'], context)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -49,7 +46,7 @@ class EventRiskAssessmentCreate(generic.CreateView):
|
|||||||
|
|
||||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
class EventRiskAssessmentEdit(generic.UpdateView):
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
template_name = 'hs/risk_assessment_form.html'
|
template_name = 'risk_assessment_form.html'
|
||||||
form_class = forms.EventRiskAssessmentForm
|
form_class = forms.EventRiskAssessmentForm
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -65,28 +62,22 @@ 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'] = f'Edit Risk Assessment for Event {ra.event.display_id}'
|
context['page_title'] = 'Edit Risk Assessment for Event {}'.format(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 = 'hs/risk_assessment_detail.html'
|
template_name = 'risk_assessment_detail.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = 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
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentList(generic.ListView):
|
class EventRiskAssessmentList(generic.ListView):
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
template_name = 'hs/hs_object_list.html'
|
template_name = 'hs_object_list.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
return self.model.objects.order_by('reviewed_at').select_related('event')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
|
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
|
||||||
@@ -112,17 +103,17 @@ class EventRiskAssessmentReview(generic.View):
|
|||||||
|
|
||||||
class EventChecklistDetail(generic.DetailView):
|
class EventChecklistDetail(generic.DetailView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'hs/event_checklist_detail.html'
|
template_name = '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'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
context['page_title'] = "Event Checklist for Event {} {}".format(self.object.event.display_id, self.object.event.name)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistEdit(generic.UpdateView):
|
class EventChecklistEdit(generic.UpdateView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'hs/event_checklist_form.html'
|
template_name = 'event_checklist_form.html'
|
||||||
form_class = forms.EventChecklistForm
|
form_class = forms.EventChecklistForm
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -138,14 +129,19 @@ 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'] = f'Edit Event Checklist for Event {ec.event.display_id}'
|
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
|
||||||
get_related(context['form'], context)
|
form = context['form']
|
||||||
|
# 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(generic.CreateView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'hs/event_checklist_form.html'
|
template_name = '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!
|
||||||
@@ -157,7 +153,7 @@ 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, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
|
messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event))
|
||||||
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
|
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
|
||||||
|
|
||||||
return super(EventChecklistCreate, self).get(self)
|
return super(EventChecklistCreate, self).get(self)
|
||||||
@@ -174,7 +170,7 @@ class EventChecklistCreate(generic.CreateView):
|
|||||||
epk = self.kwargs.get('pk')
|
epk = self.kwargs.get('pk')
|
||||||
event = models.Event.objects.get(pk=epk)
|
event = models.Event.objects.get(pk=epk)
|
||||||
context['event'] = event
|
context['event'] = event
|
||||||
context['page_title'] = f'Create Event Checklist for Event {event.display_id}'
|
context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -184,10 +180,7 @@ class EventChecklistCreate(generic.CreateView):
|
|||||||
class EventChecklistList(generic.ListView):
|
class EventChecklistList(generic.ListView):
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'hs/hs_object_list.html'
|
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(EventChecklistList, self).get_context_data(**kwargs)
|
||||||
@@ -214,22 +207,12 @@ class EventChecklistReview(generic.View):
|
|||||||
class HSList(generic.ListView):
|
class HSList(generic.ListView):
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
model = models.Event
|
model = models.Event
|
||||||
template_name = 'hs/hs_list.html'
|
template_name = '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().order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(HSList, self).get_context_data(**kwargs)
|
context = super(HSList, self).get_context_data(**kwargs)
|
||||||
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
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.utils import timezone
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django_ical.views import ICalFeed
|
from django_ical.views import ICalFeed
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
|
|
||||||
class CalendarICS(ICalFeed):
|
class CalendarICS(ICalFeed):
|
||||||
"""
|
"""
|
||||||
@@ -33,7 +31,6 @@ class CalendarICS(ICalFeed):
|
|||||||
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
|
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
|
||||||
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
|
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
|
||||||
params['rig'] = request.GET.get('rig', 'true') == 'true'
|
params['rig'] = request.GET.get('rig', 'true') == 'true'
|
||||||
params['subhire'] = request.GET.get('subhire', 'true') == 'true'
|
|
||||||
|
|
||||||
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
|
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
|
||||||
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
|
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
|
||||||
@@ -43,46 +40,42 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
def description(self, params):
|
def description(self, params):
|
||||||
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
|
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
|
||||||
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire, ' if params['dry-hire'] else '') + ('Subhires' if params['subhire'] else '') + '\n'
|
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
||||||
desc += "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
||||||
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
||||||
|
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
def items(self, params):
|
def items(self, params):
|
||||||
# include events from up to 1 year ago
|
# include events from up to 1 year ago
|
||||||
start = timezone.now() - datetime.timedelta(days=365)
|
start = datetime.datetime.now() - datetime.timedelta(days=365)
|
||||||
filter = Q(start_date__gte=start)
|
filter = Q(start_date__gte=start)
|
||||||
|
|
||||||
type_filters = Q(pk=None) # Need something that is false for every entry
|
typeFilters = Q(pk=None) # Need something that is false for every entry
|
||||||
|
|
||||||
if params['dry-hire']:
|
if params['dry-hire']:
|
||||||
type_filters = type_filters | Q(dry_hire=True, is_rig=True)
|
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
|
||||||
|
|
||||||
if params['non-rig']:
|
if params['non-rig']:
|
||||||
type_filters = type_filters | Q(is_rig=False)
|
typeFilters = typeFilters | Q(is_rig=False)
|
||||||
|
|
||||||
if params['rig']:
|
if params['rig']:
|
||||||
type_filters = type_filters | Q(is_rig=True, dry_hire=False)
|
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
||||||
|
|
||||||
status_filters = Q(pk=None) # Need something that is false for every entry
|
statusFilters = Q(pk=None) # Need something that is false for every entry
|
||||||
|
|
||||||
if params['cancelled']:
|
if params['cancelled']:
|
||||||
status_filters = status_filters | Q(status=models.Event.CANCELLED)
|
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
||||||
if params['provisional']:
|
if params['provisional']:
|
||||||
status_filters = status_filters | Q(status=models.Event.PROVISIONAL)
|
statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
|
||||||
if params['confirmed']:
|
if params['confirmed']:
|
||||||
status_filters = status_filters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||||
|
|
||||||
filter = filter & type_filters & status_filters
|
filter = filter & typeFilters & statusFilters
|
||||||
|
|
||||||
events = models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||||
'venue', 'mic')
|
'venue', 'mic')
|
||||||
|
|
||||||
subhires = models.Subhire.objects.filter(status_filters).order_by('-start_date').select_related('person', 'organisation')
|
|
||||||
|
|
||||||
return list(chain(events, subhires))
|
|
||||||
|
|
||||||
def item_title(self, item):
|
def item_title(self, item):
|
||||||
title = ''
|
title = ''
|
||||||
|
|
||||||
@@ -100,7 +93,7 @@ class CalendarICS(ICalFeed):
|
|||||||
title += item.name
|
title += item.name
|
||||||
|
|
||||||
# Add the status
|
# Add the status
|
||||||
title += f' ({item.get_status_display()})'
|
title += ' (' + str(item.get_status_display()) + ')'
|
||||||
|
|
||||||
return title
|
return title
|
||||||
|
|
||||||
@@ -108,37 +101,36 @@ 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):
|
||||||
if hasattr(item, 'venue'):
|
return item.venue
|
||||||
return item.venue
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def item_description(self, item):
|
def item_description(self, item):
|
||||||
# Create a nice information-rich description
|
# Create a nice information-rich description
|
||||||
# note: only making use of information available to "non-keyholders"
|
# note: only making use of information available to "non-keyholders"
|
||||||
|
|
||||||
desc = f'Rig ID = {item.display_id}\n'
|
tz = pytz.timezone(self.timezone)
|
||||||
desc += f'Event = {item.name}\n'
|
|
||||||
if hasattr(item, 'venue'):
|
desc = 'Rig ID = ' + str(item.pk) + '\n'
|
||||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
desc += 'Event = ' + item.name + '\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 += f'Status = {item.get_status_display()}\n'
|
desc += 'Status = ' + str(item.get_status_display()) + '\n'
|
||||||
if hasattr(item, 'mic'):
|
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
|
||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if hasattr(item, 'meet_at') and item.meet_at:
|
if item.meet_at:
|
||||||
desc += 'Crew Meet = ' + (
|
desc += 'Crew Meet = ' + (
|
||||||
timezone.make_aware(item.meet_at).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||||
if hasattr(item, 'access_at') and item.access_at:
|
if item.access_at:
|
||||||
desc += 'Access At = ' + (
|
desc += 'Access At = ' + (
|
||||||
timezone.make_aware(item.access_at).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||||
if item.start_date:
|
if item.start_date:
|
||||||
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
|
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
|
||||||
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
||||||
@@ -148,16 +140,23 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.description:
|
if item.description:
|
||||||
desc += f'Event Description:\n{item.description}\n\n'
|
desc += 'Event Description:\n' + item.description + '\n\n'
|
||||||
|
# if item.notes: // Need to add proper keyholder checks before this gets put back
|
||||||
|
# desc += 'Notes:\n'+item.notes+'\n\n'
|
||||||
|
|
||||||
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
|
base_url = "https://rigs.nottinghamtec.co.uk"
|
||||||
|
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
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@ from django.core.management.base import BaseCommand, CommandError
|
|||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from assets import models
|
from assets import models
|
||||||
from RIGS import models as rigsmodels
|
from RIGS import models as rigsmodels
|
||||||
from training import models as tmodels
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -32,11 +31,6 @@ class Command(BaseCommand):
|
|||||||
self.delete_objects(rigsmodels.Payment)
|
self.delete_objects(rigsmodels.Payment)
|
||||||
self.delete_objects(rigsmodels.RiskAssessment)
|
self.delete_objects(rigsmodels.RiskAssessment)
|
||||||
self.delete_objects(rigsmodels.EventChecklist)
|
self.delete_objects(rigsmodels.EventChecklist)
|
||||||
self.delete_objects(tmodels.TrainingCategory)
|
|
||||||
self.delete_objects(tmodels.TrainingItem)
|
|
||||||
self.delete_objects(tmodels.TrainingLevel)
|
|
||||||
self.delete_objects(tmodels.TrainingItemQualification)
|
|
||||||
self.delete_objects(tmodels.TrainingLevelRequirement)
|
|
||||||
|
|
||||||
def delete_objects(self, model):
|
def delete_objects(self, model):
|
||||||
for obj in model.objects.all():
|
for obj in model.objects.all():
|
||||||
|
|||||||
@@ -12,4 +12,3 @@ class Command(BaseCommand):
|
|||||||
call_command('generateSampleUserData')
|
call_command('generateSampleUserData')
|
||||||
call_command('generateSampleRIGSData')
|
call_command('generateSampleRIGSData')
|
||||||
call_command('generateSampleAssetsData')
|
call_command('generateSampleAssetsData')
|
||||||
call_command('generateSampleTrainingData')
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class Command(BaseCommand):
|
|||||||
profiles = models.Profile.objects.all()
|
profiles = models.Profile.objects.all()
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
print("Generating rigboard data")
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
if not (settings.DEBUG or settings.STAGING):
|
if not (settings.DEBUG or settings.STAGING):
|
||||||
@@ -36,7 +35,6 @@ class Command(BaseCommand):
|
|||||||
self.setup_organisations()
|
self.setup_organisations()
|
||||||
self.setup_venues()
|
self.setup_venues()
|
||||||
self.setup_events()
|
self.setup_events()
|
||||||
print("Done generating rigboard data")
|
|
||||||
|
|
||||||
def setup_people(self):
|
def setup_people(self):
|
||||||
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
|
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
import versioning
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -26,6 +25,6 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
import versioning
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -22,6 +21,6 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
import versioning
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -42,7 +41,7 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='EventItem',
|
name='EventItem',
|
||||||
@@ -71,7 +70,7 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import RIGS.models
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import versioning
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -59,7 +58,7 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['event'],
|
'ordering': ['event'],
|
||||||
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
|
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
|
||||||
},
|
},
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='EventChecklistCrew',
|
name='EventChecklistCrew',
|
||||||
@@ -70,7 +69,7 @@ class Migration(migrations.Migration):
|
|||||||
('end', models.DateTimeField()),
|
('end', models.DateTimeField()),
|
||||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
|
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
|
||||||
],
|
],
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='EventChecklistVehicle',
|
name='EventChecklistVehicle',
|
||||||
@@ -79,7 +78,7 @@ class Migration(migrations.Migration):
|
|||||||
('vehicle', models.CharField(max_length=255)),
|
('vehicle', models.CharField(max_length=255)),
|
||||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
|
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
|
||||||
],
|
],
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='RiskAssessment',
|
name='RiskAssessment',
|
||||||
@@ -118,7 +117,7 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['event'],
|
'ordering': ['event'],
|
||||||
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
|
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
|
||||||
},
|
},
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='eventcrew',
|
model_name='eventcrew',
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
# Generated by Django 3.1.7 on 2021-03-02 11:48
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def postgres_migration_prep(apps, schema_editor):
|
|
||||||
model = apps.get_model("RIGS", "Event")
|
|
||||||
for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "EventAuthorisation")
|
|
||||||
for field in ["account_code", "uni_id"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "EventChecklist")
|
|
||||||
for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "EventItem")
|
|
||||||
for field in ["description"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "Organisation")
|
|
||||||
for field in ["address", "email", "notes", "phone"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "Payment")
|
|
||||||
for field in ["method"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "Person")
|
|
||||||
for field in ["address", "email", "notes", "phone"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "Profile")
|
|
||||||
for field in ["phone"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "RiskAssessment")
|
|
||||||
for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
model = apps.get_model("RIGS", "Venue")
|
|
||||||
for field in ["address", "email", "notes", "phone"]:
|
|
||||||
filter_param = {"{}__isnull".format(field): True}
|
|
||||||
update_param = {field: ""}
|
|
||||||
model.objects.filter(**filter_param).update(**update_param)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0039_auto_20210123_1910'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
|
|
||||||
]
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 3.2.11 on 2022-01-09 14:56
|
# Generated by Django 3.1.5 on 2021-02-06 10:43
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('RIGS', '0043_auto_20211027_1519'),
|
('RIGS', '0039_auto_20210123_1910'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='profile',
|
model_name='profile',
|
||||||
name='is_supervisor',
|
name='dark_theme',
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 3.1.7 on 2021-03-02 12:04
|
# Generated by Django 3.1.5 on 2021-02-08 16:03
|
||||||
|
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -7,27 +7,10 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('RIGS', '0040_auto_20210302_1148'),
|
('RIGS', '0040_profile_dark_theme'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='event',
|
|
||||||
name='meet_info',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='event',
|
|
||||||
name='payment_method',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='event',
|
|
||||||
name='payment_received',
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='dark_theme',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='auth_request_to',
|
name='auth_request_to',
|
||||||
@@ -43,11 +26,26 @@ class Migration(migrations.Migration):
|
|||||||
name='description',
|
name='description',
|
||||||
field=models.TextField(blank=True, default=''),
|
field=models.TextField(blank=True, default=''),
|
||||||
),
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='meet_info',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='notes',
|
name='notes',
|
||||||
field=models.TextField(blank=True, default=''),
|
field=models.TextField(blank=True, default=''),
|
||||||
),
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='payment_method',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='payment_received',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='purchase_order',
|
name='purchase_order',
|
||||||
@@ -146,7 +144,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='profile',
|
model_name='profile',
|
||||||
name='phone',
|
name='phone',
|
||||||
field=models.CharField(blank=True, default='', max_length=13),
|
field=models.CharField(default='', max_length=13, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='riskassessment',
|
model_name='riskassessment',
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Generated by Django 3.1.13 on 2021-10-07 22:38
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0041_auto_20210302_1204'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='fd_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w1_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w2_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w3_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.1.13 on 2021-10-27 14:19
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0042_auto_20211007_2338'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='initials',
|
|
||||||
field=models.CharField(max_length=5, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Generated by Django 3.2.16 on 2022-12-16 14:41
|
|
||||||
|
|
||||||
import RIGS.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import versioning.versioning
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0045_alter_profile_is_approved'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Subhire',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('description', models.TextField(blank=True, default='')),
|
|
||||||
('status', models.IntegerField(choices=[(0, 'Provisional'), (1, 'Confirmed'), (2, 'Booked'), (3, 'Cancelled')], default=0)),
|
|
||||||
('start_date', models.DateField()),
|
|
||||||
('start_time', models.TimeField(blank=True, null=True)),
|
|
||||||
('end_date', models.DateField(blank=True, null=True)),
|
|
||||||
('end_time', models.TimeField(blank=True, null=True)),
|
|
||||||
('purchase_order', models.CharField(blank=True, default='', max_length=255, verbose_name='PO')),
|
|
||||||
('insurance_value', models.DecimalField(decimal_places=2, max_digits=10)),
|
|
||||||
('quote', models.URLField(default='', validators=[RIGS.validators.validate_url])),
|
|
||||||
('events', models.ManyToManyField(to='RIGS.Event')),
|
|
||||||
('organisation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.organisation')),
|
|
||||||
('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.person')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'permissions': [('subhire_finance', 'Can see financial data for subhire - insurance values')],
|
|
||||||
},
|
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
843
RIGS/models.py
Normal file
@@ -0,0 +1,843 @@
|
|||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from collections import Counter
|
||||||
|
from decimal import Decimal
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
from reversion.models import Version
|
||||||
|
|
||||||
|
|
||||||
|
class Profile(AbstractUser):
|
||||||
|
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
||||||
|
phone = models.CharField(max_length=13, null=True, default='')
|
||||||
|
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||||
|
is_approved = models.BooleanField(default=False)
|
||||||
|
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||||
|
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||||
|
dark_theme = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_api_key(cls):
|
||||||
|
size = 20
|
||||||
|
chars = string.ascii_letters + string.digits
|
||||||
|
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
||||||
|
return new_api_key
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profile_picture(self):
|
||||||
|
url = ""
|
||||||
|
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
||||||
|
url = "https://www.gravatar.com/avatar/" + hashlib.md5(
|
||||||
|
self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
|
||||||
|
return url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
name = self.get_full_name()
|
||||||
|
if self.initials:
|
||||||
|
name += ' "{}"'.format(self.initials)
|
||||||
|
return name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def admins(cls):
|
||||||
|
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def users_awaiting_approval_count(cls):
|
||||||
|
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
# TODO move to versioning - currently get import errors with that
|
||||||
|
|
||||||
|
|
||||||
|
class RevisionMixin(object):
|
||||||
|
@property
|
||||||
|
def is_first_version(self):
|
||||||
|
versions = Version.objects.get_for_object(self)
|
||||||
|
return len(versions) == 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_version(self):
|
||||||
|
version = Version.objects.get_for_object(self).select_related('revision').first()
|
||||||
|
return version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_edited_at(self):
|
||||||
|
version = self.current_version
|
||||||
|
if version is None:
|
||||||
|
return None
|
||||||
|
return version.revision.date_created
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_edited_by(self):
|
||||||
|
version = self.current_version
|
||||||
|
if version is None:
|
||||||
|
return None
|
||||||
|
return version.revision.user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_version_id(self):
|
||||||
|
version = self.current_version
|
||||||
|
if version is None:
|
||||||
|
return None
|
||||||
|
return "V{0} | R{1}".format(version.pk, version.revision.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class Person(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes is not None:
|
||||||
|
if len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def organisations(self):
|
||||||
|
o = []
|
||||||
|
for e in Event.objects.filter(person=self).select_related('organisation'):
|
||||||
|
if e.organisation:
|
||||||
|
o.append(e.organisation)
|
||||||
|
|
||||||
|
# Count up occurances and put them in descending order
|
||||||
|
c = Counter(o)
|
||||||
|
stats = c.most_common()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('person_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class Organisation(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
union_account = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes is not None:
|
||||||
|
if len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def persons(self):
|
||||||
|
p = []
|
||||||
|
for e in Event.objects.filter(organisation=self).select_related('person'):
|
||||||
|
if e.person:
|
||||||
|
p.append(e.person)
|
||||||
|
|
||||||
|
# Count up occurances and put them in descending order
|
||||||
|
c = Counter(p)
|
||||||
|
stats = c.most_common()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class VatManager(models.Manager):
|
||||||
|
def current_rate(self):
|
||||||
|
return self.find_rate(timezone.now())
|
||||||
|
|
||||||
|
def find_rate(self, date):
|
||||||
|
try:
|
||||||
|
return self.filter(start_at__lte=date).latest()
|
||||||
|
except VatRate.DoesNotExist:
|
||||||
|
r = VatRate
|
||||||
|
r.rate = 0
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class VatRate(models.Model, RevisionMixin):
|
||||||
|
start_at = models.DateField()
|
||||||
|
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
||||||
|
comment = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
objects = VatManager()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_percent(self):
|
||||||
|
return self.rate * 100
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-start_at']
|
||||||
|
get_latest_by = 'start_at'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
|
||||||
|
|
||||||
|
|
||||||
|
class Venue(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
three_phase_available = models.BooleanField(default=False)
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes and len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('venue_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class EventManager(models.Manager):
|
||||||
|
def current_events(self):
|
||||||
|
events = self.filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Ends after
|
||||||
|
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Active dry hire
|
||||||
|
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
||||||
|
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||||
|
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def events_in_bounds(self, start, end):
|
||||||
|
events = self.filter(
|
||||||
|
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
|
||||||
|
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
|
||||||
|
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
|
||||||
|
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
|
||||||
|
|
||||||
|
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
|
||||||
|
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
|
||||||
|
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
|
||||||
|
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
|
||||||
|
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
|
||||||
|
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
||||||
|
'organisation',
|
||||||
|
'venue', 'mic')
|
||||||
|
return events
|
||||||
|
|
||||||
|
def rig_count(self):
|
||||||
|
event_count = self.filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
||||||
|
is_rig=True) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Ends after
|
||||||
|
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) # Active dry hire
|
||||||
|
).count()
|
||||||
|
return event_count
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register(follow=['items'])
|
||||||
|
class Event(models.Model, RevisionMixin):
|
||||||
|
# Done to make it much nicer on the database
|
||||||
|
PROVISIONAL = 0
|
||||||
|
CONFIRMED = 1
|
||||||
|
BOOKED = 2
|
||||||
|
CANCELLED = 3
|
||||||
|
EVENT_STATUS_CHOICES = (
|
||||||
|
(PROVISIONAL, 'Provisional'),
|
||||||
|
(CONFIRMED, 'Confirmed'),
|
||||||
|
(BOOKED, 'Booked'),
|
||||||
|
(CANCELLED, 'Cancelled'),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
|
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
|
description = models.TextField(blank=True, default='')
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||||
|
dry_hire = models.BooleanField(default=False)
|
||||||
|
is_rig = models.BooleanField(default=True)
|
||||||
|
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
start_date = models.DateField()
|
||||||
|
start_time = models.TimeField(blank=True, null=True)
|
||||||
|
end_date = models.DateField(blank=True, null=True)
|
||||||
|
end_time = models.TimeField(blank=True, null=True)
|
||||||
|
access_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
meet_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
meet_info = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
|
||||||
|
# Crew management
|
||||||
|
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
||||||
|
verbose_name="MIC", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# Monies
|
||||||
|
payment_method = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
payment_received = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||||
|
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||||
|
|
||||||
|
# Authorisation request details
|
||||||
|
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
auth_request_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
auth_request_to = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
if self.is_rig:
|
||||||
|
return str("N%05d" % self.pk)
|
||||||
|
else:
|
||||||
|
return self.pk
|
||||||
|
|
||||||
|
# Calculated values
|
||||||
|
"""
|
||||||
|
EX Vat
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sum_total(self):
|
||||||
|
total = self.items.aggregate(
|
||||||
|
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||||
|
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||||
|
)['sum_total']
|
||||||
|
if total:
|
||||||
|
return total
|
||||||
|
return Decimal("0.00")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def vat_rate(self):
|
||||||
|
return VatRate.objects.find_rate(self.start_date)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vat(self):
|
||||||
|
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Inc VAT
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancelled(self):
|
||||||
|
return (self.status == self.CANCELLED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def confirmed(self):
|
||||||
|
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hs_done(self):
|
||||||
|
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_start_time(self):
|
||||||
|
return self.start_time is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_end_time(self):
|
||||||
|
return self.end_time is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def earliest_time(self):
|
||||||
|
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
|
|
||||||
|
# Put all the datetimes in a list
|
||||||
|
datetime_list = []
|
||||||
|
|
||||||
|
if self.access_at:
|
||||||
|
datetime_list.append(self.access_at)
|
||||||
|
|
||||||
|
if self.meet_at:
|
||||||
|
datetime_list.append(self.meet_at)
|
||||||
|
|
||||||
|
# If there is no start time defined, pretend it's midnight
|
||||||
|
startTimeFaked = False
|
||||||
|
if self.has_start_time:
|
||||||
|
startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
|
||||||
|
else:
|
||||||
|
startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
|
||||||
|
startTimeFaked = True
|
||||||
|
|
||||||
|
# timezoneIssues - apply the default timezone to the naiive datetime
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
startDateTime = tz.localize(startDateTime)
|
||||||
|
datetime_list.append(startDateTime) # then add it to the list
|
||||||
|
|
||||||
|
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
||||||
|
|
||||||
|
# if we faked it & it's the earliest, better own up
|
||||||
|
if startTimeFaked and earliest == startDateTime:
|
||||||
|
return self.start_date
|
||||||
|
|
||||||
|
return earliest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_time(self):
|
||||||
|
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDate = self.end_date
|
||||||
|
if endDate is None:
|
||||||
|
endDate = self.start_date
|
||||||
|
|
||||||
|
if self.has_end_time:
|
||||||
|
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDateTime = tz.localize(endDateTime)
|
||||||
|
|
||||||
|
return endDateTime
|
||||||
|
|
||||||
|
else:
|
||||||
|
return endDate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def internal(self):
|
||||||
|
return bool(self.organisation and self.organisation.union_account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authorised(self):
|
||||||
|
if self.internal:
|
||||||
|
return self.authorisation.amount == self.total
|
||||||
|
else:
|
||||||
|
return bool(self.purchase_order)
|
||||||
|
|
||||||
|
objects = EventManager()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{}: {}".format(self.display_id, self.name)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
errdict = {}
|
||||||
|
if self.end_date and self.start_date > self.end_date:
|
||||||
|
errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
|
||||||
|
|
||||||
|
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||||
|
hasStartAndEnd = self.has_start_time and self.has_end_time
|
||||||
|
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
||||||
|
errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
|
||||||
|
|
||||||
|
if self.access_at is not None:
|
||||||
|
if self.access_at.date() > self.start_date:
|
||||||
|
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||||
|
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
|
||||||
|
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||||
|
|
||||||
|
if errdict != {}: # If there was an error when validation
|
||||||
|
raise ValidationError(errdict)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""Call :meth:`full_clean` before saving."""
|
||||||
|
self.full_clean()
|
||||||
|
super(Event, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventItem(models.Model, RevisionMixin):
|
||||||
|
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True, default='')
|
||||||
|
quantity = models.IntegerField()
|
||||||
|
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
order = models.IntegerField()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return self.cost * self.quantity
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['order']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str("item {}".format(self.name))
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventAuthorisation(models.Model, RevisionMixin):
|
||||||
|
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
||||||
|
email = models.EmailField()
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
||||||
|
account_code = models.CharField(max_length=50, default='', blank=True)
|
||||||
|
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||||
|
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register(follow=['payment_set'])
|
||||||
|
class Invoice(models.Model, RevisionMixin):
|
||||||
|
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||||
|
invoice_date = models.DateField(auto_now_add=True)
|
||||||
|
void = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
reversion_perm = 'RIGS.view_invoice'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sum_total(self):
|
||||||
|
return self.event.sum_total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return self.event.total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def payment_total(self):
|
||||||
|
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
|
||||||
|
if total:
|
||||||
|
return total
|
||||||
|
return Decimal("0.00")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self):
|
||||||
|
return self.sum_total - self.payment_total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
return self.balance == 0 or self.void
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return "#{} for Event {}".format(self.display_id, self.event.display_id)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
return "{:05d}".format(self.pk)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-invoice_date']
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class Payment(models.Model, RevisionMixin):
|
||||||
|
CASH = 'C'
|
||||||
|
INTERNAL = 'I'
|
||||||
|
EXTERNAL = 'E'
|
||||||
|
SUCORE = 'SU'
|
||||||
|
ADJUSTMENT = 'T'
|
||||||
|
METHODS = (
|
||||||
|
(CASH, 'Cash'),
|
||||||
|
(INTERNAL, 'Internal'),
|
||||||
|
(EXTERNAL, 'External'),
|
||||||
|
(SUCORE, 'SU Core'),
|
||||||
|
(ADJUSTMENT, 'TEC Adjustment'),
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
||||||
|
date = models.DateField()
|
||||||
|
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
||||||
|
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s: %d" % (self.get_method_display(), self.amount)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str("payment of £{}".format(self.amount))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_url(value):
|
||||||
|
if not value:
|
||||||
|
return # Required error is done the field
|
||||||
|
obj = urlparse(value)
|
||||||
|
if obj.hostname not in ('nottinghamtec.sharepoint.com'):
|
||||||
|
raise ValidationError('URL must point to a location on the TEC Sharepoint')
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class RiskAssessment(models.Model, RevisionMixin):
|
||||||
|
SMALL = (0, 'Small')
|
||||||
|
MEDIUM = (1, 'Medium')
|
||||||
|
LARGE = (2, 'Large')
|
||||||
|
SIZES = (SMALL, MEDIUM, LARGE)
|
||||||
|
|
||||||
|
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||||
|
# General
|
||||||
|
nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>"
|
||||||
|
"TEC's standard risk assessments and method statements?</a>")
|
||||||
|
nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>")
|
||||||
|
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
|
||||||
|
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
|
||||||
|
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
|
||||||
|
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||||
|
|
||||||
|
# Power
|
||||||
|
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
|
||||||
|
# If yes to the above two, you must answer...
|
||||||
|
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
|
||||||
|
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
|
||||||
|
outside = models.BooleanField(help_text="Is the event outdoors?")
|
||||||
|
generators = models.BooleanField(help_text="Will generators be used?")
|
||||||
|
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
|
||||||
|
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
|
||||||
|
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
|
||||||
|
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||||
|
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||||
|
|
||||||
|
# Sound
|
||||||
|
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
|
||||||
|
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||||
|
|
||||||
|
# Site
|
||||||
|
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
|
||||||
|
safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
|
||||||
|
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
|
||||||
|
area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
|
||||||
|
barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
|
||||||
|
nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
|
||||||
|
|
||||||
|
# Structures
|
||||||
|
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
|
||||||
|
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
|
||||||
|
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
|
||||||
|
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||||
|
|
||||||
|
# Blimey that was a lot of options
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
expected_values = {
|
||||||
|
'nonstandard_equipment': False,
|
||||||
|
'nonstandard_use': False,
|
||||||
|
'contractors': False,
|
||||||
|
'other_companies': False,
|
||||||
|
'crew_fatigue': False,
|
||||||
|
'big_power': False,
|
||||||
|
'generators': False,
|
||||||
|
'other_companies_power': False,
|
||||||
|
'nonstandard_equipment_power': False,
|
||||||
|
'multiple_electrical_environments': False,
|
||||||
|
'noise_monitoring': False,
|
||||||
|
'known_venue': False,
|
||||||
|
'safe_loading': False,
|
||||||
|
'safe_storage': False,
|
||||||
|
'area_outside_of_control': False,
|
||||||
|
'barrier_required': False,
|
||||||
|
'nonstandard_emergency_procedure': False,
|
||||||
|
'special_structures': False,
|
||||||
|
'suspended_structures': False,
|
||||||
|
}
|
||||||
|
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# Check for idiots
|
||||||
|
if not self.outside and self.generators:
|
||||||
|
raise forms.ValidationError("Engage brain, please. <strong>No generators indoors!(!)</strong>")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['event']
|
||||||
|
permissions = [
|
||||||
|
('review_riskassessment', 'Can review Risk Assessments')
|
||||||
|
]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def fields(self):
|
||||||
|
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_size(self):
|
||||||
|
# Confirm event size. Check all except generators, since generators entails outside
|
||||||
|
if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
|
||||||
|
return self.LARGE[0]
|
||||||
|
elif self.big_power:
|
||||||
|
return self.MEDIUM[0]
|
||||||
|
else:
|
||||||
|
return self.SMALL[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%i - %s" % (self.pk, self.event)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register(follow=['vehicles', 'crew'])
|
||||||
|
class EventChecklist(models.Model, RevisionMixin):
|
||||||
|
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
date = models.DateField()
|
||||||
|
|
||||||
|
# Safety Checks
|
||||||
|
safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?<br><small>(does not obstruct venue access)</small>")
|
||||||
|
safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?<br><small>(including flightcases)</small>")
|
||||||
|
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
|
||||||
|
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
|
||||||
|
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
|
||||||
|
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
|
||||||
|
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
||||||
|
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
|
||||||
|
|
||||||
|
# Small Electrical Checks
|
||||||
|
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
|
||||||
|
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
||||||
|
|
||||||
|
# Shared electrical checks
|
||||||
|
earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>")
|
||||||
|
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
|
||||||
|
|
||||||
|
# Medium Electrical Checks
|
||||||
|
source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?<br><small>(if cable is more than 3m long) </small>")
|
||||||
|
labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
|
||||||
|
# First Distro
|
||||||
|
fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
|
||||||
|
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
|
||||||
|
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
|
||||||
|
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
|
||||||
|
fd_earth_fault = models.IntegerField(blank=True, null=True, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
|
||||||
|
# Worst case points
|
||||||
|
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||||
|
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||||
|
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||||
|
w1_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||||
|
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||||
|
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||||
|
w2_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||||
|
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||||
|
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||||
|
w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
|
||||||
|
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>")
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def fields(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]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['event']
|
||||||
|
permissions = [
|
||||||
|
('review_eventchecklist', 'Can review Event Checklists')
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%i - %s" % (self.pk, 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):
|
||||||
|
if self.start > self.end:
|
||||||
|
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{} ({})".format(str(self.crewmember), self.role)
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from .models import *
|
|
||||||
from .finance import *
|
|
||||||
from .hs import *
|
|
||||||
from .events import *
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
import datetime
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from versioning.versioning import RevisionMixin
|
|
||||||
|
|
||||||
from RIGS.validators import validate_url
|
|
||||||
from .utils import filter_by_pk
|
|
||||||
from .finance import VatRate
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEventManager(models.Manager):
|
|
||||||
def event_search(self, q, start, end, status):
|
|
||||||
filt = Q()
|
|
||||||
if end:
|
|
||||||
filt &= Q(start_date__lte=end)
|
|
||||||
if start:
|
|
||||||
filt &= Q(start_date__gte=start)
|
|
||||||
|
|
||||||
objects = self.all()
|
|
||||||
|
|
||||||
if q:
|
|
||||||
objects = self.search(q)
|
|
||||||
|
|
||||||
if len(status) > 0:
|
|
||||||
filt &= Q(status__in=status)
|
|
||||||
|
|
||||||
qs = objects.filter(filt).order_by('-start_date')
|
|
||||||
|
|
||||||
# Preselect related for efficiency
|
|
||||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|
||||||
class EventManager(BaseEventManager):
|
|
||||||
def current_events(self):
|
|
||||||
events = self.filter(
|
|
||||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
|
||||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Ends after
|
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Active dry hire
|
|
||||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
|
||||||
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
|
||||||
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
|
||||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
return events
|
|
||||||
|
|
||||||
def events_in_bounds(self, start, end):
|
|
||||||
events = self.filter(
|
|
||||||
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
|
|
||||||
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
|
|
||||||
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
|
|
||||||
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
|
|
||||||
|
|
||||||
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
|
|
||||||
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
|
|
||||||
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
|
|
||||||
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
|
|
||||||
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
|
|
||||||
|
|
||||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
|
||||||
'organisation',
|
|
||||||
'venue', 'mic')
|
|
||||||
return events
|
|
||||||
|
|
||||||
def active_dry_hires(self):
|
|
||||||
return self.filter(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)
|
|
||||||
|
|
||||||
def rig_count(self):
|
|
||||||
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
|
||||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
|
||||||
is_rig=True)) | # Starts after with no end
|
|
||||||
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True)) | # Ends after
|
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)) # Active dry hire
|
|
||||||
).count()
|
|
||||||
return event_count
|
|
||||||
|
|
||||||
def waiting_invoices(self):
|
|
||||||
events = self.filter(
|
|
||||||
(
|
|
||||||
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
|
||||||
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
|
|
||||||
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
|
|
||||||
models.Q(is_rig=True) # Is a rig (not non-rig)
|
|
||||||
).order_by('start_date') \
|
|
||||||
.select_related('person', 'organisation', 'venue', 'mic') \
|
|
||||||
.prefetch_related('items')
|
|
||||||
|
|
||||||
return events
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def find_earliest_event_time(event, datetime_list):
|
|
||||||
# If there is no start time defined, pretend it's midnight
|
|
||||||
startTimeFaked = False
|
|
||||||
if event.has_start_time:
|
|
||||||
startDateTime = datetime.datetime.combine(event.start_date, event.start_time)
|
|
||||||
else:
|
|
||||||
startDateTime = datetime.datetime.combine(event.start_date, datetime.time(00, 00))
|
|
||||||
startTimeFaked = True
|
|
||||||
|
|
||||||
# timezoneIssues - apply the default timezone to the naiive datetime
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
startDateTime = tz.localize(startDateTime)
|
|
||||||
datetime_list.append(startDateTime) # then add it to the list
|
|
||||||
|
|
||||||
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
|
||||||
|
|
||||||
# if we faked it & it's the earliest, better own up
|
|
||||||
if startTimeFaked and earliest == startDateTime:
|
|
||||||
return event.start_date
|
|
||||||
return earliest
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEvent(models.Model, RevisionMixin):
|
|
||||||
# Done to make it much nicer on the database
|
|
||||||
PROVISIONAL = 0
|
|
||||||
CONFIRMED = 1
|
|
||||||
BOOKED = 2
|
|
||||||
CANCELLED = 3
|
|
||||||
EVENT_STATUS_CHOICES = (
|
|
||||||
(PROVISIONAL, 'Provisional'),
|
|
||||||
(CONFIRMED, 'Confirmed'),
|
|
||||||
(BOOKED, 'Booked'),
|
|
||||||
(CANCELLED, 'Cancelled'),
|
|
||||||
)
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
|
||||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
|
||||||
description = models.TextField(blank=True, default='')
|
|
||||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
|
||||||
|
|
||||||
# Timing
|
|
||||||
start_date = models.DateField()
|
|
||||||
start_time = models.TimeField(blank=True, null=True)
|
|
||||||
end_date = models.DateField(blank=True, null=True)
|
|
||||||
end_time = models.TimeField(blank=True, null=True)
|
|
||||||
|
|
||||||
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cancelled(self):
|
|
||||||
return (self.status == self.CANCELLED)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def confirmed(self):
|
|
||||||
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_start_time(self):
|
|
||||||
return self.start_time is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_end_time(self):
|
|
||||||
return self.end_time is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_time(self):
|
|
||||||
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
endDate = self.end_date
|
|
||||||
if endDate is None:
|
|
||||||
endDate = self.start_date
|
|
||||||
|
|
||||||
if self.has_end_time:
|
|
||||||
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
endDateTime = tz.localize(endDateTime)
|
|
||||||
|
|
||||||
return endDateTime
|
|
||||||
|
|
||||||
else:
|
|
||||||
return endDate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def length(self):
|
|
||||||
start = self.earliest_time
|
|
||||||
if isinstance(self.earliest_time, datetime.datetime):
|
|
||||||
start = self.earliest_time.date()
|
|
||||||
end = self.latest_time
|
|
||||||
if isinstance(self.latest_time, datetime.datetime):
|
|
||||||
end = self.latest_time.date()
|
|
||||||
return (end - start).days + 1
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
errdict = {}
|
|
||||||
if self.end_date and self.start_date > self.end_date:
|
|
||||||
errdict['end_date'] = ["Unless you've invented time travel, the event can't finish before it has started."]
|
|
||||||
|
|
||||||
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
|
||||||
hasStartAndEnd = self.has_start_time and self.has_end_time
|
|
||||||
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
|
||||||
errdict['end_time'] = ["Unless you've invented time travel, the event can't finish before it has started."]
|
|
||||||
return errdict
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.display_id}: {self.name}"
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
|
||||||
class Event(BaseEvent):
|
|
||||||
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
|
||||||
verbose_name="MIC", on_delete=models.CASCADE)
|
|
||||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
dry_hire = models.BooleanField(default=False)
|
|
||||||
is_rig = models.BooleanField(default=True)
|
|
||||||
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
|
||||||
null=True)
|
|
||||||
|
|
||||||
access_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
meet_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
|
|
||||||
# Dry-hire only
|
|
||||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
# Monies
|
|
||||||
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
|
||||||
|
|
||||||
# Authorisation request details
|
|
||||||
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
|
||||||
auth_request_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
auth_request_to = models.EmailField(blank=True, default='')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
if self.pk:
|
|
||||||
if self.is_rig:
|
|
||||||
return f"N{self.pk:05d}"
|
|
||||||
return self.pk
|
|
||||||
return "????"
|
|
||||||
|
|
||||||
# Calculated values
|
|
||||||
"""
|
|
||||||
EX Vat
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sum_total(self):
|
|
||||||
total = self.items.aggregate(
|
|
||||||
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
|
||||||
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
|
||||||
)['sum_total']
|
|
||||||
if total:
|
|
||||||
return total
|
|
||||||
return Decimal("0.00")
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def vat_rate(self):
|
|
||||||
return VatRate.objects.find_rate(self.start_date)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vat(self):
|
|
||||||
# No VAT is owed on internal transfers
|
|
||||||
if self.internal:
|
|
||||||
return 0
|
|
||||||
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
|
||||||
|
|
||||||
"""
|
|
||||||
Inc VAT
|
|
||||||
"""
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hs_done(self):
|
|
||||||
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def earliest_time(self):
|
|
||||||
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
|
||||||
|
|
||||||
# Put all the datetimes in a list
|
|
||||||
datetime_list = []
|
|
||||||
|
|
||||||
if self.access_at:
|
|
||||||
datetime_list.append(self.access_at)
|
|
||||||
|
|
||||||
if self.meet_at:
|
|
||||||
datetime_list.append(self.meet_at)
|
|
||||||
|
|
||||||
earliest = find_earliest_event_time(self, datetime_list)
|
|
||||||
|
|
||||||
return earliest
|
|
||||||
|
|
||||||
@property
|
|
||||||
def internal(self):
|
|
||||||
return bool(self.organisation and self.organisation.union_account)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def authorised(self):
|
|
||||||
if self.internal and hasattr(self, 'authorisation'):
|
|
||||||
return self.authorisation.amount == self.total
|
|
||||||
else:
|
|
||||||
return bool(self.purchase_order)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self):
|
|
||||||
if self.cancelled:
|
|
||||||
return "secondary"
|
|
||||||
elif not self.is_rig:
|
|
||||||
return "info"
|
|
||||||
elif not self.mic:
|
|
||||||
return "danger"
|
|
||||||
elif self.confirmed and self.authorised:
|
|
||||||
if self.dry_hire or self.riskassessment:
|
|
||||||
return "success"
|
|
||||||
return "warning"
|
|
||||||
else:
|
|
||||||
return "warning"
|
|
||||||
|
|
||||||
objects = EventManager()
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def get_edit_url(self):
|
|
||||||
return reverse('event_update', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
errdict = super().clean()
|
|
||||||
|
|
||||||
if self.access_at is not None:
|
|
||||||
if self.access_at.date() > self.start_date:
|
|
||||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
|
||||||
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
|
|
||||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
|
||||||
|
|
||||||
if errdict: # If there was an error when validation
|
|
||||||
raise ValidationError(errdict)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""Call :meth:`full_clean` before saving."""
|
|
||||||
self.full_clean()
|
|
||||||
super(Event, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventItem(models.Model, RevisionMixin):
|
|
||||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
description = models.TextField(blank=True, default='')
|
|
||||||
quantity = models.IntegerField()
|
|
||||||
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
|
||||||
order = models.IntegerField()
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_cost(self):
|
|
||||||
return self.cost * self.quantity
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['order']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"item {self.name}"
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventAuthorisation(models.Model, RevisionMixin):
|
|
||||||
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
|
||||||
email = models.EmailField()
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
|
||||||
account_code = models.CharField(max_length=50, default='', blank=True)
|
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
|
||||||
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('event_detail', kwargs={'pk': self.event_id})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
|
||||||
|
|
||||||
|
|
||||||
class SubhireManager(BaseEventManager):
|
|
||||||
def current_events(self):
|
|
||||||
events = self.exclude(status=BaseEvent.CANCELLED).filter(
|
|
||||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
|
||||||
(models.Q(end_date__gte=timezone.now().date())) # Ends after
|
|
||||||
).order_by('start_date', 'end_date', 'start_time', 'end_time').select_related('person', 'organisation')
|
|
||||||
|
|
||||||
return events
|
|
||||||
|
|
||||||
def event_count(self):
|
|
||||||
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
|
||||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
|
||||||
(models.Q(end_date__gte=timezone.now()))
|
|
||||||
).count()
|
|
||||||
return event_count
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class Subhire(BaseEvent):
|
|
||||||
insurance_value = models.DecimalField(max_digits=10, decimal_places=2) # TODO Validate if this is over notifiable threshold
|
|
||||||
events = models.ManyToManyField(Event)
|
|
||||||
quote = models.URLField(default='', validators=[validate_url])
|
|
||||||
|
|
||||||
objects = SubhireManager()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_rig(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dry_hire(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return f"S{self.pk:05d}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self):
|
|
||||||
return "purple"
|
|
||||||
|
|
||||||
def get_edit_url(self):
|
|
||||||
return reverse('subhire_update', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('subhire_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def earliest_time(self):
|
|
||||||
return find_earliest_event_time(self, [])
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
permissions = [
|
|
||||||
('subhire_finance', 'Can see financial data for subhire - insurance values')
|
|
||||||
]
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from versioning.versioning import RevisionMixin
|
|
||||||
from .utils import filter_by_pk
|
|
||||||
|
|
||||||
|
|
||||||
class VatManager(models.Manager):
|
|
||||||
def current_rate(self):
|
|
||||||
return self.find_rate(timezone.now())
|
|
||||||
|
|
||||||
def find_rate(self, date):
|
|
||||||
try:
|
|
||||||
return self.filter(start_at__lte=date).latest()
|
|
||||||
except VatRate.DoesNotExist:
|
|
||||||
r = VatRate
|
|
||||||
r.rate = 0
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class VatRate(models.Model, RevisionMixin):
|
|
||||||
start_at = models.DateField()
|
|
||||||
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
|
||||||
comment = models.CharField(max_length=255)
|
|
||||||
|
|
||||||
objects = VatManager()
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def as_percent(self):
|
|
||||||
return self.rate * 100
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-start_at']
|
|
||||||
get_latest_by = 'start_at'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceManager(models.Manager):
|
|
||||||
def outstanding_invoices(self):
|
|
||||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
|
||||||
sql = "SELECT * FROM " \
|
|
||||||
"(SELECT " \
|
|
||||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
|
||||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
|
||||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
|
||||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
|
||||||
"AS sub " \
|
|
||||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
|
||||||
"ORDER BY invoice_date"
|
|
||||||
|
|
||||||
query = self.raw(sql)
|
|
||||||
return query
|
|
||||||
|
|
||||||
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'])
|
|
||||||
class Invoice(models.Model, RevisionMixin):
|
|
||||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
|
||||||
invoice_date = models.DateField(auto_now_add=True)
|
|
||||||
void = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
reversion_perm = 'RIGS.view_invoice'
|
|
||||||
|
|
||||||
objects = InvoiceManager()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sum_total(self):
|
|
||||||
return self.event.sum_total
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return self.event.total
|
|
||||||
|
|
||||||
@property
|
|
||||||
def payment_total(self):
|
|
||||||
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
|
|
||||||
if total:
|
|
||||||
return total
|
|
||||||
return Decimal("0.00")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def balance(self):
|
|
||||||
return self.sum_total - self.payment_total
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_closed(self):
|
|
||||||
return self.balance == 0 or self.void
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"{self.display_id} for Event {self.event.display_id}"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return f"#{self.pk:05d}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-invoice_date']
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class Payment(models.Model, RevisionMixin):
|
|
||||||
CASH = 'C'
|
|
||||||
INTERNAL = 'I'
|
|
||||||
EXTERNAL = 'E'
|
|
||||||
SUCORE = 'SU'
|
|
||||||
ADJUSTMENT = 'T'
|
|
||||||
METHODS = (
|
|
||||||
(CASH, 'Cash'),
|
|
||||||
(INTERNAL, 'Internal'),
|
|
||||||
(EXTERNAL, 'External'),
|
|
||||||
(SUCORE, 'SU Core'),
|
|
||||||
(ADJUSTMENT, 'TEC Adjustment'),
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
|
||||||
date = models.DateField()
|
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
|
||||||
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.get_method_display()}: {self.amount}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"payment of £{self.amount}"
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from versioning.versioning import RevisionMixin
|
|
||||||
|
|
||||||
from RIGS.validators import validate_url
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class RiskAssessment(models.Model, RevisionMixin):
|
|
||||||
SMALL = (0, 'Small')
|
|
||||||
MEDIUM = (1, 'Medium')
|
|
||||||
LARGE = (2, 'Large')
|
|
||||||
SIZES = (SMALL, MEDIUM, LARGE)
|
|
||||||
|
|
||||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
|
||||||
# General
|
|
||||||
nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>"
|
|
||||||
"TEC's standard risk assessments and method statements?</a>")
|
|
||||||
nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>")
|
|
||||||
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
|
|
||||||
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
|
|
||||||
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
|
|
||||||
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
|
||||||
|
|
||||||
# Power
|
|
||||||
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
|
|
||||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
|
|
||||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
|
|
||||||
outside = models.BooleanField(help_text="Is the event outdoors?")
|
|
||||||
generators = models.BooleanField(help_text="Will generators be used?")
|
|
||||||
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
|
|
||||||
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
|
|
||||||
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
|
|
||||||
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
|
||||||
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
|
||||||
|
|
||||||
# Sound
|
|
||||||
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
|
|
||||||
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
|
||||||
|
|
||||||
# Site
|
|
||||||
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
|
|
||||||
safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
|
|
||||||
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
|
|
||||||
area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
|
|
||||||
barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
|
|
||||||
nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
|
|
||||||
|
|
||||||
# Structures
|
|
||||||
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
|
|
||||||
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
|
|
||||||
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
|
|
||||||
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
|
||||||
|
|
||||||
# Blimey that was a lot of options
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
expected_values = {
|
|
||||||
'nonstandard_equipment': False,
|
|
||||||
'nonstandard_use': False,
|
|
||||||
'contractors': False,
|
|
||||||
'other_companies': False,
|
|
||||||
'crew_fatigue': False,
|
|
||||||
# 'big_power': False Doesn't require checking with a super either way
|
|
||||||
'generators': False,
|
|
||||||
'other_companies_power': False,
|
|
||||||
'nonstandard_equipment_power': False,
|
|
||||||
'multiple_electrical_environments': False,
|
|
||||||
'noise_monitoring': False,
|
|
||||||
'known_venue': False,
|
|
||||||
'safe_loading': False,
|
|
||||||
'safe_storage': False,
|
|
||||||
'area_outside_of_control': False,
|
|
||||||
'barrier_required': False,
|
|
||||||
'nonstandard_emergency_procedure': False,
|
|
||||||
'special_structures': False,
|
|
||||||
'suspended_structures': False,
|
|
||||||
}
|
|
||||||
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
# Check for idiots
|
|
||||||
if not self.outside and self.generators:
|
|
||||||
raise forms.ValidationError("Engage brain, please. <strong>No generators indoors!(!)</strong>")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['event']
|
|
||||||
permissions = [
|
|
||||||
('review_riskassessment', 'Can review Risk Assessments')
|
|
||||||
]
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def fieldz(self):
|
|
||||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def event_size(self):
|
|
||||||
# Confirm event size. Check all except generators, since generators entails outside
|
|
||||||
if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
|
|
||||||
return self.LARGE[0]
|
|
||||||
elif self.big_power:
|
|
||||||
return self.MEDIUM[0]
|
|
||||||
else:
|
|
||||||
return self.SMALL[0]
|
|
||||||
|
|
||||||
def get_event_size_display(self):
|
|
||||||
return self.SIZES[self.event_size][1] + " Event"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str(self.event)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('ra_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.pk} | {self.event}"
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['vehicles', 'crew'])
|
|
||||||
class EventChecklist(models.Model, RevisionMixin):
|
|
||||||
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
date = models.DateField()
|
|
||||||
|
|
||||||
# Safety Checks
|
|
||||||
safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?<br><small>(does not obstruct venue access)</small>")
|
|
||||||
safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?<br><small>(including flightcases)</small>")
|
|
||||||
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
|
|
||||||
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
|
|
||||||
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
|
|
||||||
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
|
|
||||||
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
|
||||||
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
|
|
||||||
|
|
||||||
# Small Electrical Checks
|
|
||||||
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
|
|
||||||
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
|
||||||
|
|
||||||
# Shared electrical checks
|
|
||||||
earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>")
|
|
||||||
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
|
|
||||||
|
|
||||||
# Medium Electrical Checks
|
|
||||||
source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?<br><small>(if cable is more than 3m long) </small>")
|
|
||||||
labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
|
|
||||||
# First Distro
|
|
||||||
fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
|
|
||||||
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
|
|
||||||
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
|
|
||||||
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
|
|
||||||
fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
|
|
||||||
# Worst case points
|
|
||||||
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
|
||||||
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
|
||||||
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
|
||||||
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
|
||||||
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
|
||||||
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
|
||||||
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
|
||||||
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
|
||||||
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
|
||||||
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
|
|
||||||
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
|
||||||
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
|
||||||
|
|
||||||
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:
|
|
||||||
ordering = ['event']
|
|
||||||
permissions = [
|
|
||||||
('review_eventchecklist', 'Can review Event Checklists')
|
|
||||||
]
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def fieldz(self):
|
|
||||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str(self.event)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('ec_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.pk} | {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 f"{self.vehicle} driven by {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):
|
|
||||||
if self.start > self.end:
|
|
||||||
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.crewmember} ({self.role})"
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from versioning.versioning import RevisionMixin
|
|
||||||
from .events import Event
|
|
||||||
from .utils import filter_by_pk
|
|
||||||
|
|
||||||
|
|
||||||
class Profile(AbstractUser):
|
|
||||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
|
||||||
phone = models.CharField(max_length=13, blank=True, default='')
|
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
|
||||||
is_approved = models.BooleanField(default=False, 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...
|
|
||||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
|
||||||
dark_theme = models.BooleanField(default=False)
|
|
||||||
is_supervisor = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def make_api_key(cls):
|
|
||||||
size = 20
|
|
||||||
chars = string.ascii_letters + string.digits
|
|
||||||
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
|
||||||
return new_api_key
|
|
||||||
|
|
||||||
@property
|
|
||||||
def profile_picture(self):
|
|
||||||
url = ""
|
|
||||||
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
|
||||||
url = "https://www.gravatar.com/avatar/" + hashlib.md5(
|
|
||||||
self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
|
|
||||||
return url
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
name = self.get_full_name()
|
|
||||||
if self.initials:
|
|
||||||
name += f' "{self.initials}"'
|
|
||||||
return name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def admins(cls):
|
|
||||||
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def users_awaiting_approval_count(cls):
|
|
||||||
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
phone = models.CharField(max_length=15, blank=True, default='')
|
|
||||||
email = models.EmailField(blank=True, default='')
|
|
||||||
address = models.TextField(blank=True, default='')
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
|
|
||||||
objects = ContactableManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = self.name
|
|
||||||
if self.notes is not None:
|
|
||||||
if len(self.notes) > 0:
|
|
||||||
string += "*"
|
|
||||||
return string
|
|
||||||
|
|
||||||
@property
|
|
||||||
def organisations(self):
|
|
||||||
o = []
|
|
||||||
for e in Event.objects.filter(person=self).select_related('organisation'):
|
|
||||||
if e.organisation:
|
|
||||||
o.append(e.organisation)
|
|
||||||
|
|
||||||
# Count up occurances and put them in descending order
|
|
||||||
c = Counter(o)
|
|
||||||
stats = c.most_common()
|
|
||||||
return stats
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('person_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class Organisation(models.Model, RevisionMixin):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
phone = models.CharField(max_length=15, blank=True, default='')
|
|
||||||
email = models.EmailField(blank=True, default='')
|
|
||||||
address = models.TextField(blank=True, default='')
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
union_account = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
objects = ContactableManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = self.name
|
|
||||||
if self.notes is not None:
|
|
||||||
if len(self.notes) > 0:
|
|
||||||
string += "*"
|
|
||||||
return string
|
|
||||||
|
|
||||||
@property
|
|
||||||
def persons(self):
|
|
||||||
p = []
|
|
||||||
for e in Event.objects.filter(organisation=self).select_related('person'):
|
|
||||||
if e.person:
|
|
||||||
p.append(e.person)
|
|
||||||
|
|
||||||
# Count up occurances and put them in descending order
|
|
||||||
c = Counter(p)
|
|
||||||
stats = c.most_common()
|
|
||||||
return stats
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class Venue(models.Model, RevisionMixin):
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
phone = models.CharField(max_length=15, blank=True, default='')
|
|
||||||
email = models.EmailField(blank=True, default='')
|
|
||||||
three_phase_available = models.BooleanField(default=False)
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
address = models.TextField(blank=True, default='')
|
|
||||||
|
|
||||||
objects = ContactableManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = self.name
|
|
||||||
if self.notes and len(self.notes) > 0:
|
|
||||||
string += "*"
|
|
||||||
return string
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('venue_detail', kwargs={'pk': self.pk})
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,25 +1,34 @@
|
|||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import premailer
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import premailer
|
||||||
|
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.storage import staticfiles_storage
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from django.urls import reverse
|
||||||
from django.urls import reverse_lazy
|
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, PrintView, get_related
|
from PyRIGS.views import OEmbedView, is_ajax
|
||||||
from RIGS import models, forms, utils
|
from RIGS import models, forms
|
||||||
|
|
||||||
__author__ = 'ghost'
|
__author__ = 'ghost'
|
||||||
|
|
||||||
@@ -29,7 +38,7 @@ class RigboardIndex(generic.TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# get super context
|
# get super context
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(RigboardIndex, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
# call out method to get current events
|
# call out method to get current events
|
||||||
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
|
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
|
||||||
@@ -37,35 +46,23 @@ class RigboardIndex(generic.TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WebCalendar(generic.ListView):
|
class WebCalendar(generic.TemplateView):
|
||||||
model = models.Event
|
|
||||||
template_name = 'calendar.html'
|
template_name = 'calendar.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(WebCalendar, self).get_context_data(**kwargs)
|
||||||
# use today's date for the calendar
|
context['view'] = kwargs.get('view', '')
|
||||||
d = utils.get_date(self.request.GET.get('month', None))
|
context['date'] = kwargs.get('date', '')
|
||||||
context['prev_month'] = utils.prev_month(d)
|
|
||||||
context['next_month'] = utils.next_month(d)
|
|
||||||
|
|
||||||
# Instantiate our calendar class with today's year and date
|
|
||||||
cal = utils.Calendar(d.year, d.month)
|
|
||||||
|
|
||||||
# Call the formatmonth method, which returns our calendar as a table
|
|
||||||
html_cal = cal.formatmonth(withyear=True)
|
|
||||||
# context['calendar'] = mark_safe(html_cal)
|
|
||||||
context['weeks'] = html_cal
|
|
||||||
context['page_title'] = d.strftime("%B %Y")
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventDetail(generic.DetailView, ModalURLMixin):
|
class EventDetail(generic.DetailView):
|
||||||
template_name = 'event_detail.html'
|
template_name = 'event_detail.html'
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventDetail, self).get_context_data(**kwargs)
|
||||||
title = f"{self.object.display_id} | {self.object.name}"
|
title = "{} | {}".format(self.object.display_id, self.object.name)
|
||||||
if self.object.dry_hire:
|
if self.object.dry_hire:
|
||||||
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
||||||
context['page_title'] = title
|
context['page_title'] = title
|
||||||
@@ -87,7 +84,7 @@ class EventCreate(generic.CreateView):
|
|||||||
template_name = 'event_form.html'
|
template_name = 'event_form.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventCreate, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = "New Event"
|
context['page_title'] = "New Event"
|
||||||
context['edit'] = True
|
context['edit'] = True
|
||||||
context['currentVAT'] = models.VatRate.objects.current_rate()
|
context['currentVAT'] = models.VatRate.objects.current_rate()
|
||||||
@@ -96,8 +93,11 @@ 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_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)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -110,13 +110,17 @@ class EventUpdate(generic.UpdateView):
|
|||||||
template_name = 'event_form.html'
|
template_name = 'event_form.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventUpdate, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = f"Event {self.object.display_id}"
|
context['page_title'] = "Event {}".format(self.object.display_id)
|
||||||
context['edit'] = True
|
context['edit'] = True
|
||||||
|
|
||||||
form = context['form']
|
form = context['form']
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -130,7 +134,7 @@ class EventUpdate(generic.UpdateView):
|
|||||||
if hasattr(self.object, 'authorised'):
|
if hasattr(self.object, 'authorised'):
|
||||||
messages.warning(self.request,
|
messages.warning(self.request,
|
||||||
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
|
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
|
||||||
return super().render_to_response(context, **response_kwargs)
|
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
||||||
@@ -138,7 +142,7 @@ class EventUpdate(generic.UpdateView):
|
|||||||
|
|
||||||
class EventDuplicate(EventUpdate):
|
class EventDuplicate(EventUpdate):
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
old = super().get_object(queryset) # Get the object (the event you're duplicating)
|
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
|
||||||
new = copy.copy(old) # Make a copy of the object in memory
|
new = copy.copy(old) # Make a copy of the object in memory
|
||||||
new.based_on = old # Make the new event based on the old event
|
new.based_on = old # Make the new event based on the old event
|
||||||
new.purchase_order = None # Remove old PO
|
new.purchase_order = None # Remove old PO
|
||||||
@@ -147,7 +151,6 @@ class EventDuplicate(EventUpdate):
|
|||||||
# Clear checked in by if it's a dry hire
|
# Clear checked in by if it's a dry hire
|
||||||
if new.dry_hire is True:
|
if new.dry_hire is True:
|
||||||
new.checked_in_by = None
|
new.checked_in_by = None
|
||||||
new.collector = None
|
|
||||||
|
|
||||||
# Remove all the authorisation information from the new event
|
# Remove all the authorisation information from the new event
|
||||||
new.auth_request_to = ''
|
new.auth_request_to = ''
|
||||||
@@ -163,22 +166,41 @@ class EventDuplicate(EventUpdate):
|
|||||||
return new
|
return new
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventDuplicate, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = f"Duplicate of Event {self.object.display_id}"
|
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id)
|
||||||
context["duplicate"] = True
|
context["duplicate"] = True
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventPrint(PrintView):
|
class EventPrint(generic.View):
|
||||||
model = models.Event
|
def get(self, request, pk):
|
||||||
template_name = 'event_print.xml'
|
object = get_object_or_404(models.Event, pk=pk)
|
||||||
append_terms = True
|
template = get_template('event_print.xml')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
merger = PdfFileMerger()
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['quote'] = True
|
context = {
|
||||||
context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf"
|
'object': object,
|
||||||
return context
|
'quote': True,
|
||||||
|
'current_user': request.user,
|
||||||
|
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
|
||||||
|
}
|
||||||
|
|
||||||
|
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):
|
||||||
@@ -187,7 +209,9 @@ class EventArchive(generic.ListView):
|
|||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
# get super context
|
||||||
|
context = super(EventArchive, self).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
|
||||||
@@ -204,9 +228,44 @@ class EventArchive(generic.ListView):
|
|||||||
"Muppet! Check the dates, it has been fixed for you.")
|
"Muppet! Check the dates, it has been fixed for you.")
|
||||||
start, end = end, start # Stop the impending fail
|
start, end = end, start # Stop the impending fail
|
||||||
|
|
||||||
qs = self.model.objects.event_search(self.request.GET.get('q', None), start, end, self.request.GET.get('status', ""))
|
filter = Q()
|
||||||
|
if end != "":
|
||||||
|
filter &= Q(start_date__lte=end)
|
||||||
|
if start:
|
||||||
|
filter &= Q(start_date__gte=start)
|
||||||
|
|
||||||
if not qs.exists():
|
q = self.request.GET.get('q', "")
|
||||||
|
|
||||||
|
if q != "":
|
||||||
|
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=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', "")
|
||||||
|
|
||||||
|
if len(status) > 0:
|
||||||
|
filter &= Q(status__in=status)
|
||||||
|
|
||||||
|
qs = self.model.objects.filter(filter).order_by('-start_date')
|
||||||
|
|
||||||
|
# Preselect related for efficiency
|
||||||
|
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
if len(qs) == 0:
|
||||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
@@ -215,7 +274,6 @@ class EventArchive(generic.ListView):
|
|||||||
class EventAuthorise(generic.UpdateView):
|
class EventAuthorise(generic.UpdateView):
|
||||||
template_name = 'eventauthorisation_form.html'
|
template_name = 'eventauthorisation_form.html'
|
||||||
success_template = 'eventauthorisation_success.html'
|
success_template = 'eventauthorisation_success.html'
|
||||||
preview = False
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
@@ -223,7 +281,7 @@ class EventAuthorise(generic.UpdateView):
|
|||||||
self.template_name = self.success_template
|
self.template_name = self.success_template
|
||||||
messages.add_message(self.request, messages.SUCCESS,
|
messages.add_message(self.request, messages.SUCCESS,
|
||||||
'Success! Your event has been authorised. ' +
|
'Success! Your event has been authorised. ' +
|
||||||
f'You will also receive email confirmation to {self.object.email}.')
|
'You will also receive email confirmation to %s.' % self.object.email)
|
||||||
return self.render_to_response(self.get_context_data())
|
return self.render_to_response(self.get_context_data())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -237,13 +295,12 @@ class EventAuthorise(generic.UpdateView):
|
|||||||
return forms.InternalClientEventAuthorisationForm
|
return forms.InternalClientEventAuthorisationForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventAuthorise, self).get_context_data(**kwargs)
|
||||||
context['event'] = self.event
|
context['event'] = self.event
|
||||||
context['tos_url'] = settings.TERMS_OF_HIRE_URL
|
context['tos_url'] = settings.TERMS_OF_HIRE_URL
|
||||||
context['page_title'] = f"{self.event.display_id}: {self.event.name}"
|
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
|
||||||
if self.event.dry_hire:
|
if self.event.dry_hire:
|
||||||
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
|
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
|
||||||
context['preview'] = self.preview
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
@@ -256,10 +313,10 @@ 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().get(request, *args, **kwargs)
|
return super(EventAuthorise, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
def get_form(self, **kwargs):
|
||||||
form = super().get_form(**kwargs)
|
form = super(EventAuthorise, self).get_form(**kwargs)
|
||||||
form.instance.event = self.event
|
form.instance.event = self.event
|
||||||
form.instance.email = self.request.email
|
form.instance.email = self.request.email
|
||||||
form.instance.sent_by = self.request.sent_by
|
form.instance.sent_by = self.request.sent_by
|
||||||
@@ -275,7 +332,7 @@ class EventAuthorise(generic.UpdateView):
|
|||||||
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
|
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
|
||||||
raise SuspiciousOperation(
|
raise SuspiciousOperation(
|
||||||
"This URL is invalid. Please ask your TEC contact for a new URL")
|
"This URL is invalid. Please ask your TEC contact for a new URL")
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super(EventAuthorise, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
|
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
|
||||||
@@ -285,7 +342,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
|
|
||||||
@method_decorator(decorators.nottinghamtec_address_required)
|
@method_decorator(decorators.nottinghamtec_address_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super().dispatch(*args, **kwargs)
|
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def object(self):
|
def object(self):
|
||||||
@@ -325,13 +382,13 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
context['to_name'] = event.organisation.name
|
context['to_name'] = event.organisation.name
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
msg = EmailMultiAlternatives(
|
||||||
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
|
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
|
||||||
get_template("email/eventauthorisation_client_request.txt").render(context),
|
get_template("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 = staticfiles_storage.path('css/email.css')
|
||||||
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context),
|
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
msg.attach_alternative(html, 'text/html')
|
msg.attach_alternative(html, 'text/html')
|
||||||
|
|
||||||
@@ -341,23 +398,23 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
|
|
||||||
|
|
||||||
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||||
template_name = "email/eventauthorisation_client_request.html"
|
template_name = "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):
|
||||||
css = finders.find('css/email.css')
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
response = super().render_to_response(context, **response_kwargs)
|
css = staticfiles_storage.path('css/email.css')
|
||||||
|
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
|
||||||
assert isinstance(response, HttpResponse)
|
assert isinstance(response, HttpResponse)
|
||||||
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
|
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs)
|
||||||
context['hmac'] = signing.dumps({
|
context['hmac'] = signing.dumps({
|
||||||
'pk': self.object.pk,
|
'pk': self.object.pk,
|
||||||
'email': self.request.GET.get('email', 'hello@world.test'),
|
'email': self.request.GET.get('email', 'hello@world.test'),
|
||||||
'sent_by': self.request.user.pk,
|
'sent_by': self.request.user.pk,
|
||||||
})
|
})
|
||||||
context['to_name'] = self.request.GET.get('to_name', None)
|
context['to_name'] = self.request.GET.get('to_name', None)
|
||||||
context['target'] = 'event_authorise_form_preview'
|
|
||||||
return context
|
return context
|
||||||
@@ -6,7 +6,7 @@ from io import BytesIO
|
|||||||
|
|
||||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
@@ -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 = f"{instance.event.display_id} | {instance.event.name} - Event Authorised"
|
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
|
||||||
|
|
||||||
client_email = EmailMultiAlternatives(
|
client_email = EmailMultiAlternatives(
|
||||||
subject,
|
subject,
|
||||||
get_template("email/eventauthorisation_client_success.txt").render(context),
|
get_template("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 = staticfiles_storage.path('css/email.css')
|
||||||
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
|
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
client_email.attach_alternative(html, 'text/html')
|
client_email.attach_alternative(html, 'text/html')
|
||||||
|
|
||||||
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(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf',
|
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
|
||||||
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("email/eventauthorisation_mic_success.txt").render(context),
|
get_template("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(
|
||||||
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
|
||||||
get_template("email/admin_awaiting_approval.txt").render(context),
|
get_template("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 = staticfiles_storage.path('css/email.css')
|
||||||
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
|
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
email.attach_alternative(html, 'text/html')
|
email.attach_alternative(html, 'text/html')
|
||||||
email.send()
|
email.send()
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 5.4 MiB |
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 164 KiB |
6
RIGS/static/js/marked.min.js
vendored
@@ -1,142 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load invoices_waiting from filters %}
|
|
||||||
{% load invoices_outstanding from filters %}
|
|
||||||
{% load total_invoices_todo from filters %}
|
|
||||||
|
|
||||||
{% block titleheader %}
|
{% block titleheader %}
|
||||||
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
|
<a class="navbar-brand" href="/">RIGS</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block titleelements %}
|
{% block titleelements %}
|
||||||
@@ -30,8 +27,6 @@
|
|||||||
{% if perms.RIGS.add_event %}
|
{% if perms.RIGS.add_event %}
|
||||||
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
|
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
|
||||||
New Event</a>
|
New Event</a>
|
||||||
<a class="dropdown-item" href="{% url 'subhire_create' %}"><span class="fas fa-truck"></span>
|
|
||||||
New Subhire</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -49,17 +44,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.RIGS.view_invoice %}
|
{% if perms.RIGS.view_invoice %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
{% total_invoices_todo as todo %}
|
|
||||||
{% invoices_waiting as waiting %}
|
|
||||||
{% invoices_outstanding as outstanding %}
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
Invoices <span class="badge {% if todo == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ todo }}</span>
|
Invoices
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
|
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
|
||||||
{% if perms.RIGS.add_invoice %}
|
{% if perms.RIGS.add_invoice %}
|
||||||
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ waiting }}</span></a>
|
<a class="dropdown-item" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding <span class="badge {% if outstanding == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ outstanding }}</span></a>
|
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding</a>
|
||||||
<a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a>
|
<a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -82,7 +74,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ block.super }}
|
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
<script src="{% static 'js/popover.js' %}"></script>
|
<script src="{% static 'js/popover.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,131 +1,197 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block js %}
|
{% block title %}Calendar{% endblock %}
|
||||||
<script src="{% static 'js/moment.js' %}"></script>
|
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
// set some button listeners
|
|
||||||
$('#today-button').click(function(){ calendar.today(); });
|
|
||||||
$('#go-to-date-input').change(function(){
|
|
||||||
if(moment($('#go-to-date-input').val()).isValid()){
|
|
||||||
document.getElementById('go-to-date-button').classList.remove('disabled');
|
|
||||||
document.getElementById('go-to-date-button').href = "?month=" + moment($('#go-to-date-input').val()).format("YYYY-MM");
|
|
||||||
} else{
|
|
||||||
document.getElementById('go-to-date-button').classList.add('disabled');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<style>
|
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
|
||||||
.week {
|
{% endblock %}
|
||||||
display:grid;
|
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
||||||
grid-auto-flow: dense;
|
|
||||||
grid-gap: 2px 10px;
|
|
||||||
border: 1px solid black;
|
|
||||||
height: 8em;
|
|
||||||
align-content: start;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day {
|
{% block js %}
|
||||||
display:contents;
|
<script src="{% static 'js/moment.js' %}"></script>
|
||||||
}
|
<script src="{% static 'js/main.js' %}"></script>
|
||||||
.day-label {
|
<script>
|
||||||
grid-row-start: 1;
|
viewToUrl = {
|
||||||
text-align: right;
|
'timeGridWeek':'week',
|
||||||
margin:0;
|
'timeGridDay':'day',
|
||||||
font-size: 1em !important;
|
'dayGridMonth':'month'
|
||||||
height: 1em;
|
}
|
||||||
}
|
viewFromUrl = {
|
||||||
|
'week':'timeGridWeek',
|
||||||
|
'day':'timeGridDay',
|
||||||
|
'month':'dayGridMonth'
|
||||||
|
}
|
||||||
|
var calendar; //Need to access it from jquery ready
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
.week-day, .day-label, .event {
|
calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
padding: 4px 10px;
|
themeSystem: 'bootstrap',
|
||||||
}
|
//defaultView: 'dayGridMonth', This is now default
|
||||||
|
aspectRatio: 1.5,
|
||||||
|
eventTimeFormat: {
|
||||||
|
'hour': '2-digit',
|
||||||
|
'minute': '2-digit',
|
||||||
|
'hour12': false
|
||||||
|
},
|
||||||
|
//nowIndicator: true,
|
||||||
|
//firstDay: 1,
|
||||||
|
headerToolbar: false,
|
||||||
|
editable: false,
|
||||||
|
dayMaxEventRows: true, // allow "more" link when too many events
|
||||||
|
events: function(fetchInfo, successCallback, failureCallback) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/event',
|
||||||
|
dataType: 'json',
|
||||||
|
data: {
|
||||||
|
start: moment(fetchInfo.startStr).format("YYYY-MM-DD[T]HH:mm:ss"),
|
||||||
|
end: moment(fetchInfo.endStr).format("YYYY-MM-DD[T]HH:mm:ss")
|
||||||
|
},
|
||||||
|
success: function(doc) {
|
||||||
|
var events = [];
|
||||||
|
colours = {
|
||||||
|
'Provisional': '#FFE89B',
|
||||||
|
'Confirmed': '#3AB54A' ,
|
||||||
|
'Booked': '#3AB54A' ,
|
||||||
|
'Cancelled': 'grey' ,
|
||||||
|
'non-rig': '#25AAE2'
|
||||||
|
};
|
||||||
|
$(doc).each(function() {
|
||||||
|
end = $(this).attr('latest')
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
.event {
|
thisEvent = {
|
||||||
background-color: #CCC;
|
'start': $(this).attr('earliest'),
|
||||||
font-size: 0.8em !important;
|
'end': end,
|
||||||
white-space: nowrap;
|
'className': 'modal-href',
|
||||||
overflow: hidden;
|
'title': $(this).attr('title'),
|
||||||
}
|
'url': $(this).attr('url')
|
||||||
|
}
|
||||||
|
|
||||||
.event-end {
|
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
||||||
border-top-right-radius: 5px;
|
thisEvent['color'] = colours[$(this).attr('status')];
|
||||||
border-bottom-right-radius: 5px;
|
}else{
|
||||||
}
|
thisEvent['color'] = colours['non-rig'];
|
||||||
|
}
|
||||||
|
events.push(thisEvent);
|
||||||
|
});
|
||||||
|
successCallback(events);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
datesSet: function(info) {
|
||||||
|
var view = info.view;
|
||||||
|
// Set the title of the view
|
||||||
|
$('#calendar-header').text(view.title);
|
||||||
|
|
||||||
.event-start {
|
// Enable/Disable "Today" button as required
|
||||||
border-top-left-radius: 5px;
|
let $today = $('#today-button');
|
||||||
border-bottom-left-radius: 5px;
|
if(moment().isBetween(view.currentStart, view.currentEnd)){
|
||||||
}
|
//Today is within the current view
|
||||||
|
$today.prop('disabled', true);
|
||||||
|
}else{
|
||||||
|
$today.prop('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
.week-day {
|
// Set active view select button
|
||||||
font-size: 0.8em;
|
let $month = $('#month-button');
|
||||||
}
|
let $week = $('#week-button');
|
||||||
|
let $day = $('#day-button');
|
||||||
|
switch(view.type){
|
||||||
|
case 'dayGridMonth':
|
||||||
|
$month.addClass('active');
|
||||||
|
$week.removeClass('active');
|
||||||
|
$day.removeClass('active');
|
||||||
|
break;
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
case 'timeGridWeek':
|
||||||
.event {
|
$month.removeClass('active');
|
||||||
padding: 2px;
|
$week.addClass('active');
|
||||||
}
|
$day.removeClass('active');
|
||||||
}
|
break;
|
||||||
|
|
||||||
[data-span="1"] { grid-column-end: span 1; }
|
case 'timeGridDay':
|
||||||
[data-span="2"] { grid-column-end: span 2; }
|
$month.removeClass('active');
|
||||||
[data-span="3"] { grid-column-end: span 3; }
|
$week.removeClass('active');
|
||||||
[data-span="4"] { grid-column-end: span 4; }
|
$day.addClass('active');
|
||||||
[data-span="5"] { grid-column-end: span 5; }
|
break;
|
||||||
[data-span="6"] { grid-column-end: span 6; }
|
}
|
||||||
[data-span="7"] { grid-column-end: span 7; }
|
history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
|
||||||
|
}
|
||||||
.day > a {
|
});
|
||||||
color: inherit !important;
|
calendar.render();
|
||||||
text-decoration: inherit !important;
|
});
|
||||||
}
|
$(document).ready(function() {
|
||||||
|
// set some button listeners
|
||||||
</style>
|
$('#next-button').click(function(){ calendar.next(); });
|
||||||
|
$('#prev-button').click(function(){ calendar.prev(); });
|
||||||
|
$('#today-button').click(function(){ calendar.today(); });
|
||||||
|
$('#month-button').click(function(){ calendar.changeView('dayGridMonth'); });
|
||||||
|
$('#week-button').click(function(){ calendar.changeView('timeGridWeek'); });
|
||||||
|
$('#day-button').click(function(){ calendar.changeView('timeGridDay'); });
|
||||||
|
$('#go-to-date-input').change(function(){
|
||||||
|
if(moment($('#go-to-date-input').val()).isValid()){
|
||||||
|
$('#go-to-date-button').prop('disabled', false);
|
||||||
|
} else{
|
||||||
|
$('#go-to-date-button').prop('disabled', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#go-to-date-button').click(function(){
|
||||||
|
day = moment($('#go-to-date-input').val());
|
||||||
|
if(day.isValid()){
|
||||||
|
calendar.gotoDate(day.format("YYYY-MM-DD"));
|
||||||
|
} else{
|
||||||
|
alert('Invalid Date');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% if view and date %}
|
||||||
|
// Go to the initial settings, if they're valid
|
||||||
|
view = viewFromUrl['{{view}}'];
|
||||||
|
calendar.changeView(view);
|
||||||
|
day = moment('{{date}}');
|
||||||
|
if(day.isValid()){
|
||||||
|
calendar.gotoDate(day.format("YYYY-MM-DD"));
|
||||||
|
} else{
|
||||||
|
console.log('Supplied date is invalid - using default')
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center mb-1">
|
<div class="row">
|
||||||
<a class="btn btn-info col-2" href="{% url 'web_calendar' %}?{{ prev_month }}"><span class="fas fa-chevron-left"></span> Previous Month</a>
|
<div class="col-sm-12">
|
||||||
<div class="form-inline col-4">
|
<div class="pull-left">
|
||||||
<div class="input-group">
|
<span id="calendar-header" class="h2"></span>
|
||||||
<input type="date" id="go-to-date-input" placeholder="Go to date..." class="form-control">
|
</div>
|
||||||
<span class="input-group-append">
|
<div class="form-inline float-right btn-page my-3">
|
||||||
<a class="btn btn-success" id="go-to-date-button">Go!</a>
|
<div class="input-group mx-2">
|
||||||
</span>
|
<input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
|
||||||
|
<span class="input-group-append">
|
||||||
|
<button class="btn btn-success" id="go-to-date-button" type="button" disabled>Go!</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group mx-2">
|
||||||
|
<button type="button" class="btn btn-primary" id="today-button">Today</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group mx-2">
|
||||||
|
<button type="button" class="btn btn-secondary" id="prev-button"><span class="fas fa-chevron-left"></span></button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="next-button"><span class="fas fa-chevron-right"></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group ml-2">
|
||||||
|
<button type="button" class="btn btn-light" id="month-button">Month</button>
|
||||||
|
<button type="button" class="btn btn-light" id="week-button">Week</button>
|
||||||
|
<button type="button" class="btn btn-light" id="day-button">Day</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div id='calendar'></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary col-2" id="today-button">Today</button>
|
|
||||||
<a class="btn btn-info mx-2 col-2" href="{% url 'web_calendar' %}?{{ next_month }}"><span class="fas fa-chevron-right"></span> Next Month</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="week" style="height: 2em;">
|
|
||||||
<div class="week-day">Monday</div>
|
|
||||||
<div class="week-day">Tuesday</div>
|
|
||||||
<div class="week-day">Wednesday</div>
|
|
||||||
<div class="week-day">Thursday</div>
|
|
||||||
<div class="week-day">Friday</div>
|
|
||||||
<div class="week-day">Saturday</div>
|
|
||||||
<div class="week-day">Sunday</div>
|
|
||||||
</div>
|
|
||||||
{% for week in weeks %}
|
|
||||||
<div class="week">
|
|
||||||
{% for day in week %}
|
|
||||||
{% if day.0 != 0 %}
|
|
||||||
<div class="day" id="{{day.0}}">
|
|
||||||
<h3 class="day-label text-muted">{{day.0}}</h3>
|
|
||||||
{{ day.2|safe }}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="day"><span style="grid-row-start: 1;"> <span></div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
|
||||||
|
|
||||||
{% load button from filters %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Upcoming Events</div>
|
|
||||||
<div class="card-body">{{ rig_count }}</div>
|
|
||||||
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Upcoming Subhire</div>
|
|
||||||
<div class="card-body">{{ subhire_count }}</div>
|
|
||||||
<div class="card-footer"><a href={% url 'subhire_list' %}>View</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Active Dry Hires</div>
|
|
||||||
<div class="card-body">{{ hire_count }}</div>
|
|
||||||
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -5,17 +5,44 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ block.super }}
|
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block preload_js %}
|
{% block preload_js %}
|
||||||
{{ block.super }}
|
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||||
<script src="{% static 'js/selects.js' %}" async></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'partials/archive_form.html' %}
|
<div class="row">
|
||||||
|
<div class="col-sm-12 py-2">
|
||||||
|
<form class="form-inline" method="GET">
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Start</span>
|
||||||
|
</div>
|
||||||
|
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">End</span>
|
||||||
|
</div>
|
||||||
|
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Keyword</span>
|
||||||
|
</div>
|
||||||
|
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
|
||||||
|
{% for status in statuses %}
|
||||||
|
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% button 'search' %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% with object_list as events %}
|
{% with object_list as events %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<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='ec_edit' pk=object.pk %}
|
||||||
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
{% button 'view' url='event_detail' pk=object.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_eventchecklist review='ec_review' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,11 +32,7 @@
|
|||||||
</dd>
|
</dd>
|
||||||
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
|
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
|
||||||
<dd class="col-6">
|
<dd class="col-6">
|
||||||
{% if object.power_mic %}
|
|
||||||
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
|
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
|
||||||
{% else %}
|
|
||||||
None
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<p>List vehicles and their drivers</p>
|
<p>List vehicles and their drivers</p>
|
||||||
@@ -102,10 +98,6 @@
|
|||||||
<td>{{crew.role}}</td>
|
<td>{{crew.role}}</td>
|
||||||
<td>{{crew.end}}</td>
|
<td>{{crew.end}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -113,27 +105,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
|
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
|
||||||
|
{% if object.event.riskassessment.event_size != 2 %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if object.event.riskassessment.event_size == 0 %}
|
{% if object.event.riskassessment.event_size == 1 %}
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.rcds|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.supply_test|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.earthing|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.pat|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
{% else %}
|
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
|
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
|
||||||
<dd class="col-2">
|
<dd class="col-2">
|
||||||
@@ -238,8 +212,28 @@
|
|||||||
</dl>
|
</dl>
|
||||||
<hr>
|
<hr>
|
||||||
{% include 'partials/ec_power_info.html' %}
|
{% include 'partials/ec_power_info.html' %}
|
||||||
|
{% else %}
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.rcds|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.supply_test|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.earthing|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.pat|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 text-right">
|
<div class="col-12 text-right">
|
||||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||||
@@ -7,19 +7,27 @@
|
|||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block preload_js %}
|
{% block preload_js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||||
|
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
<script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise-->
|
||||||
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
|
<script src="{% static 'js/modal.js' %}"></script>
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
|
||||||
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
|
|
||||||
|
{% include 'partials/datetime-fix.html' %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('button[data-action=add]').on('click', function (event) {
|
$('button[data-action=add]').on('click', function (event) {
|
||||||
@@ -242,19 +250,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% elif event.riskassessment.event_size == 1 %}
|
||||||
<div class="row my-3" id="size-1">
|
<div class="row my-3" id="size-1">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
{% if event.riskassessment.event_size == 1 %}
|
|
||||||
<div class="card border-warning">
|
<div class="card border-warning">
|
||||||
<div class="card-header">Electrical Checks <small>for ‘Medium’ TEC Events </small></div>
|
<div class="card-header">Electrical Checks <small>for ‘Medium’ TEC Events </small></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% else %}
|
|
||||||
<div class="card border-danger">
|
|
||||||
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
|
|
||||||
{% endif %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
|
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
|
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
|
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
|
||||||
@@ -344,6 +345,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row my-3" id="size-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Outside the scope of this assessment. <strong>I really hope you checked with a supervisor...</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
@@ -1,20 +1,63 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
|
{% load linkornone from filters %}
|
||||||
{% load markdown_tags %}
|
{% load namewithnotes from filters %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3 py-3">
|
<div class="row my-3 py-3">
|
||||||
{% if not request.is_ajax %}
|
{% if not request.is_ajax %}
|
||||||
{% if perms.RIGS.view_event %}
|
{% if perms.RIGS.view_event %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'partials/event_detail_buttons.html' %}
|
{% include 'event_detail_buttons.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.is_rig and perms.RIGS.view_event %}
|
{% if object.is_rig and perms.RIGS.view_event %}
|
||||||
{# only need contact details for a rig #}
|
{# only need contact details for a rig #}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% include 'partials/contact_details.html' %}
|
{% if event.person %}
|
||||||
|
<div class="card card-default mb-3">
|
||||||
|
<div class="card-header">Contact Details</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6">Person</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
{% if object.person %}
|
||||||
|
<a href="{% url 'person_detail' object.person.pk %}" class="modal-href">
|
||||||
|
{{ object.person|namewithnotes:'person_detail' }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-6">Email</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
|
||||||
|
<dt class="col-sm-6">Phone Number</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.organisation %}
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-header">Organisation</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6">Organisation</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
{% if object.organisation %}
|
||||||
|
<a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href">
|
||||||
|
{{ object.organisation|namewithnotes:'organisation_detail' }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-6">Email</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd>
|
||||||
|
<dt class="col-sm-6">Phone Number</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
|
||||||
|
<dt class="col-sm-6">Has SU Account</dt>
|
||||||
|
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -25,14 +68,16 @@
|
|||||||
{% include 'partials/hs_details.html' %}
|
{% include 'partials/hs_details.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
{% if event.is_rig %}
|
||||||
<div class="col-md-8 py-3">
|
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
||||||
{% include 'partials/auth_details.html' %}
|
<div class="col-md-8 py-3">
|
||||||
</div>
|
{% include 'partials/auth_details.html' %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'partials/event_detail_buttons.html' %}
|
{% include 'event_detail_buttons.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.is_rig %}
|
{% if event.is_rig %}
|
||||||
@@ -43,22 +88,16 @@
|
|||||||
{% if perms.RIGS.view_event %}
|
{% if perms.RIGS.view_event %}
|
||||||
<h4>Notes</h4>
|
<h4>Notes</h4>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
<p class="dont-break-out">{{ event.notes|linebreaksbr }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h4>Event Items</h4>
|
<br>
|
||||||
|
{% include 'item_table.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'partials/item_table.html' %}
|
|
||||||
{% if event.subhire_set.count > 0 %}
|
|
||||||
<div class="card-body"><h4>Associated Subhires</h4></div>
|
|
||||||
{% with event.subhire_set.all as events %}
|
|
||||||
{% include 'partials/event_table.html' %}
|
|
||||||
{%endwith%}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'partials/event_detail_buttons.html' %}
|
{% include 'event_detail_buttons.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -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 disabled
|
btn-success
|
||||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||||
btn-warning
|
btn-warning
|
||||||
{% elif event.auth_request_to %}
|
{% elif event.auth_request_to %}
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
btn-secondary
|
btn-secondary
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"
|
"
|
||||||
{% if event.authorised %}aria-disabled="true"{% else %}href="{% url 'event_authorise_request' object.pk %}"{% endif %}>
|
href="{% url 'event_authorise_request' object.pk %}">
|
||||||
<span class="fas fa-paper-plane"></span>
|
<i class="fas fa-paper-plane"></i>
|
||||||
<span class="d-none d-sm-inline">
|
<span class="d-none d-sm-inline">
|
||||||
{% if event.authorised %}
|
{% if event.authorised %}
|
||||||
Authorised
|
Authorised
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
{% extends 'base_embed.html' %}
|
{% extends 'base_embed.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block extra-head %}
|
{% block js %}
|
||||||
<link href="{% static 'fontawesome_free/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
|
<script src="{% static 'js/all.js' %}"></script>
|
||||||
<link href="{% static 'fontawesome_free/css/solid.css' %}" rel="stylesheet" type="text/css">
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -7,46 +7,44 @@
|
|||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
|
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/flatpickr.css' %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block preload_js %}
|
{% block preload_js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||||
<script src="{% static 'js/easymde.min.js' %}"></script>
|
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
<script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise--->
|
||||||
<script src="{% static 'js/interaction.js' %}"></script>
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
|
<script src="{% static 'js/modal.js' %}"></script>
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
|
||||||
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
|
|
||||||
|
{% include 'partials/datetime-fix.html' %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
|
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
dur = matches ? 0 : 500;
|
dur = matches ? 0 : 500;
|
||||||
{% if object.pk %}
|
{% if not object.pk and not form.errors %}
|
||||||
// Editing
|
$('.form-hws').slideUp(dur, function () {
|
||||||
{% if not object.is_rig %}
|
$('.form-is_rig').slideUp(dur);
|
||||||
|
});
|
||||||
|
{% elif not object.pk and form.errors %}
|
||||||
|
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
|
||||||
$('.form-is_rig').hide();
|
$('.form-is_rig').hide();
|
||||||
{% endif %}
|
}
|
||||||
//Creation
|
{% endif %}
|
||||||
{% else %}
|
{% if not object.pk %}
|
||||||
// If there were errors, apply the previous Rig/not-Rig selection
|
|
||||||
{% if form.errors %}
|
|
||||||
$('.form-hws').show();
|
|
||||||
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
|
|
||||||
$('.form-is_rig').hide();
|
|
||||||
}
|
|
||||||
{% else %}
|
|
||||||
//Initial hide
|
|
||||||
$('.form-hws').slideUp(dur);
|
|
||||||
{% endif %}
|
|
||||||
//Button handling
|
|
||||||
$('#is_rig-selector button').on('click', function () {
|
$('#is_rig-selector button').on('click', function () {
|
||||||
$('.form-non_rig').slideDown(dur); //Non rig stuff also needed for rig, so always slide down
|
$('.form-non_rig').slideDown(dur);
|
||||||
if ($(this).data('is_rig') === 1) {
|
if ($(this).data('is_rig') === 1) {
|
||||||
$('#{{form.is_rig.auto_id}}').prop('checked', true);
|
$('#{{form.is_rig.auto_id}}').prop('checked', true);
|
||||||
if ($('.form-non_rig').is(':hidden')) {
|
if ($('.form-non_rig').is(':hidden')) {
|
||||||
@@ -56,6 +54,7 @@
|
|||||||
}
|
}
|
||||||
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
|
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
$('#{{form.is_rig.auto_id}}').prop('checked', false);
|
$('#{{form.is_rig.auto_id}}').prop('checked', false);
|
||||||
$('.form-is_rig').slideUp(dur);
|
$('.form-is_rig').slideUp(dur);
|
||||||
}
|
}
|
||||||
@@ -63,26 +62,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
});
|
});
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
setupMDE('#id_description');
|
|
||||||
setupMDE('#id_notes');
|
|
||||||
setupMDE('#item_description');
|
|
||||||
|
|
||||||
$('#itemModal').on('shown.bs.modal', function (e) {
|
|
||||||
$('#item_description').data('mde_editor').value(
|
|
||||||
$('#item_description').val()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
|
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
|
||||||
});
|
});
|
||||||
$(function () {
|
$(function () {
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
.form-hws {
|
||||||
|
display: inherit !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</noscript>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'partials/item_modal.html' %}
|
{% include 'item_modal.html' %}
|
||||||
<form class="itemised_form" role="form" method="POST">
|
<form class="itemised_form" role="form" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -106,10 +102,6 @@
|
|||||||
title="Things that aren't service-based, like training, meetings and site visits.">
|
title="Things that aren't service-based, like training, meetings and site visits.">
|
||||||
<button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button>
|
<button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button>
|
||||||
</span>
|
</span>
|
||||||
<span data-toggle="tooltip"
|
|
||||||
title="Record equipment hired in from other companies">
|
|
||||||
<a href="{% url 'subhire_create' %}" class="btn bg-warning w-25">Subhire</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +118,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="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
|
<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' %}">
|
||||||
{% 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 %}
|
||||||
@@ -153,7 +145,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="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
|
<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' %}" >
|
||||||
{% 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 %}
|
||||||
@@ -182,7 +174,7 @@
|
|||||||
<label for="{{ form.description.id_for_label }}"
|
<label for="{{ form.description.id_for_label }}"
|
||||||
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-8">
|
||||||
{% render_field form.description class+="form-control" %}
|
{% render_field form.description class+="form-control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +203,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="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
<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' %}">
|
||||||
{% 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 %}
|
||||||
@@ -281,8 +273,10 @@
|
|||||||
</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">
|
||||||
{{ form.dry_hire.label }} {% render_field form.dry_hire %}
|
{% render_field form.dry_hire %}{{ form.dry_hire.label }}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,7 +298,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="px-0 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="form-control 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 %}
|
||||||
@@ -318,7 +312,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="px-0 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="form-control 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 %}
|
||||||
@@ -338,7 +332,7 @@
|
|||||||
|
|
||||||
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
||||||
<label for="{{ form.purchase_order.id_for_label }}"
|
<label for="{{ form.purchase_order.id_for_label }}"
|
||||||
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
{% render_field form.purchase_order class+="form-control" %}
|
{% render_field form.purchase_order class+="form-control" %}
|
||||||
@@ -357,10 +351,10 @@
|
|||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
|
<div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
|
||||||
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
|
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
|
||||||
{% render_field form.notes class+="form-control md-enabled" %}
|
{% render_field form.notes class+="form-control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'partials/item_table.html' %}
|
{% include 'item_table.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,129 @@
|
|||||||
{% extends 'base_print.xml' %}
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE document SYSTEM "rml.dtd">
|
||||||
|
|
||||||
{% block content %}
|
<document filename="{{filename}}">
|
||||||
{% include "event_print_page.xml" %}
|
<docinit>
|
||||||
{% endblock %}
|
<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"/>
|
||||||
|
</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>
|
||||||
|
</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,17 +1,19 @@
|
|||||||
{% 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 }}'</h1>
|
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></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>
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<keepInFrame maxHeight="500" onOverflow="shrink">
|
<keepInFrame>
|
||||||
{{ object.description|default_if_none:""|markdown:"rml" }}
|
<para style="style.event_description">
|
||||||
|
{{ object.description|default_if_none:""|linebreaksxml }}
|
||||||
|
</para>
|
||||||
</keepInFrame>
|
</keepInFrame>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -178,36 +180,39 @@
|
|||||||
{% for item in object.items.all %}
|
{% for item in object.items.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<para><b>{{ item.name }}</b></para>
|
<para>{{ item.name }}
|
||||||
{% if item.description %}
|
{% if item.description %}
|
||||||
{{ item.description|markdown:"rml" }}
|
</para>
|
||||||
{% endif %}
|
<para style="item_description">
|
||||||
|
<em>{{ item.description|linebreaksxml }}</em>
|
||||||
|
</para>
|
||||||
|
<para>
|
||||||
|
{% endif %}
|
||||||
|
</para>
|
||||||
</td>
|
</td>
|
||||||
<td>£{{ item.cost|floatformat:2 }}</td>
|
<td>£ {{ item.cost|floatformat:2 }}</td>
|
||||||
<td>{{ item.quantity }}</td>
|
<td>{{ item.quantity }}</td>
|
||||||
<td>£{{ item.total_cost|floatformat:2 }}</td>
|
<td>£ {{ item.total_cost|floatformat:2 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</blockTable>
|
</blockTable>
|
||||||
<keepTogether>
|
<keepTogether>
|
||||||
<blockTable style="totalTable" colWidths="300,115,80">
|
<blockTable style="totalTable" colWidths="300,115,80">
|
||||||
{% if object.vat > 0 %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% if quote %}VAT Registration Number: 170734807</td>
|
<td>{% if quote %}VAT Registration Number: 170734807{% endif %}</td>
|
||||||
<td>Total (ex. VAT){% endif %}</td>
|
<td>Total (ex. VAT)</td>
|
||||||
<td>£ {{ object.sum_total|floatformat:2 }}</td>
|
<td>£ {{ object.sum_total|floatformat:2 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if quote %}
|
{% if quote %}
|
||||||
<para>This quote is valid for 30 days unless otherwise arranged.</para>
|
<para>
|
||||||
|
This quote is valid for 30 days unless otherwise arranged.
|
||||||
|
</para>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if object.vat > 0 %}
|
|
||||||
<td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td>
|
<td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td>
|
||||||
<td>£{{ object.vat|floatformat:2 }}</td>
|
<td>£ {{ object.vat|floatformat:2 }}</td>
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -219,7 +224,7 @@
|
|||||||
</td>
|
</td>
|
||||||
{% if invoice %}
|
{% if invoice %}
|
||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td>£{{ object.total|floatformat:2 }}</td>
|
<td>£ {{ object.total|floatformat:2 }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>
|
<td>
|
||||||
<para>
|
<para>
|
||||||
@@ -228,7 +233,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<para>
|
<para>
|
||||||
<b>£{{ object.total|floatformat:2 }}</b>
|
<b>£ {{ object.total|floatformat:2 }}</b>
|
||||||
</para>
|
</para>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -262,7 +267,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ payment.get_method_display }}</td>
|
<td>{{ payment.get_method_display }}</td>
|
||||||
<td>{{ payment.date }}</td>
|
<td>{{ payment.date }}</td>
|
||||||
<td>£{{ payment.amount|floatformat:2 }}</td>
|
<td>£ {{ payment.amount|floatformat:2 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</blockTable>
|
</blockTable>
|
||||||
@@ -270,18 +275,18 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>Payment Total</td>
|
<td>Payment Total</td>
|
||||||
<td>£{{ object.invoice.payment_total|floatformat:2 }}</td>
|
<td>£ {{ object.invoice.payment_total|floatformat:2 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<para>
|
<para>
|
||||||
<b>Balance</b> {% if object.vat > 0 %}(ex. VAT){% endif %}
|
<b>Balance</b> (ex. VAT)
|
||||||
</para>
|
</para>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<para>
|
<para>
|
||||||
<b>£{{ object.invoice.balance|floatformat:2 }}</b>
|
<b>£ {{ object.invoice.balance|floatformat:2 }}</b>
|
||||||
</para>
|
</para>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -311,7 +316,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>General Enquires and 24 Hour Emergency Contact: 0115 84 68720</td>
|
<td>General Enquires and 24 Hour Emergency Contact: 0115 84 68720</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% elif object.vat > 0 %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<para>VAT Registration Number: 170734807</para>
|
<para>VAT Registration Number: 170734807</para>
|
||||||
|
|||||||
10
RIGS/templates/event_print_signature.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<blockTable style="signatureTable" colWidths="50,120,60,120,35,110">
|
||||||
|
<tr>
|
||||||
|
<td>Signature</td>
|
||||||
|
<td></td>
|
||||||
|
<td>Print Name</td>
|
||||||
|
<td></td>
|
||||||
|
<td>Date</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</blockTable>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
{% with object=event auth=True %}
|
{% with object=event auth=True %}
|
||||||
{% include 'partials/item_table.html' %}
|
{% include 'item_table.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
|
|
||||||
<p>Hi {{ to_name|default:"there" }},</p>
|
<p>Hi {{ to_name|default:"there" }},</p>
|
||||||
|
|
||||||
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>{{ object.display_id }}
|
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>N{{ object.pk|stringformat:"05d" }}
|
||||||
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{% if object.person %}{{ object.person.name }}{% else %}{{ object.organisation.name }}{% endif %}</b>{% endif %}.</p>
|
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{{ object.person.name }}</b>{% endif %}.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Please find the link below to complete the event booking process.
|
Please find the link below to complete the event booking process.
|
||||||
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
|
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
|
||||||
this
|
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
|
||||||
email on.
|
this
|
||||||
|
email on.
|
||||||
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@
|
|||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="button" align="center">
|
<td class="button" align="center">
|
||||||
<a href="{{ request.scheme }}://{{ request.get_host }}{% url target|default:'event_authorise' object.pk hmac %}">
|
<a href="{{ request.scheme }}://{{ request.get_host }}{% url 'event_authorise' object.pk hmac %}">
|
||||||
Complete Authorisation Form
|
Complete Authorisation Form
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Hi {{ to_name|default:"there" }},
|
Hi {{ to_name|default:"there" }},
|
||||||
|
|
||||||
{{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {% if object.person %}{{ object.person.name }}{% else %}{{ object.organisation.name }}{% endif %}{% endif %}.
|
{{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {{ object.person.name }}{% endif %}.
|
||||||
|
|
||||||
Please find the link below to complete the event booking process.
|
Please find the link below to complete the event booking process.
|
||||||
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
|
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
{% extends 'eventauthorisation.html' %}
|
{% extends 'eventauthorisation.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block authorisation %}
|
{% block authorisation %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
@@ -83,7 +86,7 @@
|
|||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-primary btn-lg" type="submit" {% if preview %}disabled="" data-toggle="tooltip" title="This is only a preview!"{%endif%}>Authorise</button>
|
<button class="btn btn-primary btn-lg" type="submit">Authorise</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
5
RIGS/templates/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.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
|
||||||
@@ -1,33 +1,24 @@
|
|||||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load static %}
|
|
||||||
{% load button from filters %}
|
|
||||||
|
|
||||||
{% block title %}Request Authorisation{% endblock %}
|
{% block title %}Request Authorisation{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<div class="alert alert-warning pb-0">
|
<div class="alert alert-warning">
|
||||||
<h1>Send authorisation request email.</h1>
|
<h1>Send authorisation request email.</h1>
|
||||||
<p>Pressing send will email the address provided. <strong>Please triple check everything before continuing.</strong></p>
|
<p>Pressing send will email the address provided. Please triple check everything before continuing.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info pb-0">
|
<div class="alert alert-info">
|
||||||
{% if object.person.email or object.organisation.email %}
|
|
||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
{% if object.person.email %}
|
|
||||||
<dt>Person Email</dt>
|
<dt>Person Email</dt>
|
||||||
<dd><span id="person-email" class="pr-1">{{ object.person.email }}</span> {% button 'copy' id='#person-email' %}</dd>
|
<dd>{{ object.person.email }}</dd>
|
||||||
{% endif %}
|
|
||||||
{% if object.organisation.email %}
|
|
||||||
<dt>Organisation Email</dt>
|
<dt>Organisation Email</dt>
|
||||||
<dd><span id="org-email" class="pr-1">{{ object.organisation.email }}</span> {% button 'copy' id='#org-email' %}</dd>
|
<dd>{{ object.organisation.email }}</dd>
|
||||||
{% endif %}
|
|
||||||
</dl>
|
</dl>
|
||||||
{% else %}
|
|
||||||
<p>No email addresses saved to the client ಠ_ಠ</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form">
|
<form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -39,33 +30,21 @@
|
|||||||
{% render_field form.email type="email" class+="form-control" %}
|
{% render_field form.email type="email" class+="form-control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-right col-sm-3 offset-sm-9">
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="form-control btn btn-primary">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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 %}
|
||||||
|
|
||||||
{% block footer %}
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<a type="button" target="_blank" href="{% url 'event_authorise_preview' object.pk %}" class="btn btn-info text-nowrap"><span class="fas fa-drafting-compass"></span> Preview</a>
|
|
||||||
<button type="submit" class="form-control btn btn-primary" form="auth-request-form">
|
|
||||||
<span class="fas fa-paper-plane"></span> Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -4,11 +4,10 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table mb-0 table-sm">
|
<table class="table mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Event</th>
|
<th scope="col">Event</th>
|
||||||
<th scope="col">MIC</th>
|
|
||||||
<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>
|
||||||
@@ -17,8 +16,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for event in object_list %}
|
{% for event in object_list %}
|
||||||
<tr id="event_row">
|
<tr id="event_row">
|
||||||
<th scope="row" id="event_number"><a href="{% url 'event_detail' event.pk %}">{{ event }}</a><br><small>{{ event.get_status_display }}</small></th>
|
<th scope="row" id="event_number"><a href="{% url 'event_detail' event.pk %}">{{ event }}</a></th>
|
||||||
<td>{% if event.mic is not None %}<a href="{% url 'profile_detail' event.mic.pk %}">{% else %}<span class="text-danger">{% endif %}{{ event.mic }}{% if event.mic is not None %}</a>{% else %}</span>{%endif%}</td>
|
|
||||||
<!--Dates-->
|
<!--Dates-->
|
||||||
<td id="event_dates">
|
<td id="event_dates">
|
||||||
<span><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></span>
|
<span><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></span>
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table mb-0 table-sm">
|
<table class="table mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Event</th>
|
<th scope="col">Event</th>
|
||||||
{# mmm hax #}
|
{# mmm hax #}
|
||||||
{% if object_list.0 != None %}
|
{% if object_list.0 != None %}
|
||||||
{% for field in object_list.0.fieldz %}
|
{% for field in object_list.0.fields %}
|
||||||
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
|
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
{% for object in object_list %}
|
{% for object in object_list %}
|
||||||
<tr class="{% if object.reviewed_by %}table-success{%endif%}">
|
<tr class="{% if object.reviewed_by %}table-success{%endif%}">
|
||||||
{# General #}
|
{# General #}
|
||||||
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
|
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a></th>
|
||||||
{% for field in object_list.0.fieldz %}
|
{% for field in object_list.0.fields %}
|
||||||
<td>{{ object|get_field:field }}</td>
|
<td>{{ object|get_field:field }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{# Buttons #}
|
{# Buttons #}
|
||||||
@@ -2,88 +2,102 @@
|
|||||||
{% load button from filters %}
|
{% load button from filters %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row py-4">
|
<div class="col-sm-12">
|
||||||
<div class="col-sm-12 text-right px-0">
|
<div class="row justify-content-end py-3">
|
||||||
<div class="btn-group">
|
<div class="col-sm-4 text-right">
|
||||||
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
|
<div class="btn-group btn-page">
|
||||||
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
|
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
|
||||||
<span class="fas fa-times"></span> <span
|
<span class="fas fa-times"></span> <span
|
||||||
class="d-none d-sm-inline">Delete</span>
|
class="d-none d-sm-inline">Delete</span>
|
||||||
</a>
|
|
||||||
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice">
|
|
||||||
<span class="fas fa-ban"></span> <span
|
|
||||||
class="d-none d-sm-inline">Void</span>
|
|
||||||
</a>
|
|
||||||
{% button 'print' url='invoice_print' pk=object.pk %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="row py-4">
|
|
||||||
{% with object.event as object %}
|
|
||||||
<div class="col-sm-6">
|
|
||||||
{% include 'partials/contact_details.html' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
{% include 'partials/event_details.html' %}
|
|
||||||
</div>
|
|
||||||
{% if object.event.internal %}
|
|
||||||
<div class="col-sm-6">
|
|
||||||
{% include 'partials/auth_details.html' %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
<div class="row py-4">
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<div class="card card-default">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="text-right py-3">
|
|
||||||
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
|
|
||||||
class="btn btn-success modal-href"
|
|
||||||
data-target="#{{ form.person.id_for_label }}">
|
|
||||||
<span class="fas fa-plus"></span> Add
|
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice">
|
||||||
|
<span class="fas fa-ban"></span> <span
|
||||||
|
class="d-none d-sm-inline">Void</span>
|
||||||
|
</a>
|
||||||
|
{% button 'print' url='invoice_print' pk=object.pk %}
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Date</th>
|
|
||||||
<th scope="col">Amount</th>
|
|
||||||
<th scope="col">Method</th>
|
|
||||||
<th scope="col"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for payment in object.payment_set.all %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ payment.date }}</th>
|
|
||||||
<td>{{ payment.amount|floatformat:2 }}</td>
|
|
||||||
<td>{{ payment.get_method_display }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"></span></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
<tr>
|
|
||||||
<td class="text-right"><strong>Balance:</strong></td>
|
|
||||||
<td>{{ object.balance|floatformat:2 }}</td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="row">
|
||||||
<div class="card">
|
<div class="col-sm-6">
|
||||||
{% with object.event as object %}
|
<div class="card card-default">
|
||||||
{% include 'partials/item_table.html' %}
|
<div class="card-header">Invoice Details<span class="float-right">
|
||||||
{% endwith %}
|
{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(OUTSTANDING){% endif %}
|
||||||
|
</span></div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if object.event.organisation %}
|
||||||
|
{{ object.event.organisation.name }}<br/>
|
||||||
|
{{ object.event.organisation.address|linebreaksbr }}
|
||||||
|
{% else %}
|
||||||
|
{{ object.event.person.name }}<br/>
|
||||||
|
{{ object.event.person.address|linebreaksbr }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
{% include 'partials/event_details.html' %}
|
||||||
|
</div>
|
||||||
|
{% if object.event.internal %}
|
||||||
|
<div class="col-sm-6">
|
||||||
|
{% include 'partials/auth_details.html' %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-4">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-right py-3">
|
||||||
|
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
|
||||||
|
class="btn btn-success modal-href"
|
||||||
|
data-target="#{{ form.person.id_for_label }}">
|
||||||
|
<span class="fas fa-plus"></span> Add
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">Amount</th>
|
||||||
|
<th scope="col">Method</th>
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for payment in object.payment_set.all %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ payment.date }}</th>
|
||||||
|
<td>{{ payment.amount|floatformat:2 }}</td>
|
||||||
|
<td>{{ payment.get_method_display }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"></span></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-right"><strong>Balance:</strong></td>
|
||||||
|
<td>{{ object.balance|floatformat:2 }}</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="card">
|
||||||
|
{% with object.event as object %}
|
||||||
|
{% include 'item_table.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
{% include 'partials/last_edited.html' with target="invoice_history" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-12 text-right">
|
|
||||||
{% include 'partials/last_edited.html' with target="invoice_history" %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
id="item_name"/>
|
id="item_name"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row" data-toggle="tooltip" title="A detailed description of the kit. MD enabled.">
|
<div class="form-group form-row">
|
||||||
<label for="item_description" class="col-sm-2 col-form-label">Description</label>
|
<label for="item_description" class="col-sm-2 col-form-label">Description</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<textarea type="text" placeholder="Description" class="form-control md-enabled"
|
<textarea type="text" placeholder="Description" class="form-control"
|
||||||
id="item_description" rows="8"></textarea>
|
id="item_description" rows="8"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
{% load markdown_tags %}
|
|
||||||
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
|
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<span class="name">{{ item.name }}</span>
|
<span class="name">{{ item.name }}</span>
|
||||||
<div class="item-description">
|
<div class="item-description">
|
||||||
<em class="description">{{item.description|markdown}}</em>
|
<em class="description">{{item.description|linebreaksbr}}</em>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{% if perms.RIGS.view_event %}
|
{% if perms.RIGS.view_event %}
|
||||||
<td>£<span class="cost">{{item.cost|floatformat:2}}</span></td>
|
<td>£ <span class="cost">{{item.cost|floatformat:2}}</span></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="quantity">{{item.quantity}}</td>
|
<td class="quantity">{{item.quantity}}</td>
|
||||||
{% if perms.RIGS.view_event %}
|
{% if perms.RIGS.view_event %}
|
||||||
<td>£<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
|
<td>£ <span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if edit %}
|
{% if edit %}
|
||||||
<td class="vert-align text-right">
|
<td class="vert-align text-right">
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<button type="button" class="btn btn-success btn-sm item-add"
|
<button type="button" class="btn btn-success btn-sm item-add"
|
||||||
data-toggle="modal"
|
data-toggle="modal"
|
||||||
data-target="#itemModal">
|
data-target="#itemModal">
|
||||||
<span class="fas fa-plus"></span> Add Item
|
<i class="fas fa-plus"></i> Add Item
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -23,17 +23,16 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="item-table-body">
|
<tbody id="item-table-body">
|
||||||
{% for item in object.items.all %}
|
{% for item in object.items.all %}
|
||||||
{% include 'partials/item_row.html' %}
|
{% include 'item_row.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
{% if auth or perms.RIGS.view_event %}
|
{% if auth or perms.RIGS.view_event %}
|
||||||
<tfoot style="font-weight: bold">
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="3" colspan="2"></td>
|
<td rowspan="3" colspan="2"></td>
|
||||||
<td>Total {% if object.vat > 0 or not object.pk %}(ex. VAT){% endif %}</td>
|
<td>Total (ex. VAT)</td>
|
||||||
<td colspan="2">£<span id="sumtotal">{{object.sum_total|default:0|floatformat:2}}</span></td>
|
<td colspan="2">£ <span id="sumtotal">{{object.sum_total|default:0|floatformat:2}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if object.vat > 0 or not object.pk %}
|
|
||||||
<tr>
|
<tr>
|
||||||
{% if not object.pk %}
|
{% if not object.pk %}
|
||||||
<td id="vat-rate" data-rate="{{currentVAT.rate}}">VAT @
|
<td id="vat-rate" data-rate="{{currentVAT.rate}}">VAT @
|
||||||
@@ -42,13 +41,12 @@
|
|||||||
<td id="vat-rate" data-rate="{{object.vat_rate.rate}}">VAT @
|
<td id="vat-rate" data-rate="{{object.vat_rate.rate}}">VAT @
|
||||||
{{object.vat_rate.as_percent|floatformat|default:"TBD"}}%</td>
|
{{object.vat_rate.as_percent|floatformat|default:"TBD"}}%</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td colspan="2">£<span id="vat">{{object.vat|default:0|floatformat:2}}</span></td>
|
<td colspan="2">£ <span id="vat">{{object.vat|default:0|floatformat:2}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td colspan="2">£<span id="total">{{object.total|default:0|floatformat:2}}</span></td>
|
<td colspan="2">£ <span id="total">{{object.total|default:0|floatformat:2}}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
</tfoot>
|
</tfoot>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
@@ -61,9 +59,9 @@
|
|||||||
<em class="description"></em>
|
<em class="description"></em>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>£<span class="cost"></span></td>
|
<td>£ <span class="cost"></span></td>
|
||||||
<td class="quantity"></td>
|
<td class="quantity"></td>
|
||||||
<td>£<span class="sub-total"></span></td>
|
<td>£ <span class="sub-total"></span></td>
|
||||||
{% if edit %}
|
{% if edit %}
|
||||||
<td class="vert-align text-right">
|
<td class="vert-align text-right">
|
||||||
<div class="btn-group" role="group" aria-label="Action buttons">
|
<div class="btn-group" role="group" aria-label="Action buttons">
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{% load get_list from filters %}
|
|
||||||
{% load button from filters %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12 py-2">
|
|
||||||
<form class="form-inline" method="GET">
|
|
||||||
<div class="input-group mx-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text">Start</span>
|
|
||||||
</div>
|
|
||||||
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<div class="input-group mx-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text">End</span>
|
|
||||||
</div>
|
|
||||||
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<div class="input-group mx-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text">Keyword</span>
|
|
||||||
</div>
|
|
||||||
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
|
|
||||||
{% for status in statuses %}
|
|
||||||
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% button 'search' %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
<div class="card card-default
|
<div class="card card-default
|
||||||
{% if event.authorised %}
|
{% if object.authorised %}
|
||||||
border-success
|
card-success
|
||||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||||
border-warning
|
card-warning
|
||||||
{% elif event.auth_request_to %}
|
{% elif event.auth_request_to %}
|
||||||
border-info
|
card-info
|
||||||
{% endif %}
|
{% endif %}
|
||||||
">
|
">
|
||||||
<div class="card-header">Client Authorisation</div>
|
<div class="card-header">Client Authorisation</div>
|
||||||
<div class="card-body row">
|
<div class="card-body row">
|
||||||
<dl class="col-sm-6">
|
<dl class="col-md-6">
|
||||||
<dt>Authorisation Request</dt>
|
<dt>Authorisation Request</dt>
|
||||||
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
|
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
<dt>To</dt>
|
<dt>To</dt>
|
||||||
<dd>{{ object.auth_request_to }}</dd>
|
<dd>{{ object.auth_request_to }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="col-sm-6">
|
<dd class="d-block d-sm-none"> </dd>
|
||||||
<hr class="d-block d-sm-none">
|
<dl class="col-md-6">
|
||||||
<dt>Authorised</dt>
|
<dt>Authorised</dt>
|
||||||
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>
|
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% if event.person %}
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">Contact Details</div>
|
<div class="card-header">Contact Details</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-5">Person</dt>
|
<dt class="col-sm-5">Person</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{ event.person.name }}
|
{% if event.person %}
|
||||||
|
{{ event.person.name }}
|
||||||
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
{% if event.person.email %}
|
|
||||||
<dt class="col-sm-5">Email</dt>
|
<dt class="col-sm-5">Email</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
<span class="overflow-ellipsis">{{ event.person.email }}</span>
|
<span class="overflow-ellipsis">{{ event.person.email }}</span>
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
|
||||||
{% if event.person.phone %}
|
|
||||||
<dt class="col-sm-5">Phone Number</dt>
|
<dt class="col-sm-5">Phone Number</dt>
|
||||||
<dd class="col-sm-7">{{ event.person.phone }}</dd>
|
<dd class="col-sm-7">{{ event.person.phone }}</dd>
|
||||||
{% endif %}
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% if event.organisation %}
|
{% if event.organisation %}
|
||||||
<div class="card">
|
<div class="card mt-3">
|
||||||
<div class="card-header">Organisation Details</div>
|
<div class="card-header">Organisation Details</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@@ -31,10 +29,9 @@
|
|||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{ event.organisation.name }}
|
{{ event.organisation.name }}
|
||||||
</dd>
|
</dd>
|
||||||
{% if event.organisation.phone %}
|
|
||||||
<dt class="col-sm-5">Phone Number</dt>
|
<dt class="col-sm-5">Phone Number</dt>
|
||||||
<dd class="col-sm-7">{{ event.organisation.phone }}</dd>
|
<dd class="col-sm-7">{{ object.organisation.phone }}</dd>
|
||||||
{% endif %}
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,12 +43,15 @@
|
|||||||
<div class="card-header">Event Info</div>
|
<div class="card-header">Event Info</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
{% if event.venue %}
|
|
||||||
<dt class="col-sm-5">Event Venue</dt>
|
<dt class="col-sm-5">Event Venue</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{ event.venue }}
|
{% if object.venue %}
|
||||||
|
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
|
||||||
|
{{ object.venue }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
|
||||||
<dt class="col-sm-5">Status</dt>
|
<dt class="col-sm-5">Status</dt>
|
||||||
<dd class="col-sm-7">{{ event.get_status_display }}</dd>
|
<dd class="col-sm-7">{{ event.get_status_display }}</dd>
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
{% load linkornone from filters %}
|
|
||||||
{% load namewithnotes from filters %}
|
|
||||||
|
|
||||||
{% if object.person %}
|
|
||||||
<div class="card card-default mb-3">
|
|
||||||
<div class="card-header">Person Details</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-6">Person</dt>
|
|
||||||
<dd class="col-sm-6">
|
|
||||||
{% if object.person %}
|
|
||||||
<a href="{% url 'person_detail' object.person.pk %}" class="modal-href">
|
|
||||||
{{ object.person|namewithnotes:'person_detail' }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-sm-6">Email</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
|
|
||||||
<dt class="col-sm-6">Phone Number</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if object.organisation %}
|
|
||||||
<div class="card card-default">
|
|
||||||
<div class="card-header">Organisation Details</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-6">Organisation</dt>
|
|
||||||
<dd class="col-sm-6">
|
|
||||||
{% if object.organisation %}
|
|
||||||
<a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href">
|
|
||||||
{{ object.organisation|namewithnotes:'organisation_detail' }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-sm-6">Email</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd>
|
|
||||||
<dt class="col-sm-6">Phone Number</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
|
|
||||||
<dt class="col-sm-6">Has SU Account</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.organisation.union_account|yesno|capfirst }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{% load namewithnotes from filters %}
|
{% load namewithnotes from filters %}
|
||||||
{% load markdown_tags %}
|
|
||||||
<div class="card card-info">
|
<div class="card card-info">
|
||||||
<div class="card-header">Event Info</div>
|
<div class="card-header">Event Info</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -15,13 +14,21 @@
|
|||||||
{% 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|markdown }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%}
|
{{ object.venue.notes }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%}
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if event.is_rig %}
|
{% if event.is_rig %}
|
||||||
<dt class="col-sm-6">Event MIC</dt>
|
<dt class="col-sm-6">Event MIC</dt>
|
||||||
<dd class="col-sm-6">{% include 'partials/linked_name.html' with profile=event.mic %}</dd>
|
<dd class="col-sm-6">
|
||||||
|
{% if event.mic and perms.RIGS.view_profile %}
|
||||||
|
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
|
||||||
|
{{ event.mic.name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{{ event.mic.name }}
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-6">Status</dt>
|
<dt class="col-sm-6">Status</dt>
|
||||||
@@ -47,7 +54,7 @@
|
|||||||
<dd class="col-sm-12"> </dd>
|
<dd class="col-sm-12"> </dd>
|
||||||
|
|
||||||
<dt class="col-sm-6">Event Description</dt>
|
<dt class="col-sm-6">Event Description</dt>
|
||||||
<dd class="dont-break-out col-sm-12">{{ event.description|markdown }}</dd>
|
<dd class="dont-break-out col-sm-12">{{ event.description|linebreaksbr }}</dd>
|
||||||
|
|
||||||
<dd class="col-sm-12"> </dd>
|
<dd class="col-sm-12"> </dd>
|
||||||
|
|
||||||
@@ -64,7 +71,7 @@
|
|||||||
|
|
||||||
{% if event.dry_hire %}
|
{% if event.dry_hire %}
|
||||||
<dt class="col-sm-6">Checked In By</dt>
|
<dt class="col-sm-6">Checked In By</dt>
|
||||||
<dd class="col-sm-6">{% include 'partials/linked_name.html' with profile=event.checked_in_by %}</dd>
|
<dd class="col-sm-6">{{ object.checked_in_by.name }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if event.is_rig %}
|
{% if event.is_rig %}
|
||||||
|
|||||||
@@ -6,17 +6,13 @@
|
|||||||
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
|
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
|
||||||
{% elif event.authorised %}
|
{% elif event.authorised %}
|
||||||
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
|
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
|
||||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
|
||||||
<span class="badge badge-warning"> Authorisation: Issue <span class="fas fa-exclamation-circle"></span></span>
|
|
||||||
{% elif event.auth_request_to %}
|
|
||||||
<span class="badge badge-info"> Authorisation: Sent <span class="fas fa-paper-plane"></span></span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
|
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not event.dry_hire %}
|
{% if not event.dry_hire %}
|
||||||
{% if event.riskassessment %}
|
{% if event.riskassessment %}
|
||||||
<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>
|
<span class="badge badge-success">RA: <span class="fas fa-check"></span>{%if event.riskassessment.reviewed_by%}<span class="fas fa-check"></span>{%endif%}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
|
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% 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>
|
||||||
@@ -12,37 +11,51 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
<tr class="{% if event.cancelled %}
|
||||||
|
table-secondary
|
||||||
|
{% elif not event.is_rig %}
|
||||||
|
table-info
|
||||||
|
{% elif not event.mic %}
|
||||||
|
table-danger
|
||||||
|
{% elif event.confirmed and event.authorised %}
|
||||||
|
{% if event.dry_hire or event.riskassessment %}
|
||||||
|
table-success
|
||||||
|
{% else %}
|
||||||
|
table-warning
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
table-warning
|
||||||
|
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||||
<!---Number-->
|
<!---Number-->
|
||||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||||
<!--Dates & Times-->
|
<!--Dates & Times-->
|
||||||
<td id="event_dates" style="text-align: justify;">
|
<td id="event_dates">
|
||||||
{% if not event.cancelled %}
|
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}</strong>
|
||||||
{% 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" }}
|
|
||||||
{% if event.has_start_time %}
|
{% if event.has_start_time %}
|
||||||
{{ event.start_time|date:"H:i" }}
|
{{ event.start_time|date:"H:i" }}
|
||||||
{% endif %}</strong>
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% if event.end_date %}
|
{% if event.end_date %}
|
||||||
<br>
|
<br>
|
||||||
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
|
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}</strong>{% endif %}
|
||||||
{% if event.has_end_time %}
|
{% if event.has_end_time %}
|
||||||
{{ event.end_time|date:"H:i" }}
|
{{ event.end_time|date:"H:i" }}
|
||||||
{% endif %}</strong>
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not event.cancelled %}
|
||||||
|
{% if event.meet_at %}
|
||||||
|
<br><span>Crew meet: <strong>{{ event.meet_at|date:"H:i" }}</strong> {{ event.meet_at|date:"(d/m/Y)" }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.access_at %}
|
||||||
|
<br><span>Access at: <strong>{{ event.access_at|date:"H:i" }}</strong> {{ event.access_at|date:"(d/m/Y)" }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<!---Details-->
|
<!---Details-->
|
||||||
<td id="event_details" class="w-100">
|
<td id="event_details" class="w-100">
|
||||||
<h4>
|
<h4>
|
||||||
<a href="{{event.get_absolute_url}}">
|
<a href="{% url 'event_detail' event.pk %}">
|
||||||
{{ event.name }}
|
{{ event.name }}
|
||||||
</a>
|
</a>
|
||||||
{% if event.venue %}
|
{% if event.venue %}
|
||||||
@@ -54,14 +67,14 @@
|
|||||||
</h4>
|
</h4>
|
||||||
{% if event.is_rig and not event.cancelled %}
|
{% if event.is_rig and not event.cancelled %}
|
||||||
<h5>
|
<h5>
|
||||||
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
|
{{ event.person.name }}
|
||||||
{% if event.organisation %}
|
{% if event.organisation %}
|
||||||
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
|
for {{ event.organisation.name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not event.cancelled and event.description %}
|
{% if not event.cancelled and event.description %}
|
||||||
<p>{{ event.description|markdown }}</p>
|
<p>{{ event.description|linebreaksbr }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'partials/event_status.html' %}
|
{% include 'partials/event_status.html' %}
|
||||||
</td>
|
</td>
|
||||||
@@ -77,7 +90,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif event.is_rig %}
|
{% elif event.is_rig %}
|
||||||
<span class="fas fa-user-slash"></span>
|
<span class="fas fa-exclamation"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{% if profile and perms.RIGS.view_profile %}
|
|
||||||
<a href="{% url 'profile_detail' profile.pk %}" class="modal-href">
|
|
||||||
{{ profile.name }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
{{ profile.name }}
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
{% load linked_name from filters %}
|
|
||||||
{% load markdown_tags %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table mb-0" id="event_table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">#</th>
|
|
||||||
<th scope="col">Dates & Times</th>
|
|
||||||
<th scope="col">Hire Details</th>
|
|
||||||
<th scope="col">Associated Event(s)</th>
|
|
||||||
{% if perms.RIGS.subhire_finance %}
|
|
||||||
<th scope="col">Insurance Value</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for event in events %}
|
|
||||||
<tr {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
|
||||||
<!---Number-->
|
|
||||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
|
||||||
<!--Dates & Times-->
|
|
||||||
<td id="event_dates" style="text-align: justify;">
|
|
||||||
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
|
|
||||||
{% if event.has_start_time %}
|
|
||||||
{{ event.start_time|date:"H:i" }}
|
|
||||||
{% endif %}</strong>
|
|
||||||
</span>
|
|
||||||
{% if event.end_date %}
|
|
||||||
<br>
|
|
||||||
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
|
|
||||||
{% if event.has_end_time %}
|
|
||||||
{{ event.end_time|date:"H:i" }}
|
|
||||||
{% endif %}</strong>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<!---Details-->
|
|
||||||
<td id="event_details" class="w-100">
|
|
||||||
<h4>
|
|
||||||
<a href="{{event.get_absolute_url}}">
|
|
||||||
{{ event.name }}
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<h5>
|
|
||||||
Primary Contact: {{ event.person|linked_name }}
|
|
||||||
{% if event.organisation %}
|
|
||||||
({{ event.organisation|linked_name }})
|
|
||||||
{% endif %}
|
|
||||||
</h5>
|
|
||||||
{% if not event.cancelled and event.description %}
|
|
||||||
<p>{{ event.description|markdown }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% include 'partials/event_status.html' %}
|
|
||||||
</td>
|
|
||||||
<td class="p-0 text-nowrap">
|
|
||||||
<ul class="list-group">
|
|
||||||
{% for event in event.events.all %}
|
|
||||||
<li class="list-group-item"><a href="{{event.get_absolute_url}}">{{ event }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
{% if perms.RIGS.subhire_finance %}
|
|
||||||
<td id="insurance_value" class="text-nowrap">
|
|
||||||
£{{ event.insurance_value }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr class="bg-warning">
|
|
||||||
<td colspan="4">No events found</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td>Total Value:</td>
|
|
||||||
<td>£{{ total_value }}</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
{% load filters %}
|
{% block title %}Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}{% endblock %}
|
||||||
|
{% load help_text from filters %}
|
||||||
|
{% load yesnoi from filters %}
|
||||||
|
{% load linkornone from filters %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row py-3">
|
<div class="row py-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
<h3>Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}</h3>
|
||||||
<div class="card card-default mb-3">
|
<div class="card card-default mb-3">
|
||||||
<div class="card-header">General</div>
|
<div class="card-header">General</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -45,13 +49,13 @@
|
|||||||
</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:object.event.mic }}
|
{{ object.power_mic.name|default:'None' }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
|
<dt class="col-sm-6">{{ object|help_text:'generators' }}</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
{{ object.outside|yesnoi:'invert' }}
|
{{ object.outside|yesnoi:'invert' }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-6">{{ object|help_text:'generators' }}</dt>
|
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
{{ object.generators|yesnoi:'invert' }}
|
{{ object.generators|yesnoi:'invert' }}
|
||||||
</dd>
|
</dd>
|
||||||
@@ -93,69 +97,62 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="card card-default mb-3">
|
||||||
<div class="col-lg-6 col-12">
|
<div class="card-header">Site Details</div>
|
||||||
<div class="card card-default mb-3">
|
<div class="card-body">
|
||||||
<div class="card-header">Site Details</div>
|
<dl class="row">
|
||||||
<div class="card-body">
|
<dt class="col-sm-6">{{ object|help_text:'known_venue' }}</dt>
|
||||||
<dl class="row">
|
<dd class="col-sm-6">
|
||||||
<dt class="col-10">{{ object|help_text:'known_venue' }}</dt>
|
{{ object.known_venue|yesnoi:'invert' }}
|
||||||
<dd class="col-2">
|
</dd>
|
||||||
{{ object.known_venue|yesnoi:'invert' }}
|
<dt class="col-sm-6">{{ object|help_text:'safe_loading'|safe }}</dt>
|
||||||
</dd>
|
<dd class="col-sm-6">
|
||||||
<dt class="col-10">{{ object|help_text:'safe_loading'|safe }}</dt>
|
{{ object.safe_loading|yesnoi:'invert' }}
|
||||||
<dd class="col-2">
|
</dd>
|
||||||
{{ object.safe_loading|yesnoi:'invert' }}
|
<dt class="col-sm-6">{{ object|help_text:'safe_storage' }}</dt>
|
||||||
</dd>
|
<dd class="col-sm-6">
|
||||||
<dt class="col-10">{{ object|help_text:'safe_storage' }}</dt>
|
{{ object.safe_storage|yesnoi:'invert' }}
|
||||||
<dd class="col-2">
|
</dd>
|
||||||
{{ object.safe_storage|yesnoi:'invert' }}
|
<dt class="col-sm-6">{{ object|help_text:'area_outside_of_control' }}</dt>
|
||||||
</dd>
|
<dd class="col-sm-6">
|
||||||
<dt class="col-10">{{ object|help_text:'area_outside_of_control' }}</dt>
|
{{ object.area_outside_of_control|yesnoi:'invert' }}
|
||||||
<dd class="col-2">
|
</dd>
|
||||||
{{ object.area_outside_of_control|yesnoi:'invert' }}
|
<dt class="col-sm-6">{{ object|help_text:'barrier_required' }}</dt>
|
||||||
</dd>
|
<dd class="col-sm-6">
|
||||||
<dt class="col-10">{{ object|help_text:'barrier_required' }}</dt>
|
{{ object.barrier_required|yesnoi:'invert' }}
|
||||||
<dd class="col-2">
|
</dd>
|
||||||
{{ object.barrier_required|yesnoi:'invert' }}
|
<dt class="col-sm-6">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
|
||||||
</dd>
|
<dd class="col-sm-6">
|
||||||
<dt class="col-10">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
|
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
|
||||||
<dd class="col-2">
|
</dd>
|
||||||
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
|
</dl>
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 col-12">
|
</div>
|
||||||
<div class="card card-default mb-3">
|
<div class="card card-default mb-3">
|
||||||
<div class="card-header">Structures</div>
|
<div class="card-header">Structures</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-10">{{ object|help_text:'special_structures' }}</dt>
|
<dt class="col-sm-6">{{ object|help_text:'special_structures' }}</dt>
|
||||||
<dd class="col-2">
|
<dd class="col-sm-6">
|
||||||
{{ object.special_structures|yesnoi:'invert' }}
|
{{ object.special_structures|yesnoi:'invert' }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-10">{{ object|help_text:'suspended_structures' }}</dt>
|
<dt class="col-sm-6">{{ object|help_text:'suspended_structures' }}</dt>
|
||||||
<dd class="col-2">
|
<dd class="col-sm-6">
|
||||||
{{ object.suspended_structures|yesnoi:'invert' }}
|
{{ object.suspended_structures|yesnoi:'invert' }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
|
<dt class="col-sm-6">{{ object|help_text:'persons_responsible_structures' }}</dt>
|
||||||
<dd class="col-12">
|
<dd class="col-sm-6">
|
||||||
{{ object.persons_responsible_structures|default:'N/A'|linebreaks }}
|
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
|
<dt class="col-6">{{ object|help_text:'rigging_plan'|safe }}</dt>
|
||||||
<dd class="col-12">
|
<dd class="col-6">
|
||||||
{{ object.rigging_plan|linkornone|default:'N/A' }}
|
{{ object.rigging_plan|linkornone }}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
@@ -5,17 +5,26 @@
|
|||||||
{% load nice_errors from filters %}
|
{% load nice_errors from filters %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block preload_js %}
|
{% block preload_js %}
|
||||||
<script src="{% static 'js/selects.js' %}" async></script>
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/bootstrap-select.js' %}"></script>
|
||||||
|
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise--->
|
||||||
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
|
<script src="{% static 'js/modal.js' %}"></script>
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
|
||||||
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function parseBool(str) {
|
function parseBool(str) {
|
||||||
return str.toLowerCase() == 'true';
|
return str.toLowerCase() == 'true';
|
||||||
@@ -98,9 +107,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="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="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||||
{% if power_mic %}
|
{% if object.power_mic %}
|
||||||
<option value="{{form.power_mic.value}}" selected="selected">{{ power_mic }}</option>
|
<option value="{{object.power_mic.pk}}" selected="selected">{{ object.power_mic.name }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
|
||||||
|
|
||||||
{% load markdown_tags %}
|
|
||||||
{% load button from filters %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row my-3 py-3">
|
|
||||||
<div class="col-sm-12 text-right mb-2">
|
|
||||||
{% button 'edit' 'subhire_update' object.pk %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
{% include 'partials/contact_details.html' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card card-default">
|
|
||||||
<div class="card-header">Hire Details</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-6">Name</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.name }}</dd>
|
|
||||||
<dt class="col-sm-6">Event Starts</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.start_date|date:"D d M Y" }} {{ object.start_time|date:"H:i" }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-6">Event Ends</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.end_date|date:"D d M Y" }} {{ object.end_time|date:"H:i" }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-6">Status</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.get_status_display }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-6">PO</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.po }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mt-2">
|
|
||||||
<div class="card card-default">
|
|
||||||
<div class="card-header">Equipment Information</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-6">Description</dt>
|
|
||||||
<dd class="col-sm-6">{{ object.description }}</dd>
|
|
||||||
{% if perms.RIGS.subhire_finance %}
|
|
||||||
<dt class="col-sm-6">Insurance Value</dt>
|
|
||||||
<dd class="col-sm-6">£{{ object.insurance_value }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
<dt class="col-sm-6">Quote</dt>
|
|
||||||
<dd class="col-sm-6"><a href="{{ object.quote }}">View</a></dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 mt-2">
|
|
||||||
<div class="card card-default">
|
|
||||||
<div class="card-header">Associated Event(s)</div>
|
|
||||||
{% with object.events.all as events %}
|
|
||||||
{% include 'partials/event_table.html' %}
|
|
||||||
{%endwith%}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
|
||||||
<div class="col-sm-12 text-right">
|
|
||||||
{% include 'partials/last_edited.html' with target="event_history" %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% if request.is_ajax %}
|
|
||||||
{% block footer %}
|
|
||||||
{% if perms.RIGS.view_event %}
|
|
||||||
{% include 'partials/last_edited.html' with target="event_history" %}
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'subhire_detail' object.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
|
|
||||||
{% endblock %}
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
|
||||||
|
|
||||||
{% load widget_tweaks %}
|
|
||||||
{% load static %}
|
|
||||||
{% load multiply from filters %}
|
|
||||||
{% 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>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready(function () {
|
|
||||||
setupMDE('#id_description');
|
|
||||||
});
|
|
||||||
$(function () {
|
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<form class="row" role="form" method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="col-12">
|
|
||||||
{% include 'form_errors.html' %}
|
|
||||||
</div>
|
|
||||||
{# Contact details #}
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Contact Details</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group" data-toggle="tooltip">
|
|
||||||
<label for="{{ form.person.id_for_label }}">Primary Contact</label>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-9">
|
|
||||||
<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 %}
|
|
||||||
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 align-right">
|
|
||||||
<div class="btn-group">
|
|
||||||
<a href="{% url 'person_create' %}" class="btn btn-success modal-href"
|
|
||||||
data-target="#{{ form.person.id_for_label }}">
|
|
||||||
<span class="fas fa-plus"></span>
|
|
||||||
</a>
|
|
||||||
<a {% if form.person.value %}href="{% url 'person_update' form.person.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.person.id_for_label }}-update" data-target="#{{ form.person.id_for_label }}">
|
|
||||||
<span class="fas fa-user-edit"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.organisation.id_for_label }}">Hire Company</label>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-9">
|
|
||||||
<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 %}
|
|
||||||
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 align-right">
|
|
||||||
<div class="btn-group">
|
|
||||||
<a href="{% url 'organisation_create' %}" class="btn btn-success modal-href"
|
|
||||||
data-target="#{{ form.organisation.id_for_label }}">
|
|
||||||
<span class="fas fa-plus"></span>
|
|
||||||
</a>
|
|
||||||
<a {% if form.organisation.value %}href="{% url 'organisation_update' form.organisation.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.organisation.id_for_label }}-update" data-target="#{{ form.organisation.id_for_label }}">
|
|
||||||
<span class="fas fa-edit"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Associated Event(s)</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<select multiple name="events" id="events_id" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='event' %}">
|
|
||||||
{% if object.events.count > 0 %}
|
|
||||||
{% for event in object.events.all %}
|
|
||||||
<option value="{{event.id}}" selected>{{ event }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{# Event details #}
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<div class="card card-default">
|
|
||||||
<div class="card-header">Hire Details</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group" data-toggle="tooltip" title="Name of the event, displays on rigboard and on paperwork">
|
|
||||||
<label for="{{ form.name.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
{% render_field form.name class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.start_date.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
|
|
||||||
{% render_field form.start_date class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="Start time of event, can be left blank">
|
|
||||||
{% render_field form.start_time class+="form-control" step="60" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.end_date.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
|
|
||||||
{% render_field form.end_date class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="End time of event, leave blank if unknown">
|
|
||||||
{% render_field form.end_time class+="form-control" step="60" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" data-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
|
|
||||||
<label for="{{ form.status.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">{{ form.status.label }}</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
{% render_field form.status class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.purchase_order.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
{% render_field form.purchase_order class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-2">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Equipment Information</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.description.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
|
|
||||||
<div class="col-sm-12">
|
|
||||||
{% render_field form.description class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.insurance_value.id_for_label }}"
|
|
||||||
class="col-sm-6 col-form-label">{{ form.insurance_value.label }}</label>
|
|
||||||
<div class="col-sm-8 input-group">
|
|
||||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
|
||||||
{% render_field form.insurance_value class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
<div class="border border-info p-2 rounded mt-1 font-weight-bold" style="border-width: thin thin thin thick !important;">
|
|
||||||
If this value is greater than £50,000 then please email productions@nottinghamtec.co.uk in addition to complete the additional insurance requirements
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.quote.id_for_label }}" class="col-sm-6 col-form-label">{{ form.quote.label }} (TEC SharePoint link)</label>
|
|
||||||
<div class="col-sm-12">{% render_field form.quote class+="form-control" %}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 text-right my-3">
|
|
||||||
{% button 'submit' %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||