Compare commits

...

42 Commits

Author SHA1 Message Date
63a2f6d47b Add URLField for linking to uploaded quotes 2022-12-16 14:48:54 +00:00
8393e85b74 Add appropriate permissions for subhire 2022-12-16 14:25:55 +00:00
311c02d554 Fix associated events being discarded on subhire edit 2022-12-16 14:14:43 +00:00
e100f5a1d4 Initial work on productions dashboard 2022-12-16 14:06:06 +00:00
eb07990f4c More template work 2022-12-16 13:33:18 +00:00
7b7c1b86de Update for use with github codespaces 2022-12-16 12:35:52 +00:00
2b8945c513 Update subhire detail template 2022-12-16 12:35:08 +00:00
eb3638b93a Merge remote-tracking branch 'origin/master' into subhire 2022-12-16 12:05:41 +00:00
86c033ba97 Update django.yml 2022-12-11 14:18:05 +00:00
52fd662340 Update django.yml 2022-12-11 14:13:47 +00:00
9818ed995f Update django.yml 2022-12-11 14:13:33 +00:00
5178614d71 Update django.yml
Rollback runner for now
2022-12-11 01:01:01 +00:00
1660f51e55 Merge branch 'master' into subhire
# Conflicts:
#	Pipfile.lock
2022-12-11 00:53:27 +00:00
a7bf990666 FIX: Do not specify minor version of python in CI 2022-12-11 00:51:19 +00:00
9feea56211 Draft "upcoming subhire" view 2022-12-11 00:49:56 +00:00
951227e68b Make calendar work reasonably on mobile 2022-12-11 00:44:14 +00:00
a4a28a6130 Add nickname field to assets
Seems necessary given all the lights and some of the amps and distros have names :-)
2022-12-11 00:29:30 +00:00
e3d8cf8978 Dependency update 2022-12-11 00:26:24 +00:00
6e8779c81b Revamped calendar basically there
To fix:
- Does not yet display events that span weeks correctly!
- Breaks (overflows) on mobile
2022-12-06 15:31:35 +00:00
e0da6a3120 Remove rogue print statement from JS 2022-12-06 15:03:57 +00:00
0c80ef1b72 More calendar work 2022-12-06 15:03:27 +00:00
0f127d8ca4 Entirely new concept 2022-12-06 13:20:07 +00:00
626779ef25 Turn off RA reminders for non-rigs 2022-12-05 21:30:24 +00:00
fa1dc31639 FIX: Only require login for viewing RAs/ECs 2022-11-26 12:58:15 +00:00
d69543e309 FIX: Only require login to view training profiles
Previously required a specific permission only granted to keyholders
2022-11-26 12:54:42 +00:00
04ec728972 Reimplemented calendar mostly working
Multi day alignment puzzle is pretty hard...
2022-11-22 23:59:21 +00:00
dependabot[bot]
a24e6d4495 Build(deps): Bump pillow from 9.0.1 to 9.3.0 (#516)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.0.1 to 9.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.0.1...9.3.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-22 17:33:42 +00:00
dependabot[bot]
fa5792914a Build(deps): Bump engine.io from 6.2.0 to 6.2.1 (#515)
Bumps [engine.io](https://github.com/socketio/engine.io) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-22 17:33:14 +00:00
bede8b4176 Merge branch 'master' into subhire
# Conflicts:
#	Pipfile
#	Pipfile.lock
#	package-lock.json
2022-11-21 19:49:33 +00:00
0117091f3e FEAT #513: Implement email reminders to complete risk assessments (#514)
* FEAT #513: Implement email reminders to complete risk assessments

* Add missing f-string tag
2022-11-19 15:34:15 +00:00
8cade512d1 Initial work at reimplementing the calendar in python
Buhby fullcalendar
2022-11-18 16:16:06 +00:00
418219940b Add subhire link to new event page 2022-11-18 14:58:59 +00:00
37101d3340 Update lockfile 2022-11-18 14:29:44 +00:00
de4bed92a4 Build(deps): Generally update JS & Python deps (#512) 2022-11-17 12:06:21 +00:00
dependabot[bot]
3767923175 Build(deps): Bump yargs-parser and yargs (#507)
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) and [yargs](https://github.com/yargs/yargs). These dependencies needed to be updated together.

Updates `yargs-parser` from 5.0.0-security.0 to 20.2.9
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs-parser/commits/yargs-parser-v20.2.9)

Updates `yargs` from 7.1.1 to 15.4.1
- [Release notes](https://github.com/yargs/yargs/releases)
- [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs/commits/v15.4.1)

---
updated-dependencies:
- dependency-name: yargs-parser
  dependency-type: indirect
- dependency-name: yargs
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-15 15:52:49 +00:00
dependabot[bot]
1d77cf95d3 Build(deps): Bump scss-tokenizer and node-sass (#506)
Bumps [scss-tokenizer](https://github.com/sasstools/scss-tokenizer) to 0.4.3 and updates ancestor dependency [node-sass](https://github.com/sass/node-sass). These dependencies need to be updated together.


Updates `scss-tokenizer` from 0.3.0 to 0.4.3
- [Release notes](https://github.com/sasstools/scss-tokenizer/releases)
- [Commits](https://github.com/sasstools/scss-tokenizer/compare/v0.3.0...v0.4.3)

Updates `node-sass` from 7.0.1 to 7.0.3
- [Release notes](https://github.com/sass/node-sass/releases)
- [Changelog](https://github.com/sass/node-sass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sass/node-sass/compare/v7.0.1...v7.0.3)

---
updated-dependencies:
- dependency-name: scss-tokenizer
  dependency-type: indirect
- dependency-name: node-sass
  dependency-type: direct:production
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-15 15:44:25 +00:00
dependabot[bot]
1f21d0b265 Build(deps): Bump engine.io and browser-sync (#508)
Bumps [engine.io](https://github.com/socketio/engine.io) to 6.2.0 and updates ancestor dependency [browser-sync](https://github.com/BrowserSync/browser-sync). These dependencies need to be updated together.


Updates `engine.io` from 3.5.0 to 6.2.0
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/3.5.0...6.2.0)

Updates `browser-sync` from 2.27.7 to 2.27.10
- [Release notes](https://github.com/BrowserSync/browser-sync/releases)
- [Changelog](https://github.com/BrowserSync/browser-sync/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BrowserSync/browser-sync/compare/v2.27.7...v2.27.10)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
- dependency-name: browser-sync
  dependency-type: direct:development
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-15 15:33:50 +00:00
7846a6d31e Rename combine-prs to combine-prs.yml 2022-11-14 18:20:57 +00:00
d28b73a0b8 Create combine-prs
Copied from here: https://github.com/hrvey/combine-prs-workflow
2022-11-14 16:30:55 +00:00
948a41f43a Initial work on associating events with subhires 2022-10-20 12:56:42 +01:00
4449efcced Add subhire detail view 2022-10-20 00:00:57 +01:00
8b0cd13159 Initially create subhire model and form 2022-10-15 19:09:51 +01:00
49 changed files with 5296 additions and 4664 deletions

151
.github/workflows/combine-prs.yml vendored Normal file
View 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
});

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -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;">&nbsp;<span></div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
{% endblock %} {% endblock %}

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

View 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

View 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 &amp; Lighting</p>
{% endblock %}

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -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
View 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
View 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')

View File

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

View File

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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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",'')

View File

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

View File

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

View File

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

View File

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