mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-17 12:09:41 +00:00
Compare commits
42 Commits
combine-pr
...
63a2f6d47b
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a2f6d47b | |||
| 8393e85b74 | |||
| 311c02d554 | |||
| e100f5a1d4 | |||
| eb07990f4c | |||
| 7b7c1b86de | |||
| 2b8945c513 | |||
| eb3638b93a | |||
| 86c033ba97 | |||
| 52fd662340 | |||
| 9818ed995f | |||
| 5178614d71 | |||
|
1660f51e55
|
|||
| a7bf990666 | |||
|
9feea56211
|
|||
|
951227e68b
|
|||
|
a4a28a6130
|
|||
|
e3d8cf8978
|
|||
|
6e8779c81b
|
|||
|
e0da6a3120
|
|||
|
0c80ef1b72
|
|||
|
0f127d8ca4
|
|||
|
626779ef25
|
|||
| fa1dc31639 | |||
| d69543e309 | |||
|
04ec728972
|
|||
|
|
a24e6d4495 | ||
|
|
fa5792914a | ||
|
bede8b4176
|
|||
| 0117091f3e | |||
|
8cade512d1
|
|||
|
418219940b
|
|||
|
37101d3340
|
|||
| de4bed92a4 | |||
|
|
3767923175 | ||
|
|
1d77cf95d3 | ||
|
|
1f21d0b265 | ||
| 7846a6d31e | |||
| d28b73a0b8 | |||
| 948a41f43a | |||
|
4449efcced
|
|||
|
8b0cd13159
|
151
.github/workflows/combine-prs.yml
vendored
Normal file
151
.github/workflows/combine-prs.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
name: 'Combine PRs'
|
||||||
|
|
||||||
|
# Controls when the action will run - in this case triggered manually
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branchPrefix:
|
||||||
|
description: 'Branch prefix to find combinable PRs based on'
|
||||||
|
required: true
|
||||||
|
default: 'dependabot'
|
||||||
|
mustBeGreen:
|
||||||
|
description: 'Only combine PRs that are green (status is success)'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
combineBranchName:
|
||||||
|
description: 'Name of the branch to combine PRs into'
|
||||||
|
required: true
|
||||||
|
default: 'combine-prs-branch'
|
||||||
|
ignoreLabel:
|
||||||
|
description: 'Exclude PRs with this label'
|
||||||
|
required: true
|
||||||
|
default: 'nocombine'
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
# This workflow contains a single job called "combine-prs"
|
||||||
|
combine-prs:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v6
|
||||||
|
id: create-combined-pr
|
||||||
|
name: Create Combined PR
|
||||||
|
with:
|
||||||
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
script: |
|
||||||
|
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo
|
||||||
|
});
|
||||||
|
let branchesAndPRStrings = [];
|
||||||
|
let baseBranch = null;
|
||||||
|
let baseBranchSHA = null;
|
||||||
|
for (const pull of pulls) {
|
||||||
|
const branch = pull['head']['ref'];
|
||||||
|
console.log('Pull for branch: ' + branch);
|
||||||
|
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
|
||||||
|
console.log('Branch matched prefix: ' + branch);
|
||||||
|
let statusOK = true;
|
||||||
|
if(${{ github.event.inputs.mustBeGreen }}) {
|
||||||
|
console.log('Checking green status: ' + branch);
|
||||||
|
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number:$pull_number) {
|
||||||
|
commits(last: 1) {
|
||||||
|
nodes {
|
||||||
|
commit {
|
||||||
|
statusCheckRollup {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const vars = {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: pull['number']
|
||||||
|
};
|
||||||
|
const result = await github.graphql(stateQuery, vars);
|
||||||
|
const [{ commit }] = result.repository.pullRequest.commits.nodes;
|
||||||
|
const state = commit.statusCheckRollup.state
|
||||||
|
console.log('Validating status: ' + state);
|
||||||
|
if(state != 'SUCCESS') {
|
||||||
|
console.log('Discarding ' + branch + ' with status ' + state);
|
||||||
|
statusOK = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Checking labels: ' + branch);
|
||||||
|
const labels = pull['labels'];
|
||||||
|
for(const label of labels) {
|
||||||
|
const labelName = label['name'];
|
||||||
|
console.log('Checking label: ' + labelName);
|
||||||
|
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
|
||||||
|
console.log('Discarding ' + branch + ' with label ' + labelName);
|
||||||
|
statusOK = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusOK) {
|
||||||
|
console.log('Adding branch to array: ' + branch);
|
||||||
|
const prString = '#' + pull['number'] + ' ' + pull['title'];
|
||||||
|
branchesAndPRStrings.push({ branch, prString });
|
||||||
|
baseBranch = pull['base']['ref'];
|
||||||
|
baseBranchSHA = pull['base']['sha'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (branchesAndPRStrings.length == 0) {
|
||||||
|
core.setFailed('No PRs/branches matched criteria');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await github.rest.git.createRef({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
sha: baseBranchSHA
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let combinedPRs = [];
|
||||||
|
let mergeFailedPRs = [];
|
||||||
|
for(const { branch, prString } of branchesAndPRStrings) {
|
||||||
|
try {
|
||||||
|
await github.rest.repos.merge({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
base: '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
head: branch,
|
||||||
|
});
|
||||||
|
console.log('Merged branch ' + branch);
|
||||||
|
combinedPRs.push(prString);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to merge branch ' + branch);
|
||||||
|
mergeFailedPRs.push(prString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Creating combined PR');
|
||||||
|
const combinedPRsString = combinedPRs.join('\n');
|
||||||
|
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
|
||||||
|
if(mergeFailedPRs.length > 0) {
|
||||||
|
const mergeFailedPRsString = mergeFailedPRs.join('\n');
|
||||||
|
body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
|
||||||
|
}
|
||||||
|
await github.rest.pulls.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: 'Combined PR',
|
||||||
|
head: '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
base: baseBranch,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
24
.github/workflows/django.yml
vendored
24
.github/workflows/django.yml
vendored
@@ -13,26 +13,20 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9.1
|
python-version: 3.9
|
||||||
- uses: actions/cache@v2
|
cache: 'pipenv'
|
||||||
id: pcache
|
|
||||||
with:
|
|
||||||
path: ~/.local/share/virtualenvs
|
|
||||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pipenv-
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip pipenv
|
python3 -m pip install --upgrade pip pipenv
|
||||||
pipenv install -d
|
pipenv install -d
|
||||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||||
- name: Cache Static Files
|
- name: Cache Static Files
|
||||||
id: static-cache
|
id: static-cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: 'pipeline/built_assets'
|
path: 'pipeline/built_assets'
|
||||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||||
@@ -43,9 +37,9 @@ jobs:
|
|||||||
- name: Basic Checks
|
- name: Basic Checks
|
||||||
run: |
|
run: |
|
||||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
pipenv run pycodestyle . --exclude=migrations,node_modules
|
||||||
pipenv run python manage.py check
|
pipenv run python3 manage.py check
|
||||||
pipenv run python manage.py makemigrations --check --dry-run
|
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||||
pipenv run python manage.py collectstatic --noinput
|
pipenv run python3 manage.py collectstatic --noinput
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: pipenv run pytest -n auto -vv --cov
|
run: pipenv run pytest -n auto -vv --cov
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
|
|||||||
30
Pipfile
30
Pipfile
@@ -11,7 +11,6 @@ asgiref = "~=3.3.1"
|
|||||||
beautifulsoup4 = "~=4.9.3"
|
beautifulsoup4 = "~=4.9.3"
|
||||||
Brotli = "~=1.0.9"
|
Brotli = "~=1.0.9"
|
||||||
cachetools = "~=4.2.1"
|
cachetools = "~=4.2.1"
|
||||||
certifi = "~=2020.12.5"
|
|
||||||
chardet = "~=4.0.0"
|
chardet = "~=4.0.0"
|
||||||
configparser = "~=5.0.1"
|
configparser = "~=5.0.1"
|
||||||
contextlib2 = "~=0.6.0.post1"
|
contextlib2 = "~=0.6.0.post1"
|
||||||
@@ -22,22 +21,19 @@ dj-static = "~=0.0.6"
|
|||||||
Django = "~=3.2"
|
Django = "~=3.2"
|
||||||
django-debug-toolbar = "~=3.2"
|
django-debug-toolbar = "~=3.2"
|
||||||
django-filter = "~=2.4.0"
|
django-filter = "~=2.4.0"
|
||||||
django-ical = "~=1.7.1"
|
django-ical = "~=1.8.3"
|
||||||
django-recurrence = "~=1.10.3"
|
|
||||||
django-registration-redux = "~=2.9"
|
django-registration-redux = "~=2.9"
|
||||||
django-reversion = "~=3.0.9"
|
django-reversion = "~=3.0.9"
|
||||||
django-toolbelt = "~=0.0.1"
|
|
||||||
django-widget-tweaks = "~=1.4.8"
|
django-widget-tweaks = "~=1.4.8"
|
||||||
django-htmlmin = "~=0.11.0"
|
django-htmlmin = "~=0.11.0"
|
||||||
envparse = "~=0.2.0"
|
envparse = "*"
|
||||||
gunicorn = "~=20.0.4"
|
gunicorn = "~=20.0.4"
|
||||||
icalendar = "~=4.0.7"
|
icalendar = "~=4.0.7"
|
||||||
idna = "~=2.10"
|
idna = "~=2.10"
|
||||||
lxml = "~=4.7.1"
|
|
||||||
Markdown = "~=3.3.3"
|
Markdown = "~=3.3.3"
|
||||||
msgpack = "~=1.0.2"
|
msgpack = "~=1.0.2"
|
||||||
pep517 = "~=0.9.1"
|
pep517 = "~=0.9.1"
|
||||||
Pillow = "~=9.0.0"
|
Pillow = "~=9.3.0"
|
||||||
premailer = "~=3.7.0"
|
premailer = "~=3.7.0"
|
||||||
progress = "~=1.5"
|
progress = "~=1.5"
|
||||||
psutil = "~=5.8.0"
|
psutil = "~=5.8.0"
|
||||||
@@ -45,7 +41,7 @@ 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.27.5"
|
||||||
PyPOM = "~=2.2.0"
|
PyPOM = "~=2.2.4"
|
||||||
python-dateutil = "~=2.8.1"
|
python-dateutil = "~=2.8.1"
|
||||||
pytoml = "~=0.1.21"
|
pytoml = "~=0.1.21"
|
||||||
pytz = "~=2020.5"
|
pytz = "~=2020.5"
|
||||||
@@ -79,13 +75,12 @@ django-hCaptcha = "*"
|
|||||||
importlib-metadata = "*"
|
importlib-metadata = "*"
|
||||||
django-hcaptcha = "*"
|
django-hcaptcha = "*"
|
||||||
"z3c.rml" = "*"
|
"z3c.rml" = "*"
|
||||||
pikepdf = "*"
|
|
||||||
django-queryable-properties = "*"
|
django-queryable-properties = "*"
|
||||||
django-mass-edit = "*"
|
django-mass-edit = "*"
|
||||||
|
selenium = "~=3.141.0"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
selenium = "~=3.141.0"
|
pycodestyle = "~=2.9.1"
|
||||||
pycodestyle = "*"
|
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
django-coverage-plugin = "*"
|
django-coverage-plugin = "*"
|
||||||
pytest-cov = "*"
|
pytest-cov = "*"
|
||||||
@@ -94,14 +89,11 @@ pluggy = "*"
|
|||||||
pytest-splinter = "*"
|
pytest-splinter = "*"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-reverse = "*"
|
pytest-reverse = "*"
|
||||||
|
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
||||||
|
PyPOM = {extras = [ "splinter",], version = "*"}
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.9"
|
python_version = "3.10"
|
||||||
|
|
||||||
[dev-packages.pytest-xdist]
|
[pipenv]
|
||||||
extras = [ "psutil",]
|
allow_prereleases = true
|
||||||
version = "*"
|
|
||||||
|
|
||||||
[dev-packages.PyPOM]
|
|
||||||
extras = [ "splinter",]
|
|
||||||
version = "*"
|
|
||||||
|
|||||||
1344
Pipfile.lock
generated
1344
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ 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')
|
||||||
@@ -35,6 +36,7 @@ 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:
|
||||||
@@ -42,8 +44,9 @@ if not DEBUG:
|
|||||||
|
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
||||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
DOMAIN = env('DOMAIN', default='example.com')
|
||||||
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
|
||||||
|
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,22 @@ 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")
|
||||||
@@ -217,7 +233,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:
|
||||||
|
|||||||
38
RIGS/management/commands/send_reminders.py
Normal file
38
RIGS/management/commands/send_reminders.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import premailer
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
events = models.Event.objects.current_events().select_related('riskassessment')
|
||||||
|
for event in events:
|
||||||
|
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
|
||||||
|
# 48 hours = 172800 seconds
|
||||||
|
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
|
||||||
|
context = {
|
||||||
|
"event": event,
|
||||||
|
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
|
||||||
|
}
|
||||||
|
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
f"{event} - Risk Assessment Incomplete",
|
||||||
|
get_template("email/ra_reminder.txt").render(context),
|
||||||
|
to=[target],
|
||||||
|
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
|
||||||
|
)
|
||||||
|
css = finders.find('css/email.css')
|
||||||
|
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
|
||||||
|
msg.attach_alternative(html, 'text/html')
|
||||||
|
msg.send()
|
||||||
39
RIGS/migrations/0046_subhire.py
Normal file
39
RIGS/migrations/0046_subhire.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
272
RIGS/models.py
272
RIGS/models.py
@@ -8,7 +8,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -17,9 +17,10 @@ from django.urls import reverse
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
from reversion.models import Version
|
|
||||||
from versioning.versioning import RevisionMixin
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
|
from .validators import validate_url
|
||||||
|
|
||||||
|
|
||||||
def filter_by_pk(filt, query):
|
def filter_by_pk(filt, query):
|
||||||
# try and parse an int
|
# try and parse an int
|
||||||
@@ -261,15 +262,15 @@ class EventManager(models.Manager):
|
|||||||
'venue', 'mic')
|
'venue', 'mic')
|
||||||
return events
|
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):
|
def rig_count(self):
|
||||||
event_count = self.filter(
|
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
||||||
is_rig=True) & ~models.Q(
|
is_rig=True)) | # Starts after with no end
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True)) | # Ends after
|
||||||
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
|
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)) # Active dry hire
|
||||||
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()
|
).count()
|
||||||
return event_count
|
return event_count
|
||||||
|
|
||||||
@@ -304,8 +305,29 @@ class EventManager(models.Manager):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
def find_earliest_event_time(event, datetime_list):
|
||||||
class Event(models.Model, RevisionMixin):
|
# 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
|
# Done to make it much nicer on the database
|
||||||
PROVISIONAL = 0
|
PROVISIONAL = 0
|
||||||
CONFIRMED = 1
|
CONFIRMED = 1
|
||||||
@@ -321,31 +343,97 @@ class Event(models.Model, RevisionMixin):
|
|||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
|
||||||
description = models.TextField(blank=True, default='')
|
description = models.TextField(blank=True, default='')
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||||
dry_hire = models.BooleanField(default=False)
|
|
||||||
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
|
# Timing
|
||||||
start_date = models.DateField()
|
start_date = models.DateField()
|
||||||
start_time = models.TimeField(blank=True, null=True)
|
start_time = models.TimeField(blank=True, null=True)
|
||||||
end_date = models.DateField(blank=True, null=True)
|
end_date = models.DateField(blank=True, null=True)
|
||||||
end_time = models.TimeField(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
|
||||||
|
|
||||||
|
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)
|
access_at = models.DateTimeField(blank=True, null=True)
|
||||||
meet_at = models.DateTimeField(blank=True, null=True)
|
meet_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
# Crew management
|
# Dry-hire only
|
||||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||||
on_delete=models.CASCADE)
|
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
|
# Monies
|
||||||
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')
|
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||||
|
|
||||||
# Authorisation request details
|
# Authorisation request details
|
||||||
@@ -395,26 +483,10 @@ class Event(models.Model, RevisionMixin):
|
|||||||
def total(self):
|
def total(self):
|
||||||
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
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
|
@property
|
||||||
def hs_done(self):
|
def hs_done(self):
|
||||||
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
return self.riskassessment is not None and 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
|
@property
|
||||||
def earliest_time(self):
|
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"""
|
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
@@ -428,73 +500,47 @@ class Event(models.Model, RevisionMixin):
|
|||||||
if self.meet_at:
|
if self.meet_at:
|
||||||
datetime_list.append(self.meet_at)
|
datetime_list.append(self.meet_at)
|
||||||
|
|
||||||
# If there is no start time defined, pretend it's midnight
|
earliest = find_earliest_event_time(self, datetime_list)
|
||||||
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
|
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
|
@property
|
||||||
def internal(self):
|
def internal(self):
|
||||||
return bool(self.organisation and self.organisation.union_account)
|
return bool(self.organisation and self.organisation.union_account)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def authorised(self):
|
def authorised(self):
|
||||||
if self.internal:
|
if self.internal and hasattr(self, 'authorisation'):
|
||||||
return self.authorisation.amount == self.total
|
return self.authorisation.amount == self.total
|
||||||
else:
|
else:
|
||||||
return bool(self.purchase_order)
|
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"
|
||||||
|
else:
|
||||||
|
return "warning"
|
||||||
|
else:
|
||||||
|
return "warning"
|
||||||
|
|
||||||
objects = EventManager()
|
objects = EventManager()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def __str__(self):
|
def get_edit_url(self):
|
||||||
return f"{self.display_id}: {self.name}"
|
return reverse('event_update', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
errdict = {}
|
errdict = super().clean()
|
||||||
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 is not None:
|
||||||
if self.access_at.date() > self.start_date:
|
if self.access_at.date() > self.start_date:
|
||||||
@@ -555,6 +601,56 @@ class EventAuthorisation(models.Model, RevisionMixin):
|
|||||||
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireManager(models.Manager):
|
||||||
|
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 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')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class InvoiceManager(models.Manager):
|
class InvoiceManager(models.Manager):
|
||||||
def outstanding_invoices(self):
|
def outstanding_invoices(self):
|
||||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||||
@@ -681,14 +777,6 @@ class Payment(models.Model, RevisionMixin):
|
|||||||
return f"payment of £{self.amount}"
|
return f"payment of £{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
|
@reversion.register
|
||||||
class RiskAssessment(models.Model, RevisionMixin):
|
class RiskAssessment(models.Model, RevisionMixin):
|
||||||
SMALL = (0, 'Small')
|
SMALL = (0, 'Small')
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
|
|
||||||
client_email = EmailMultiAlternatives(
|
client_email = EmailMultiAlternatives(
|
||||||
subject,
|
subject,
|
||||||
get_template("eventauthorisation_client_success.txt").render(context),
|
get_template("email/eventauthorisation_client_success.txt").render(context),
|
||||||
to=[instance.email],
|
to=[instance.email],
|
||||||
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
||||||
)
|
)
|
||||||
|
|
||||||
css = finders.find('css/email.css')
|
css = finders.find('css/email.css')
|
||||||
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
|
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
client_email.attach_alternative(html, 'text/html')
|
client_email.attach_alternative(html, 'text/html')
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
|
|
||||||
mic_email = EmailMessage(
|
mic_email = EmailMessage(
|
||||||
subject,
|
subject,
|
||||||
get_template("eventauthorisation_mic_success.txt").render(context),
|
get_template("email/eventauthorisation_mic_success.txt").render(context),
|
||||||
to=[mic_email_address]
|
to=[mic_email_address]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,12 +117,12 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|||||||
|
|
||||||
email = EmailMultiAlternatives(
|
email = EmailMultiAlternatives(
|
||||||
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
||||||
get_template("admin_awaiting_approval.txt").render(context),
|
get_template("email/admin_awaiting_approval.txt").render(context),
|
||||||
to=[admin.email],
|
to=[admin.email],
|
||||||
reply_to=[user.email],
|
reply_to=[user.email],
|
||||||
)
|
)
|
||||||
css = finders.find('css/email.css')
|
css = finders.find('css/email.css')
|
||||||
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
email.attach_alternative(html, 'text/html')
|
email.attach_alternative(html, 'text/html')
|
||||||
email.send()
|
email.send()
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
{% 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>
|
||||||
|
|||||||
@@ -1,197 +1,131 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Calendar{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'js/moment.js' %}"></script>
|
<script src="{% static 'js/moment.js' %}"></script>
|
||||||
<script src="{% static 'js/main.js' %}"></script>
|
<script>
|
||||||
<script>
|
$(document).ready(function() {
|
||||||
viewToUrl = {
|
|
||||||
'timeGridWeek':'week',
|
|
||||||
'timeGridDay':'day',
|
|
||||||
'dayGridMonth':'month'
|
|
||||||
}
|
|
||||||
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');
|
|
||||||
|
|
||||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
|
||||||
themeSystem: 'bootstrap',
|
|
||||||
aspectRatio: 1.5,
|
|
||||||
eventTimeFormat: {
|
|
||||||
'hour': '2-digit',
|
|
||||||
'minute': '2-digit',
|
|
||||||
'hour12': false
|
|
||||||
},
|
|
||||||
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')
|
|
||||||
allDay = false
|
|
||||||
if(end.indexOf("T") < 0){ //If latest does not contain a time
|
|
||||||
end = moment(end + " 23:59").format("YYYY-MM-DD[T]HH:mm:ss")
|
|
||||||
allDay = true
|
|
||||||
}
|
|
||||||
|
|
||||||
thisEvent = {
|
|
||||||
'start': $(this).attr('earliest'),
|
|
||||||
'end': end,
|
|
||||||
'className': 'modal-href',
|
|
||||||
'title': $(this).attr('title'),
|
|
||||||
'url': $(this).attr('url'),
|
|
||||||
'allDay': allDay
|
|
||||||
}
|
|
||||||
|
|
||||||
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
|
||||||
thisEvent['color'] = colours[$(this).attr('status')];
|
|
||||||
}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);
|
|
||||||
|
|
||||||
// Enable/Disable "Today" button as required
|
|
||||||
let $today = $('#today-button');
|
|
||||||
if(moment().isBetween(view.currentStart, view.currentEnd)){
|
|
||||||
//Today is within the current view
|
|
||||||
$today.prop('disabled', true);
|
|
||||||
}else{
|
|
||||||
$today.prop('disabled', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set active view select button
|
|
||||||
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;
|
|
||||||
|
|
||||||
case 'timeGridWeek':
|
|
||||||
$month.removeClass('active');
|
|
||||||
$week.addClass('active');
|
|
||||||
$day.removeClass('active');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'timeGridDay':
|
|
||||||
$month.removeClass('active');
|
|
||||||
$week.removeClass('active');
|
|
||||||
$day.addClass('active');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
$(document).ready(function() {
|
|
||||||
// set some button listeners
|
// set some button listeners
|
||||||
$('#next-button').click(function(){ calendar.next(); });
|
|
||||||
$('#prev-button').click(function(){ calendar.prev(); });
|
|
||||||
$('#today-button').click(function(){ calendar.today(); });
|
$('#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(){
|
$('#go-to-date-input').change(function(){
|
||||||
if(moment($('#go-to-date-input').val()).isValid()){
|
if(moment($('#go-to-date-input').val()).isValid()){
|
||||||
$('#go-to-date-button').prop('disabled', false);
|
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{
|
} else{
|
||||||
$('#go-to-date-button').prop('disabled', true);
|
document.getElementById('go-to-date-button').classList.add('disabled');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#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>
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.week {
|
||||||
|
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 {
|
||||||
|
display:contents;
|
||||||
|
}
|
||||||
|
.day-label {
|
||||||
|
grid-row-start: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin:0;
|
||||||
|
font-size: 1em !important;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day, .day-label, .event {
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
background-color: #CCC;
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-end {
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-start {
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.event {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-span="1"] { grid-column-end: span 1; }
|
||||||
|
[data-span="2"] { grid-column-end: span 2; }
|
||||||
|
[data-span="3"] { grid-column-end: span 3; }
|
||||||
|
[data-span="4"] { grid-column-end: span 4; }
|
||||||
|
[data-span="5"] { grid-column-end: span 5; }
|
||||||
|
[data-span="6"] { grid-column-end: span 6; }
|
||||||
|
[data-span="7"] { grid-column-end: span 7; }
|
||||||
|
|
||||||
|
.day > a {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row justify-content-center mb-1">
|
||||||
<div class="col-sm-12">
|
<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="pull-left">
|
<div class="form-inline col-4">
|
||||||
<span id="calendar-header" class="h2"></span>
|
<div class="input-group">
|
||||||
</div>
|
<input type="date" id="go-to-date-input" placeholder="Go to date..." class="form-control">
|
||||||
<div class="form-inline float-right btn-page my-3">
|
<span class="input-group-append">
|
||||||
<div class="input-group mx-2">
|
<a class="btn btn-success" id="go-to-date-button">Go!</a>
|
||||||
<input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
|
</span>
|
||||||
<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 %}
|
||||||
|
|||||||
29
RIGS/templates/dashboards/productions.html
Normal file
29
RIGS/templates/dashboards/productions.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% 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 %}
|
||||||
5
RIGS/templates/email/eventauthorisation_mic_success.txt
Normal file
5
RIGS/templates/email/eventauthorisation_mic_success.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
|
||||||
|
|
||||||
|
Just to let you know your event N{{object.eventdisplay_id}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
||||||
|
|
||||||
|
The TEC Rig Information Gathering System
|
||||||
16
RIGS/templates/email/ra_reminder.html
Normal file
16
RIGS/templates/email/ra_reminder.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'base_client_email.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},</p>
|
||||||
|
|
||||||
|
{% if event.mic %}
|
||||||
|
<p>Just to let you know your event {{event.display_id}} <em>requires<em> a pre-event risk assessment completing prior to the event. Please do so as soon as possible.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>Fill it out here:</p>
|
||||||
|
<a href="{{url}}" class="btn btn-info"><span class="fas fa-paperclip"></span> Create Risk Assessment</a>
|
||||||
|
|
||||||
|
<p>TEC PA & Lighting</p>
|
||||||
|
{% endblock %}
|
||||||
9
RIGS/templates/email/ra_reminder.txt
Normal file
9
RIGS/templates/email/ra_reminder.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},
|
||||||
|
|
||||||
|
{% if event.mic %}
|
||||||
|
Just to let you know your event {{event.display_id}} requires a risk assessment completing prior to the event. Please do so as soon as possible.
|
||||||
|
{% else %}
|
||||||
|
This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
The TEC Rig Information Gathering System
|
||||||
@@ -25,12 +25,10 @@
|
|||||||
{% include 'partials/hs_details.html' %}
|
{% include 'partials/hs_details.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.is_rig %}
|
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
||||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
<div class="col-md-8 py-3">
|
||||||
<div class="col-md-8 py-3">
|
{% include 'partials/auth_details.html' %}
|
||||||
{% include 'partials/auth_details.html' %}
|
</div>
|
||||||
</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">
|
||||||
@@ -47,9 +45,15 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<h4>Event Items</h4>
|
||||||
{% include 'partials/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 %}
|
||||||
|
|||||||
@@ -106,6 +106,10 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
|
|
||||||
|
|
||||||
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
|
||||||
|
|
||||||
The TEC Rig Information Gathering System
|
|
||||||
@@ -47,7 +47,5 @@
|
|||||||
class="fas fa-pound-sign"></span>
|
class="fas fa-pound-sign"></span>
|
||||||
<span class="d-none d-sm-inline">Invoice</span></a>
|
<span class="d-none d-sm-inline">Invoice</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,21 +12,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
<tr class="{% if event.cancelled %}
|
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||||
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-->
|
||||||
@@ -56,7 +42,7 @@
|
|||||||
<!---Details-->
|
<!---Details-->
|
||||||
<td id="event_details" class="w-100">
|
<td id="event_details" class="w-100">
|
||||||
<h4>
|
<h4>
|
||||||
<a href="{% url 'event_detail' event.pk %}">
|
<a href="{{event.get_absolute_url}}">
|
||||||
{{ event.name }}
|
{{ event.name }}
|
||||||
</a>
|
</a>
|
||||||
{% if event.venue %}
|
{% if event.venue %}
|
||||||
|
|||||||
@@ -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">
|
||||||
<i class="fas fa-plus"></i> Add Item
|
<span class="fas fa-plus"></span> Add Item
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
76
RIGS/templates/subhire_detail.html
Normal file
76
RIGS/templates/subhire_detail.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{% 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 %}
|
||||||
202
RIGS/templates/subhire_form.html
Normal file
202
RIGS/templates/subhire_form.html
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -118,9 +118,9 @@ def orderby(request, field, attr):
|
|||||||
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
||||||
def get_field(obj, field, autoescape=True):
|
def get_field(obj, field, autoescape=True):
|
||||||
value = getattr(obj, field)
|
value = getattr(obj, field)
|
||||||
if(isinstance(value, bool)):
|
if (isinstance(value, bool)):
|
||||||
value = yesnoi(value, field in obj.inverted_fields)
|
value = yesnoi(value, field in obj.inverted_fields)
|
||||||
elif(isinstance(value, str)):
|
elif (isinstance(value, str)):
|
||||||
value = truncatewords(value, 20)
|
value = truncatewords(value, 20)
|
||||||
return mark_safe(value)
|
return mark_safe(value)
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ def get_list(dictionary, key):
|
|||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def profile_by_index(value):
|
def profile_by_index(value):
|
||||||
if(value):
|
if (value):
|
||||||
return models.Profile.objects.get(pk=int(value))
|
return models.Profile.objects.get(pk=int(value))
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
26
RIGS/urls.py
26
RIGS/urls.py
@@ -43,12 +43,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Rigboard
|
# Rigboard
|
||||||
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
|
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
|
||||||
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
|
re_path(r'^rigboard/calendar/$', login_required()(views.WebCalendar.as_view()),
|
||||||
name='web_calendar'),
|
name='web_calendar'),
|
||||||
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
|
|
||||||
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
|
|
||||||
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
|
|
||||||
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
|
|
||||||
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||||
|
|
||||||
|
|
||||||
@@ -70,12 +66,28 @@ urlpatterns = [
|
|||||||
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
|
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
|
||||||
name='event_duplicate'),
|
name='event_duplicate'),
|
||||||
|
|
||||||
|
|
||||||
|
# Subhire
|
||||||
|
path('subhire/<int:pk>/', login_required(views.SubhireDetail.as_view()),
|
||||||
|
name='subhire_detail'),
|
||||||
|
path('subhire/create/', permission_required_with_403('RIGS.add_event')(views.SubhireCreate.as_view()),
|
||||||
|
name='subhire_create'),
|
||||||
|
path('subhire/<int:pk>/edit', permission_required_with_403('RIGS.change_event')(views.SubhireEdit.as_view()),
|
||||||
|
name='subhire_update'),
|
||||||
|
path('subhire/upcoming', login_required(views.SubhireList.as_view()),
|
||||||
|
name='subhire_list'),
|
||||||
|
|
||||||
|
# Dashboards
|
||||||
|
path('dashboard/productions/', views.ProductionsDashboard.as_view(),
|
||||||
|
name='productions_dashboard'),
|
||||||
|
|
||||||
|
|
||||||
# Event H&S
|
# Event H&S
|
||||||
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
||||||
|
|
||||||
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
|
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
|
||||||
name='event_ra'),
|
name='event_ra'),
|
||||||
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentDetail.as_view()),
|
path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()),
|
||||||
name='ra_detail'),
|
name='ra_detail'),
|
||||||
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
||||||
name='ra_edit'),
|
name='ra_edit'),
|
||||||
@@ -87,7 +99,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
||||||
name='event_ec'),
|
name='event_ec'),
|
||||||
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistDetail.as_view()),
|
path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()),
|
||||||
name='ec_detail'),
|
name='ec_detail'),
|
||||||
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
||||||
name='ec_edit'),
|
name='ec_edit'),
|
||||||
|
|||||||
54
RIGS/utils.py
Normal file
54
RIGS/utils.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from datetime import datetime, timedelta, date
|
||||||
|
import calendar
|
||||||
|
from calendar import HTMLCalendar
|
||||||
|
from RIGS.models import Event, Subhire
|
||||||
|
|
||||||
|
|
||||||
|
class Calendar(HTMLCalendar):
|
||||||
|
def __init__(self, year=None, month=None):
|
||||||
|
self.year = year
|
||||||
|
self.month = month
|
||||||
|
super(Calendar, self).__init__()
|
||||||
|
|
||||||
|
def get_html(self, day, event):
|
||||||
|
return f"<a href='{event.get_absolute_url()}' class='modal-href' style='display: contents;'><div class='event event-start event-end bg-{event.color}' data-span='{event.length}' style='grid-column-start: calc({day[1]} + 1)'>{event}</div></a>"
|
||||||
|
|
||||||
|
def formatmonth(self, withyear=True):
|
||||||
|
events = Event.objects.filter(start_date__year=self.year, start_date__month=self.month)
|
||||||
|
subhires = Subhire.objects.filter(start_date__year=self.year, start_date__month=self.month)
|
||||||
|
weeks = self.monthdays2calendar(self.year, self.month)
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for week in weeks:
|
||||||
|
weeks_events = []
|
||||||
|
for day in week:
|
||||||
|
events_per_day = events.order_by("start_date").filter(start_date__day=day[0])
|
||||||
|
subhires_per_day = subhires.order_by("start_date").filter(start_date__day=day[0])
|
||||||
|
event_html = ""
|
||||||
|
for event in events_per_day:
|
||||||
|
event_html += self.get_html(day, event)
|
||||||
|
for sh in subhires_per_day:
|
||||||
|
event_html += self.get_html(day, sh)
|
||||||
|
weeks_events.append((day[0], day[1], event_html))
|
||||||
|
data.append(weeks_events)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_date(req_day):
|
||||||
|
if req_day:
|
||||||
|
year, month = (int(x) for x in req_day.split('-'))
|
||||||
|
return date(year, month, day=1)
|
||||||
|
return datetime.today()
|
||||||
|
|
||||||
|
def prev_month(d):
|
||||||
|
first = d.replace(day=1)
|
||||||
|
prev_month = first - timedelta(days=1)
|
||||||
|
month = f'month={str(prev_month.year)}-{str(prev_month.month)}'
|
||||||
|
return month
|
||||||
|
|
||||||
|
def next_month(d):
|
||||||
|
days_in_month = calendar.monthrange(d.year, d.month)[1]
|
||||||
|
last = d.replace(day=days_in_month)
|
||||||
|
next_month = last + timedelta(days=1)
|
||||||
|
month = f'month={str(next_month.year)}-{str(next_month.month)}'
|
||||||
|
return month
|
||||||
10
RIGS/validators.py
Normal file
10
RIGS/validators.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -3,3 +3,5 @@ from .finance import *
|
|||||||
from .hs import *
|
from .hs import *
|
||||||
from .ical import *
|
from .ical import *
|
||||||
from .rigboard import *
|
from .rigboard import *
|
||||||
|
from .subhire import *
|
||||||
|
from .dashboards import *
|
||||||
14
RIGS/views/dashboards.py
Normal file
14
RIGS/views/dashboards.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.views import generic
|
||||||
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionsDashboard(generic.TemplateView):
|
||||||
|
template_name = 'dashboards/productions.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = "Productions Dashboard"
|
||||||
|
context['rig_count'] = models.Event.objects.rig_count()
|
||||||
|
context['subhire_count'] = models.Subhire.objects.event_count()
|
||||||
|
context['hire_count'] = models.Event.objects.active_dry_hires().count()
|
||||||
|
return context
|
||||||
@@ -2,7 +2,6 @@ import copy
|
|||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import premailer
|
import premailer
|
||||||
import simplejson
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -12,9 +11,7 @@ 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
|
||||||
@@ -22,7 +19,7 @@ from django.views import generic
|
|||||||
|
|
||||||
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, ModalURLMixin, PrintView, get_related
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms, utils
|
||||||
|
|
||||||
__author__ = 'ghost'
|
__author__ = 'ghost'
|
||||||
|
|
||||||
@@ -40,14 +37,25 @@ class RigboardIndex(generic.TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WebCalendar(generic.TemplateView):
|
class WebCalendar(generic.ListView):
|
||||||
|
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().get_context_data(**kwargs)
|
||||||
context['view'] = kwargs.get('view', '')
|
# use today's date for the calendar
|
||||||
context['date'] = kwargs.get('date', '')
|
d = utils.get_date(self.request.GET.get('month', None))
|
||||||
# context['page_title'] = "Calendar"
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -61,10 +69,6 @@ class EventDetail(generic.DetailView, ModalURLMixin):
|
|||||||
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
|
||||||
if is_ajax(self.request):
|
|
||||||
context['override'] = "base_ajax.html"
|
|
||||||
else:
|
|
||||||
context['override'] = 'base_assets.html'
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -342,12 +346,12 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
msg = EmailMultiAlternatives(
|
||||||
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
|
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
|
||||||
get_template("eventauthorisation_client_request.txt").render(context),
|
get_template("email/eventauthorisation_client_request.txt").render(context),
|
||||||
to=[email],
|
to=[email],
|
||||||
reply_to=[self.request.user.email],
|
reply_to=[self.request.user.email],
|
||||||
)
|
)
|
||||||
css = finders.find('css/email.css')
|
css = finders.find('css/email.css')
|
||||||
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
|
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
msg.attach_alternative(html, 'text/html')
|
msg.attach_alternative(html, 'text/html')
|
||||||
|
|
||||||
@@ -357,7 +361,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
|
|
||||||
|
|
||||||
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||||
template_name = "eventauthorisation_client_request.html"
|
template_name = "email/eventauthorisation_client_request.html"
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
|
|||||||
58
RIGS/views/subhire.py
Normal file
58
RIGS/views/subhire.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views import generic
|
||||||
|
from PyRIGS.views import ModalURLMixin, get_related
|
||||||
|
from RIGS import models, forms
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireDetail(generic.DetailView, ModalURLMixin):
|
||||||
|
template_name = 'subhire_detail.html'
|
||||||
|
model = models.Subhire
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = f"{self.object.display_id} | {self.object.name}"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireCreate(generic.CreateView):
|
||||||
|
model = models.Subhire
|
||||||
|
form_class = forms.SubhireForm
|
||||||
|
template_name = 'subhire_form.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = "New Subhire"
|
||||||
|
context['edit'] = True
|
||||||
|
form = context['form']
|
||||||
|
get_related(form, context)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('subhire_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireEdit(generic.UpdateView):
|
||||||
|
model = models.Subhire
|
||||||
|
form_class = forms.SubhireForm
|
||||||
|
template_name = 'subhire_form.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = f"Edit Subhire: {self.object.display_id} | {self.object.name}"
|
||||||
|
context['edit'] = True
|
||||||
|
form = context['form']
|
||||||
|
get_related(form, context)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('subhire_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireList(generic.TemplateView):
|
||||||
|
template_name = 'rigboard.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['events'] = models.Subhire.objects.current_events()
|
||||||
|
context['page_title'] = "Upcoming Subhire"
|
||||||
|
return context
|
||||||
18
assets/migrations/0027_asset_nickname.py
Normal file
18
assets/migrations/0027_asset_nickname.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2022-12-11 00:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0026_auto_20220526_1623'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='asset',
|
||||||
|
name='nickname',
|
||||||
|
field=models.CharField(blank=True, max_length=120),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -95,7 +95,7 @@ class AssetManager(models.Manager):
|
|||||||
def search(self, query=None):
|
def search(self, query=None):
|
||||||
qs = self.get_queryset()
|
qs = self.get_queryset()
|
||||||
if query is not None:
|
if query is not None:
|
||||||
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query))
|
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query) | Q(nickname__icontains=query))
|
||||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -125,6 +125,7 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||||
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
nickname = models.CharField(max_length=120, blank=True)
|
||||||
|
|
||||||
# Audit
|
# Audit
|
||||||
last_audited_at = models.DateTimeField(blank=True, null=True)
|
last_audited_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
<label for="{{ form.description.id_for_label }}">Description</label>
|
<label for="{{ form.description.id_for_label }}">Description</label>
|
||||||
{% render_field form.description|add_class:'form-control' value=object.description %}
|
{% render_field form.description|add_class:'form-control' value=object.description %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.nickname.id_for_label }}">Nickname</label>
|
||||||
|
{% render_field form.nickname|add_class:'form-control' value=object.nickname %}
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.category.id_for_label }}" >Category</label>
|
<label for="{{ form.category.id_for_label }}" >Category</label>
|
||||||
{% render_field form.category|add_class:'form-control'%}
|
{% render_field form.category|add_class:'form-control'%}
|
||||||
@@ -45,7 +49,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<dt>Asset ID</dt>
|
<dt>Asset ID</dt>
|
||||||
<dd>{{ object.asset_id }}</dd>
|
<dd>{{ object.asset_id }}</dd>
|
||||||
|
{% if object.nickname %}
|
||||||
|
<dt>Nickname</dt>
|
||||||
|
<dd>"{{ object.nickname }}"</dd>
|
||||||
|
{% endif %}
|
||||||
<dt>Description</dt>
|
<dt>Description</dt>
|
||||||
<dd>{{ object.description }}</dd>
|
<dd>{{ object.description }}</dd>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ function fonts(done) {
|
|||||||
function styles(done) {
|
function styles(done) {
|
||||||
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
|
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
|
||||||
return gulp.src(['pipeline/source_assets/scss/**/*.scss',
|
return gulp.src(['pipeline/source_assets/scss/**/*.scss',
|
||||||
'node_modules/fullcalendar/main.css',
|
|
||||||
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
|
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
|
||||||
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
|
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
|
||||||
'node_modules/easymde/dist/easymde.min.css'
|
'node_modules/easymde/dist/easymde.min.css'
|
||||||
@@ -59,7 +58,6 @@ function scripts() {
|
|||||||
'node_modules/html5sortable/dist/html5sortable.min.js',
|
'node_modules/html5sortable/dist/html5sortable.min.js',
|
||||||
'node_modules/clipboard/dist/clipboard.min.js',
|
'node_modules/clipboard/dist/clipboard.min.js',
|
||||||
'node_modules/moment/moment.js',
|
'node_modules/moment/moment.js',
|
||||||
'node_modules/fullcalendar/main.js',
|
|
||||||
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
|
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
|
||||||
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
|
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
|
||||||
'node_modules/easymde/dist/easymde.min.js',
|
'node_modules/easymde/dist/easymde.min.js',
|
||||||
|
|||||||
7080
package-lock.json
generated
7080
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@
|
|||||||
"clipboard": "^2.0.8",
|
"clipboard": "^2.0.8",
|
||||||
"cssnano": "^5.0.13",
|
"cssnano": "^5.0.13",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
"fullcalendar": "^5.10.1",
|
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-flatten": "^0.4.0",
|
"gulp-flatten": "^0.4.0",
|
||||||
@@ -28,13 +27,13 @@
|
|||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"konami": "^1.6.3",
|
"konami": "^1.6.3",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"node-sass": "^7.0.0",
|
"node-sass": "^7.0.3",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"uglify-js": "^3.14.5"
|
"uglify-js": "^3.14.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browser-sync": "^2.27.7"
|
"browser-sync": "^2.27.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gulp": "gulp",
|
"gulp": "gulp",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ function initPicker(obj) {
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
console.log(obj.data);
|
|
||||||
if (!obj.data('noclear')) {
|
if (!obj.data('noclear')) {
|
||||||
obj.prepend($("<option></option>")
|
obj.prepend($("<option></option>")
|
||||||
.attr("value",'')
|
.attr("value",'')
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ Date.prototype.getISOString = function () {
|
|||||||
var dd = this.getDate().toString();
|
var dd = this.getDate().toString();
|
||||||
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
|
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
|
||||||
};
|
};
|
||||||
jQuery(document).ready(function () {
|
$(document).ready(function () {
|
||||||
jQuery(document).on('click', '.modal-href', function (e) {
|
$(document).on('click', '.modal-href', function (e) {
|
||||||
$link = jQuery(this);
|
$link = $(this);
|
||||||
// Anti modal inception
|
// Anti modal inception
|
||||||
if ($link.parents('#modal').length == 0) {
|
if ($link.parents('#modal').length == 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
modaltarget = $link.data('target');
|
modaltarget = $link.data('target');
|
||||||
modalobject = "";
|
modalobject = "";
|
||||||
jQuery('#modal').load($link.attr('href'), function (e) {
|
$('#modal').load($link.attr('href'), function (e) {
|
||||||
jQuery('#modal').modal();
|
$('#modal').modal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -23,7 +23,6 @@ jQuery(document).ready(function () {
|
|||||||
s.type = 'text/javascript';
|
s.type = 'text/javascript';
|
||||||
document.body.appendChild(s);
|
document.body.appendChild(s);
|
||||||
s.src = '{% static "js/asteroids.min.js"%}';
|
s.src = '{% static "js/asteroids.min.js"%}';
|
||||||
ga('send', 'event', 'easter_egg', 'activated');
|
|
||||||
}
|
}
|
||||||
easter_egg.load();
|
easter_egg.load();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -281,3 +281,7 @@ html.embedded {
|
|||||||
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
||||||
padding-right: 1rem !important;
|
padding-right: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-purple, .bg-purple {
|
||||||
|
background-color: #800080 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from django.urls import path
|
|||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from training.decorators import is_supervisor
|
from training.decorators import is_supervisor
|
||||||
from PyRIGS.decorators import permission_required_with_403
|
|
||||||
|
|
||||||
from training import views, models
|
from training import views, models
|
||||||
from versioning.views import VersionHistory
|
from versioning.views import VersionHistory
|
||||||
@@ -12,10 +11,9 @@ urlpatterns = [
|
|||||||
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
|
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
|
||||||
|
|
||||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||||
path('trainee/<int:pk>/',
|
path('trainee/<int:pk>/', login_required(views.TraineeDetail.as_view()),
|
||||||
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
|
||||||
name='trainee_detail'),
|
name='trainee_detail'),
|
||||||
path('trainee/<int:pk>/history', permission_required_with_403('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
path('trainee/<int:pk>/history', login_required(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||||
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
|
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
|
||||||
name='add_qualification'),
|
name='add_qualification'),
|
||||||
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
|
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class ModelComparison:
|
|||||||
def name(self):
|
def name(self):
|
||||||
obj = self.new if self.new else self.old
|
obj = self.new if self.new else self.old
|
||||||
|
|
||||||
if(hasattr(obj, 'activity_feed_string')):
|
if (hasattr(obj, 'activity_feed_string')):
|
||||||
return obj.activity_feed_string
|
return obj.activity_feed_string
|
||||||
else:
|
else:
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|||||||
Reference in New Issue
Block a user