Compare commits

..

2 Commits

Author SHA1 Message Date
github-actions[bot]
9b16cf6333 Merge dependabot/npm_and_yarn/engine.io-and-browser-sync-6.2.0 into combine-prs-branch 2022-11-14 18:21:26 +00:00
dependabot[bot]
7798f5c368 Build(deps): Bump engine.io and browser-sync
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>
2022-11-14 15:14:28 +00:00
49 changed files with 3889 additions and 4806 deletions

View File

@@ -1,151 +0,0 @@
name: 'Combine PRs'
# Controls when the action will run - in this case triggered manually
on:
workflow_dispatch:
inputs:
branchPrefix:
description: 'Branch prefix to find combinable PRs based on'
required: true
default: 'dependabot'
mustBeGreen:
description: 'Only combine PRs that are green (status is success)'
required: true
default: true
combineBranchName:
description: 'Name of the branch to combine PRs into'
required: true
default: 'combine-prs-branch'
ignoreLabel:
description: 'Exclude PRs with this label'
required: true
default: 'nocombine'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "combine-prs"
combine-prs:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/github-script@v6
id: create-combined-pr
name: Create Combined PR
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
owner: context.repo.owner,
repo: context.repo.repo
});
let branchesAndPRStrings = [];
let baseBranch = null;
let baseBranchSHA = null;
for (const pull of pulls) {
const branch = pull['head']['ref'];
console.log('Pull for branch: ' + branch);
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
console.log('Branch matched prefix: ' + branch);
let statusOK = true;
if(${{ github.event.inputs.mustBeGreen }}) {
console.log('Checking green status: ' + branch);
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number:$pull_number) {
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
state
}
}
}
}
}
}
}`
const vars = {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull['number']
};
const result = await github.graphql(stateQuery, vars);
const [{ commit }] = result.repository.pullRequest.commits.nodes;
const state = commit.statusCheckRollup.state
console.log('Validating status: ' + state);
if(state != 'SUCCESS') {
console.log('Discarding ' + branch + ' with status ' + state);
statusOK = false;
}
}
console.log('Checking labels: ' + branch);
const labels = pull['labels'];
for(const label of labels) {
const labelName = label['name'];
console.log('Checking label: ' + labelName);
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
console.log('Discarding ' + branch + ' with label ' + labelName);
statusOK = false;
}
}
if (statusOK) {
console.log('Adding branch to array: ' + branch);
const prString = '#' + pull['number'] + ' ' + pull['title'];
branchesAndPRStrings.push({ branch, prString });
baseBranch = pull['base']['ref'];
baseBranchSHA = pull['base']['sha'];
}
}
}
if (branchesAndPRStrings.length == 0) {
core.setFailed('No PRs/branches matched criteria');
return;
}
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
sha: baseBranchSHA
});
} catch (error) {
console.log(error);
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
return;
}
let combinedPRs = [];
let mergeFailedPRs = [];
for(const { branch, prString } of branchesAndPRStrings) {
try {
await github.rest.repos.merge({
owner: context.repo.owner,
repo: context.repo.repo,
base: '${{ github.event.inputs.combineBranchName }}',
head: branch,
});
console.log('Merged branch ' + branch);
combinedPRs.push(prString);
} catch (error) {
console.log('Failed to merge branch ' + branch);
mergeFailedPRs.push(prString);
}
}
console.log('Creating combined PR');
const combinedPRsString = combinedPRs.join('\n');
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
if(mergeFailedPRs.length > 0) {
const mergeFailedPRsString = mergeFailedPRs.join('\n');
body += '\n\n⚠ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
}
await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Combined PR',
head: '${{ github.event.inputs.combineBranchName }}',
base: baseBranch,
body: body
});

View File

@@ -13,20 +13,26 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.9
cache: 'pipenv'
python-version: 3.9.1
- uses: actions/cache@v2
id: pcache
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install Dependencies
run: |
python3 -m pip install --upgrade pip pipenv
python -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
- name: Cache Static Files
id: static-cache
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
@@ -37,9 +43,9 @@ jobs:
- name: Basic Checks
run: |
pipenv run pycodestyle . --exclude=migrations,node_modules
pipenv run python3 manage.py check
pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput
pipenv run python manage.py check
pipenv run python manage.py makemigrations --check --dry-run
pipenv run python manage.py collectstatic --noinput
- name: Run Tests
run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v2

30
Pipfile
View File

@@ -11,6 +11,7 @@ asgiref = "~=3.3.1"
beautifulsoup4 = "~=4.9.3"
Brotli = "~=1.0.9"
cachetools = "~=4.2.1"
certifi = "~=2020.12.5"
chardet = "~=4.0.0"
configparser = "~=5.0.1"
contextlib2 = "~=0.6.0.post1"
@@ -21,19 +22,22 @@ dj-static = "~=0.0.6"
Django = "~=3.2"
django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0"
django-ical = "~=1.8.3"
django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9"
django-toolbelt = "~=0.0.1"
django-widget-tweaks = "~=1.4.8"
django-htmlmin = "~=0.11.0"
envparse = "*"
envparse = "~=0.2.0"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
lxml = "~=4.7.1"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=9.3.0"
Pillow = "~=9.0.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
@@ -41,7 +45,7 @@ psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4"
PyPOM = "~=2.2.0"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
@@ -75,12 +79,13 @@ django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
"z3c.rml" = "*"
pikepdf = "*"
django-queryable-properties = "*"
django-mass-edit = "*"
selenium = "~=3.141.0"
[dev-packages]
pycodestyle = "~=2.9.1"
selenium = "~=3.141.0"
pycodestyle = "*"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
@@ -89,11 +94,14 @@ pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
pytest-xdist = {extras = [ "psutil",], version = "*"}
PyPOM = {extras = [ "splinter",], version = "*"}
[requires]
python_version = "3.10"
python_version = "3.9"
[pipenv]
allow_prereleases = true
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

1344
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,6 @@ STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
CSRF_TRUSTED_ORIGINS = []
if STAGING:
ALLOWED_HOSTS.append('.herokuapp.com')
@@ -36,7 +35,6 @@ if DEBUG:
ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1')
CSRF_TRUSTED_ORIGINS.append('.preview.app.github.dev')
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG:
@@ -44,9 +42,8 @@ if not DEBUG:
INTERNAL_IPS = ['127.0.0.1']
DOMAIN = env('DOMAIN', default='example.com')
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
if DEBUG:
ADMINS.append(('Testing Superuser', 'superuser@example.com'))

View File

@@ -124,22 +124,6 @@ class EventForm(forms.ModelForm):
'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):
tos = forms.BooleanField(required=True, label="Terms of hire")
name = forms.CharField(label="Your Name")
@@ -233,7 +217,7 @@ class EventChecklistForm(forms.ModelForm):
for key in vehicles:
pk = int(key.split('_')[1])
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')
else:
try:

View File

@@ -1,38 +0,0 @@
import premailer
import datetime
from django.template.loader import get_template
from django.contrib.staticfiles import finders
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.mail import EmailMultiAlternatives
from django.utils import timezone
from django.urls import reverse
from RIGS import models
class Command(BaseCommand):
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
def handle(self, *args, **options):
events = models.Event.objects.current_events().select_related('riskassessment')
for event in events:
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
# 48 hours = 172800 seconds
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
context = {
"event": event,
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
}
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
msg = EmailMultiAlternatives(
f"{event} - Risk Assessment Incomplete",
get_template("email/ra_reminder.txt").render(context),
to=[target],
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
msg.send()

View File

@@ -1,39 +0,0 @@
# Generated by Django 3.2.16 on 2022-12-16 14:41
import RIGS.validators
from django.db import migrations, models
import django.db.models.deletion
import versioning.versioning
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0045_alter_profile_is_approved'),
]
operations = [
migrations.CreateModel(
name='Subhire',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, default='')),
('status', models.IntegerField(choices=[(0, 'Provisional'), (1, 'Confirmed'), (2, 'Booked'), (3, 'Cancelled')], default=0)),
('start_date', models.DateField()),
('start_time', models.TimeField(blank=True, null=True)),
('end_date', models.DateField(blank=True, null=True)),
('end_time', models.TimeField(blank=True, null=True)),
('purchase_order', models.CharField(blank=True, default='', max_length=255, verbose_name='PO')),
('insurance_value', models.DecimalField(decimal_places=2, max_digits=10)),
('quote', models.URLField(default='', validators=[RIGS.validators.validate_url])),
('events', models.ManyToManyField(to='RIGS.Event')),
('organisation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.organisation')),
('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.person')),
],
options={
'permissions': [('subhire_finance', 'Can see financial data for subhire - insurance values')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
),
]

View File

@@ -8,7 +8,7 @@ from urllib.parse import urlparse
import pytz
from django import forms
from django.db.models import Q
from django.db.models import Q, F
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
@@ -17,10 +17,9 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from reversion import revisions as reversion
from reversion.models import Version
from versioning.versioning import RevisionMixin
from .validators import validate_url
def filter_by_pk(filt, query):
# try and parse an int
@@ -262,15 +261,15 @@ class EventManager(models.Manager):
'venue', 'mic')
return events
def active_dry_hires(self):
return self.filter(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)
def rig_count(self):
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
event_count = self.filter(
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
is_rig=True)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)) # Active dry hire
is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
status=Event.CANCELLED)) # Active dry hire
).count()
return event_count
@@ -305,29 +304,8 @@ class EventManager(models.Manager):
return qs
def find_earliest_event_time(event, datetime_list):
# If there is no start time defined, pretend it's midnight
startTimeFaked = False
if event.has_start_time:
startDateTime = datetime.datetime.combine(event.start_date, event.start_time)
else:
startDateTime = datetime.datetime.combine(event.start_date, datetime.time(00, 00))
startTimeFaked = True
# timezoneIssues - apply the default timezone to the naiive datetime
tz = pytz.timezone(settings.TIME_ZONE)
startDateTime = tz.localize(startDateTime)
datetime_list.append(startDateTime) # then add it to the list
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
# if we faked it & it's the earliest, better own up
if startTimeFaked and earliest == startDateTime:
return event.start_date
return earliest
class BaseEvent(models.Model, RevisionMixin):
@reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database
PROVISIONAL = 0
CONFIRMED = 1
@@ -343,97 +321,31 @@ class BaseEvent(models.Model, RevisionMixin):
name = models.CharField(max_length=255)
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
description = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, default='')
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True)
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
null=True)
# Timing
start_date = models.DateField()
start_time = models.TimeField(blank=True, null=True)
end_date = models.DateField(blank=True, null=True)
end_time = models.TimeField(blank=True, null=True)
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)
meet_at = models.DateTimeField(blank=True, null=True)
# Dry-hire only
# Crew management
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
on_delete=models.CASCADE)
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
verbose_name="MIC", on_delete=models.CASCADE)
# Monies
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
# Authorisation request details
@@ -483,10 +395,26 @@ class Event(BaseEvent):
def total(self):
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
@property
def cancelled(self):
return (self.status == self.CANCELLED)
@property
def confirmed(self):
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
@property
def hs_done(self):
return self.riskassessment is not None and len(self.checklists.all()) > 0
@property
def has_start_time(self):
return self.start_time is not None
@property
def has_end_time(self):
return self.end_time is not None
@property
def earliest_time(self):
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
@@ -500,47 +428,73 @@ class Event(BaseEvent):
if self.meet_at:
datetime_list.append(self.meet_at)
earliest = find_earliest_event_time(self, datetime_list)
# If there is no start time defined, pretend it's midnight
startTimeFaked = False
if self.has_start_time:
startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
else:
startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
startTimeFaked = True
# timezoneIssues - apply the default timezone to the naiive datetime
tz = pytz.timezone(settings.TIME_ZONE)
startDateTime = tz.localize(startDateTime)
datetime_list.append(startDateTime) # then add it to the list
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
# if we faked it & it's the earliest, better own up
if startTimeFaked and earliest == startDateTime:
return self.start_date
return earliest
@property
def latest_time(self):
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
tz = pytz.timezone(settings.TIME_ZONE)
endDate = self.end_date
if endDate is None:
endDate = self.start_date
if self.has_end_time:
endDateTime = datetime.datetime.combine(endDate, self.end_time)
tz = pytz.timezone(settings.TIME_ZONE)
endDateTime = tz.localize(endDateTime)
return endDateTime
else:
return endDate
@property
def internal(self):
return bool(self.organisation and self.organisation.union_account)
@property
def authorised(self):
if self.internal and hasattr(self, 'authorisation'):
if self.internal:
return self.authorisation.amount == self.total
else:
return bool(self.purchase_order)
@property
def color(self):
if self.cancelled:
return "secondary"
elif not self.is_rig:
return "info"
elif not self.mic:
return "danger"
elif self.confirmed and self.authorised:
if self.dry_hire or self.riskassessment:
return "success"
else:
return "warning"
else:
return "warning"
objects = EventManager()
def get_absolute_url(self):
return reverse('event_detail', kwargs={'pk': self.pk})
def get_edit_url(self):
return reverse('event_update', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.display_id}: {self.name}"
def clean(self):
errdict = super().clean()
errdict = {}
if self.end_date and self.start_date > self.end_date:
errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
startEndSameDay = not self.end_date or self.end_date == self.start_date
hasStartAndEnd = self.has_start_time and self.has_end_time
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
if self.access_at is not None:
if self.access_at.date() > self.start_date:
@@ -601,56 +555,6 @@ class EventAuthorisation(models.Model, RevisionMixin):
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):
def outstanding_invoices(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
@@ -777,6 +681,14 @@ class Payment(models.Model, RevisionMixin):
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
class RiskAssessment(models.Model, RevisionMixin):
SMALL = (0, 'Small')

View File

@@ -58,13 +58,13 @@ def send_eventauthorisation_success_email(instance):
client_email = EmailMultiAlternatives(
subject,
get_template("email/eventauthorisation_client_success.txt").render(context),
get_template("eventauthorisation_client_success.txt").render(context),
to=[instance.email],
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
css = finders.find('css/email.css')
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
@@ -82,7 +82,7 @@ def send_eventauthorisation_success_email(instance):
mic_email = EmailMessage(
subject,
get_template("email/eventauthorisation_mic_success.txt").render(context),
get_template("eventauthorisation_mic_success.txt").render(context),
to=[mic_email_address]
)
@@ -117,12 +117,12 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
email = EmailMultiAlternatives(
f"{context['number_of_users']} new users awaiting approval on RIGS",
get_template("email/admin_awaiting_approval.txt").render(context),
get_template("admin_awaiting_approval.txt").render(context),
to=[admin.email],
reply_to=[user.email],
)
css = finders.find('css/email.css')
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
external_styles=css).transform()
email.attach_alternative(html, 'text/html')
email.send()

View File

@@ -30,8 +30,6 @@
{% if perms.RIGS.add_event %}
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
New Event</a>
<a class="dropdown-item" href="{% url 'subhire_create' %}"><span class="fas fa-truck"></span>
New Subhire</a>
{% endif %}
</div>
</li>

View File

@@ -1,131 +1,197 @@
{% extends 'base_rigs.html' %}
{% load static %}
{% block js %}
<script src="{% static 'js/moment.js' %}"></script>
<script>
$(document).ready(function() {
// set some button listeners
$('#today-button').click(function(){ calendar.today(); });
$('#go-to-date-input').change(function(){
if(moment($('#go-to-date-input').val()).isValid()){
document.getElementById('go-to-date-button').classList.remove('disabled');
document.getElementById('go-to-date-button').href = "?month=" + moment($('#go-to-date-input').val()).format("YYYY-MM");
} else{
document.getElementById('go-to-date-button').classList.add('disabled');
}
});
});
</script>
{% endblock %}
{% block title %}Calendar{% 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%;
}
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
{% endblock %}
.day {
display:contents;
}
.day-label {
grid-row-start: 1;
text-align: right;
margin:0;
font-size: 1em !important;
height: 1em;
}
{% block js %}
<script src="{% static 'js/moment.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
<script>
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');
.week-day, .day-label, .event {
padding: 4px 10px;
}
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
}
.event {
background-color: #CCC;
font-size: 0.8em !important;
white-space: nowrap;
overflow: hidden;
}
thisEvent = {
'start': $(this).attr('earliest'),
'end': end,
'className': 'modal-href',
'title': $(this).attr('title'),
'url': $(this).attr('url'),
'allDay': allDay
}
.event-end {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
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);
.event-start {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
// 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);
}
.week-day {
font-size: 0.8em;
}
// 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;
@media (max-width: 767.98px) {
.event {
padding: 2px;
}
}
case 'timeGridWeek':
$month.removeClass('active');
$week.addClass('active');
$day.removeClass('active');
break;
[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>
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
$('#next-button').click(function(){ calendar.next(); });
$('#prev-button').click(function(){ calendar.prev(); });
$('#today-button').click(function(){ calendar.today(); });
$('#month-button').click(function(){ calendar.changeView('dayGridMonth'); });
$('#week-button').click(function(){ calendar.changeView('timeGridWeek'); });
$('#day-button').click(function(){ calendar.changeView('timeGridDay'); });
$('#go-to-date-input').change(function(){
if(moment($('#go-to-date-input').val()).isValid()){
$('#go-to-date-button').prop('disabled', false);
} else{
$('#go-to-date-button').prop('disabled', true);
}
});
$('#go-to-date-button').click(function(){
day = moment($('#go-to-date-input').val());
if(day.isValid()){
calendar.gotoDate(day.format("YYYY-MM-DD"));
} else{
alert('Invalid Date');
}
});
{% if view and date %}
// Go to the initial settings, if they're valid
view = viewFromUrl['{{view}}'];
calendar.changeView(view);
day = moment('{{date}}');
if(day.isValid()){
calendar.gotoDate(day.format("YYYY-MM-DD"));
} else{
console.log('Supplied date is invalid - using default')
}
{% endif %}
});
</script>
{% endblock %}
{% block content %}
<div class="row justify-content-center mb-1">
<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="form-inline col-4">
<div class="input-group">
<input type="date" id="go-to-date-input" placeholder="Go to date..." class="form-control">
<span class="input-group-append">
<a class="btn btn-success" id="go-to-date-button">Go!</a>
</span>
<div class="row">
<div class="col-sm-12">
<div class="pull-left">
<span id="calendar-header" class="h2"></span>
</div>
<div class="form-inline float-right btn-page my-3">
<div class="input-group mx-2">
<input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
<span class="input-group-append">
<button class="btn btn-success" id="go-to-date-button" type="button" disabled>Go!</button>
</span>
</div>
<div class="btn-group mx-2">
<button type="button" class="btn btn-primary" id="today-button">Today</button>
</div>
<div class="btn-group mx-2">
<button type="button" class="btn btn-secondary" id="prev-button"><span class="fas fa-chevron-left"></span></button>
<button type="button" class="btn btn-secondary" id="next-button"><span class="fas fa-chevron-right"></span></button>
</div>
<div class="btn-group ml-2">
<button type="button" class="btn btn-light" id="month-button">Month</button>
<button type="button" class="btn btn-light" id="week-button">Week</button>
<button type="button" class="btn btn-light" id="day-button">Day</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div id='calendar'></div>
</div>
</div>
<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 %}

View File

@@ -1,29 +0,0 @@
{% extends 'base_rigs.html' %}
{% load button from filters %}
{% block content %}
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">Upcoming Events</div>
<div class="card-body">{{ rig_count }}</div>
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">Upcoming Subhire</div>
<div class="card-body">{{ subhire_count }}</div>
<div class="card-footer"><a href={% url 'subhire_list' %}>View</a></div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">Active Dry Hires</div>
<div class="card-body">{{ hire_count }}</div>
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
</div>
</div>
</div>
{% endblock %}

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

@@ -1,16 +0,0 @@
{% extends 'base_client_email.html' %}
{% block content %}
<p>Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},</p>
{% if event.mic %}
<p>Just to let you know your event {{event.display_id}} <em>requires<em> a pre-event risk assessment completing prior to the event. Please do so as soon as possible.</p>
{% else %}
<p>This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.</p>
{% endif %}
<p>Fill it out here:</p>
<a href="{{url}}" class="btn btn-info"><span class="fas fa-paperclip"></span> Create Risk Assessment</a>
<p>TEC PA &amp; Lighting</p>
{% endblock %}

View File

@@ -1,9 +0,0 @@
Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},
{% if event.mic %}
Just to let you know your event {{event.display_id}} requires a risk assessment completing prior to the event. Please do so as soon as possible.
{% else %}
This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.
{% endif %}
The TEC Rig Information Gathering System

View File

@@ -25,10 +25,12 @@
{% include 'partials/hs_details.html' %}
</div>
{% endif %}
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
<div class="col-md-8 py-3">
{% include 'partials/auth_details.html' %}
</div>
{% if event.is_rig %}
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
<div class="col-md-8 py-3">
{% include 'partials/auth_details.html' %}
</div>
{% endif %}
{% endif %}
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
@@ -45,15 +47,9 @@
<hr>
<p class="dont-break-out">{{ event.notes|markdown }}</p>
{% endif %}
<h4>Event Items</h4>
<br>
{% include 'partials/item_table.html' %}
</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>
{% if not request.is_ajax and perms.RIGS.view_event %}

View File

@@ -106,10 +106,6 @@
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>
</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>

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.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,5 +47,7 @@
class="fas fa-pound-sign"></span>
<span class="d-none d-sm-inline">Invoice</span></a>
{% 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 %}
</div>

View File

@@ -12,7 +12,21 @@
</thead>
<tbody>
{% for event in events %}
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<tr class="{% if event.cancelled %}
table-secondary
{% elif not event.is_rig %}
table-info
{% elif not event.mic %}
table-danger
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% else %}
table-warning
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times-->
@@ -42,7 +56,7 @@
<!---Details-->
<td id="event_details" class="w-100">
<h4>
<a href="{{event.get_absolute_url}}">
<a href="{% url 'event_detail' event.pk %}">
{{ event.name }}
</a>
{% if event.venue %}

View File

@@ -15,7 +15,7 @@
<button type="button" class="btn btn-success btn-sm item-add"
data-toggle="modal"
data-target="#itemModal">
<span class="fas fa-plus"></span> Add Item
<i class="fas fa-plus"></i> Add Item
</button>
</th>
{% endif %}

View File

@@ -1,76 +0,0 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %}
{% load button from filters %}
{% block content %}
<div class="row my-3 py-3">
<div class="col-sm-12 text-right mb-2">
{% button 'edit' 'subhire_update' object.pk %}
</div>
<div class="col-md-6">
{% include 'partials/contact_details.html' %}
</div>
<div class="col-md-6">
<div class="card card-default">
<div class="card-header">Hire Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Name</dt>
<dd class="col-sm-6">{{ object.name }}</dd>
<dt class="col-sm-6">Event Starts</dt>
<dd class="col-sm-6">{{ object.start_date|date:"D d M Y" }} {{ object.start_time|date:"H:i" }}</dd>
<dt class="col-sm-6">Event Ends</dt>
<dd class="col-sm-6">{{ object.end_date|date:"D d M Y" }} {{ object.end_time|date:"H:i" }}</dd>
<dt class="col-sm-6">Status</dt>
<dd class="col-sm-6">{{ object.get_status_display }}</dd>
<dt class="col-sm-6">PO</dt>
<dd class="col-sm-6">{{ object.po }}</dd>
</dl>
</div>
</div>
</div>
<div class="col-md-6 mt-2">
<div class="card card-default">
<div class="card-header">Equipment Information</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Description</dt>
<dd class="col-sm-6">{{ object.description }}</dd>
{% if perms.RIGS.subhire_finance %}
<dt class="col-sm-6">Insurance Value</dt>
<dd class="col-sm-6">£{{ object.insurance_value }}</dd>
{% endif %}
<dt class="col-sm-6">Quote</dt>
<dd class="col-sm-6"><a href="{{ object.quote }}">View</a></dd>
</dl>
</div>
</div>
</div>
<div class="col-md-12 mt-2">
<div class="card card-default">
<div class="card-header">Associated Event(s)</div>
{% with object.events.all as events %}
{% include 'partials/event_table.html' %}
{%endwith%}
</div>
</div>
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/last_edited.html' with target="event_history" %}
</div>
{% endif %}
</div>
{% endblock %}
{% if request.is_ajax %}
{% block footer %}
{% if perms.RIGS.view_event %}
{% include 'partials/last_edited.html' with target="event_history" %}
{% endif %}
<a href="{% url 'subhire_detail' object.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
{% endblock %}
{% endif %}

View File

@@ -1,202 +0,0 @@
{% extends 'base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load multiply from filters %}
{% load button from filters %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
$(document).ready(function () {
setupMDE('#id_description');
});
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %}
<form class="row" role="form" method="POST">
{% csrf_token %}
<div class="col-12">
{% include 'form_errors.html' %}
</div>
{# Contact details #}
<div class="col-md-6 mb-2">
<div class="card">
<div class="card-header">Contact Details</div>
<div class="card-body">
<div class="form-group" data-toggle="tooltip">
<label for="{{ form.person.id_for_label }}">Primary Contact</label>
<div class="row">
<div class="col-9">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
{% if person %}
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
{% endif %}
</select>
</div>
<div class="col-3 align-right">
<div class="btn-group">
<a href="{% url 'person_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.person.value %}href="{% url 'person_update' form.person.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.person.id_for_label }}-update" data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-user-edit"></span>
</a>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="{{ form.organisation.id_for_label }}">Hire Company</label>
<div class="row">
<div class="col-9">
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}">
{% if organisation %}
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
{% endif %}
</select>
</div>
<div class="col-3 align-right">
<div class="btn-group">
<a href="{% url 'organisation_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.organisation.value %}href="{% url 'organisation_update' form.organisation.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.organisation.id_for_label }}-update" data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-2">
<div class="card">
<div class="card-header">Associated Event(s)</div>
<div class="card-body">
<div class="form-group">
<select multiple name="events" id="events_id" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='event' %}">
{% if object.events.count > 0 %}
{% for event in object.events.all %}
<option value="{{event.id}}" selected>{{ event }}</option>
{% endfor %}
{% endif %}
</select>
</div>
</div>
</div>
</div>
{# Event details #}
<div class="col-md-6 mb-2">
<div class="card card-default">
<div class="card-header">Hire Details</div>
<div class="card-body">
<div class="form-group" data-toggle="tooltip" title="Name of the event, displays on rigboard and on paperwork">
<label for="{{ form.name.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
<div class="col-sm-8">
{% render_field form.name class+="form-control" %}
</div>
</div>
<div class="form-group">
<label for="{{ form.start_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
{% render_field form.start_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="Start time of event, can be left blank">
{% render_field form.start_time class+="form-control" step="60" %}
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="{{ form.end_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
{% render_field form.end_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="End time of event, leave blank if unknown">
{% render_field form.end_time class+="form-control" step="60" %}
</div>
</div>
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
<label for="{{ form.status.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.status.label }}</label>
<div class="col-sm-8">
{% render_field form.status class+="form-control" %}
</div>
</div>
<div class="form-group">
<label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %}
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-2">
<div class="card">
<div class="card-header">Equipment Information</div>
<div class="card-body">
<div class="form-group">
<label for="{{ form.description.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
<div class="col-sm-12">
{% render_field form.description class+="form-control" %}
</div>
</div>
<div class="form-group">
<label for="{{ form.insurance_value.id_for_label }}"
class="col-sm-6 col-form-label">{{ form.insurance_value.label }}</label>
<div class="col-sm-8 input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.insurance_value class+="form-control" %}
</div>
<div class="border border-info p-2 rounded mt-1 font-weight-bold" style="border-width: thin thin thin thick !important;">
If this value is greater than £50,000 then please email productions@nottinghamtec.co.uk in addition to complete the additional insurance requirements
</div>
</div>
<div class="form-group">
<label for="{{ form.quote.id_for_label }}" class="col-sm-6 col-form-label">{{ form.quote.label }} (TEC SharePoint link)</label>
<div class="col-sm-12">{% render_field form.quote class+="form-control" %}</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12 text-right my-3">
{% button 'submit' %}
</div>
</form>
{% endblock %}

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
def get_field(obj, field, autoescape=True):
value = getattr(obj, field)
if (isinstance(value, bool)):
if(isinstance(value, bool)):
value = yesnoi(value, field in obj.inverted_fields)
elif (isinstance(value, str)):
elif(isinstance(value, str)):
value = truncatewords(value, 20)
return mark_safe(value)
@@ -144,7 +144,7 @@ def get_list(dictionary, key):
@register.filter
def profile_by_index(value):
if (value):
if(value):
return models.Profile.objects.get(pk=int(value))
else:
return ""

View File

@@ -43,8 +43,12 @@ urlpatterns = [
# Rigboard
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
re_path(r'^rigboard/calendar/$', login_required()(views.WebCalendar.as_view()),
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
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')),
@@ -66,28 +70,12 @@ urlpatterns = [
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
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
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()),
name='event_ra'),
path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()),
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentDetail.as_view()),
name='ra_detail'),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
name='ra_edit'),
@@ -99,7 +87,7 @@ urlpatterns = [
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
name='event_ec'),
path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()),
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistDetail.as_view()),
name='ec_detail'),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
name='ec_edit'),

View File

@@ -1,54 +0,0 @@
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

View File

@@ -1,10 +0,0 @@
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,5 +3,3 @@ from .finance import *
from .hs import *
from .ical import *
from .rigboard import *
from .subhire import *
from .dashboards import *

View File

@@ -1,14 +0,0 @@
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,6 +2,7 @@ import copy
import datetime
import re
import premailer
import simplejson
from django.conf import settings
from django.contrib import messages
@@ -11,7 +12,9 @@ from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
@@ -19,7 +22,7 @@ from django.views import generic
from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
from RIGS import models, forms, utils
from RIGS import models, forms
__author__ = 'ghost'
@@ -37,25 +40,14 @@ class RigboardIndex(generic.TemplateView):
return context
class WebCalendar(generic.ListView):
model = models.Event
class WebCalendar(generic.TemplateView):
template_name = 'calendar.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# use today's date for the calendar
d = utils.get_date(self.request.GET.get('month', None))
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")
context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '')
# context['page_title'] = "Calendar"
return context
@@ -69,6 +61,10 @@ class EventDetail(generic.DetailView, ModalURLMixin):
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
return context
@@ -346,12 +342,12 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
msg = EmailMultiAlternatives(
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
get_template("email/eventauthorisation_client_request.txt").render(context),
get_template("eventauthorisation_client_request.txt").render(context),
to=[email],
reply_to=[self.request.user.email],
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context),
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
@@ -361,7 +357,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
class EventAuthoriseRequestEmailPreview(generic.DetailView):
template_name = "email/eventauthorisation_client_request.html"
template_name = "eventauthorisation_client_request.html"
model = models.Event
def render_to_response(self, context, **response_kwargs):

View File

@@ -1,58 +0,0 @@
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

@@ -1,18 +0,0 @@
# 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):
qs = self.get_queryset()
if query is not None:
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query) | Q(nickname__icontains=query))
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query))
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@@ -125,7 +125,6 @@ class Asset(models.Model, RevisionMixin):
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])
comments = models.TextField(blank=True)
nickname = models.CharField(max_length=120, blank=True)
# Audit
last_audited_at = models.DateTimeField(blank=True, null=True)

View File

@@ -21,10 +21,6 @@
<label for="{{ form.description.id_for_label }}">Description</label>
{% render_field form.description|add_class:'form-control' value=object.description %}
</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">
<label for="{{ form.category.id_for_label }}" >Category</label>
{% render_field form.category|add_class:'form-control'%}
@@ -49,10 +45,7 @@
{% else %}
<dt>Asset ID</dt>
<dd>{{ object.asset_id }}</dd>
{% if object.nickname %}
<dt>Nickname</dt>
<dd>"{{ object.nickname }}"</dd>
{% endif %}
<dt>Description</dt>
<dd>{{ object.description }}</dd>

View File

@@ -24,6 +24,7 @@ function fonts(done) {
function styles(done) {
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
return gulp.src(['pipeline/source_assets/scss/**/*.scss',
'node_modules/fullcalendar/main.css',
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
'node_modules/easymde/dist/easymde.min.css'
@@ -58,6 +59,7 @@ function scripts() {
'node_modules/html5sortable/dist/html5sortable.min.js',
'node_modules/clipboard/dist/clipboard.min.js',
'node_modules/moment/moment.js',
'node_modules/fullcalendar/main.js',
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
'node_modules/easymde/dist/easymde.min.js',

5819
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@
"clipboard": "^2.0.8",
"cssnano": "^5.0.13",
"easymde": "^2.16.1",
"fullcalendar": "^5.10.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-flatten": "^0.4.0",
@@ -27,7 +28,7 @@
"jquery": "^3.6.0",
"konami": "^1.6.3",
"moment": "^2.29.4",
"node-sass": "^7.0.3",
"node-sass": "^7.0.0",
"popper.js": "^1.16.1",
"postcss": "^8.4.5",
"uglify-js": "^3.14.5"

View File

@@ -73,6 +73,7 @@ function initPicker(obj) {
return array;
}
};
console.log(obj.data);
if (!obj.data('noclear')) {
obj.prepend($("<option></option>")
.attr("value",'')

View File

@@ -4,16 +4,16 @@ Date.prototype.getISOString = function () {
var dd = this.getDate().toString();
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
};
$(document).ready(function () {
$(document).on('click', '.modal-href', function (e) {
$link = $(this);
jQuery(document).ready(function () {
jQuery(document).on('click', '.modal-href', function (e) {
$link = jQuery(this);
// Anti modal inception
if ($link.parents('#modal').length == 0) {
e.preventDefault();
modaltarget = $link.data('target');
modalobject = "";
$('#modal').load($link.attr('href'), function (e) {
$('#modal').modal();
jQuery('#modal').load($link.attr('href'), function (e) {
jQuery('#modal').modal();
});
}
});
@@ -23,6 +23,7 @@ $(document).ready(function () {
s.type = 'text/javascript';
document.body.appendChild(s);
s.src = '{% static "js/asteroids.min.js"%}';
ga('send', 'event', 'easter_egg', 'activated');
}
easter_egg.load();
});

View File

@@ -281,7 +281,3 @@ html.embedded {
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
padding-right: 1rem !important;
}
.badge-purple, .bg-purple {
background-color: #800080 !important;
}

View File

@@ -2,6 +2,7 @@ from django.urls import path
from django.contrib.auth.decorators import login_required
from training.decorators import is_supervisor
from PyRIGS.decorators import permission_required_with_403
from training import views, models
from versioning.views import VersionHistory
@@ -11,9 +12,10 @@ urlpatterns = [
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/<int:pk>/', login_required(views.TraineeDetail.as_view()),
path('trainee/<int:pk>/',
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
name='trainee_detail'),
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>/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>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
name='add_qualification'),
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),

View File

@@ -183,7 +183,7 @@ class ModelComparison:
def name(self):
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
else:
return str(obj)