mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-15 02:59:41 +00:00
Compare commits
14 Commits
dependabot
...
1660f51e55
| Author | SHA1 | Date | |
|---|---|---|---|
|
1660f51e55
|
|||
|
9feea56211
|
|||
|
951227e68b
|
|||
|
6e8779c81b
|
|||
|
e0da6a3120
|
|||
|
0c80ef1b72
|
|||
|
0f127d8ca4
|
|||
|
04ec728972
|
|||
|
bede8b4176
|
|||
|
8cade512d1
|
|||
|
418219940b
|
|||
| 948a41f43a | |||
|
4449efcced
|
|||
|
8b0cd13159
|
15
Pipfile
15
Pipfile
@@ -21,8 +21,7 @@ dj-static = "~=0.0.6"
|
||||
Django = "~=3.2"
|
||||
django-debug-toolbar = "~=3.2"
|
||||
django-filter = "~=2.4.0"
|
||||
django-ical = "~=1.7.1"
|
||||
django-recurrence = "~=1.10.3"
|
||||
django-ical = "~=1.8.3"
|
||||
django-registration-redux = "~=2.9"
|
||||
django-reversion = "~=3.0.9"
|
||||
django-widget-tweaks = "~=1.4.8"
|
||||
@@ -76,7 +75,6 @@ django-hCaptcha = "*"
|
||||
importlib-metadata = "*"
|
||||
django-hcaptcha = "*"
|
||||
"z3c.rml" = "*"
|
||||
pikepdf = "*"
|
||||
django-queryable-properties = "*"
|
||||
django-mass-edit = "*"
|
||||
selenium = "~=3.141.0"
|
||||
@@ -91,14 +89,11 @@ pluggy = "*"
|
||||
pytest-splinter = "*"
|
||||
pytest = "*"
|
||||
pytest-reverse = "*"
|
||||
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
||||
PyPOM = {extras = [ "splinter",], version = "*"}
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
|
||||
[dev-packages.pytest-xdist]
|
||||
extras = [ "psutil",]
|
||||
version = "*"
|
||||
|
||||
[dev-packages.PyPOM]
|
||||
extras = [ "splinter",]
|
||||
version = "*"
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
||||
18
Pipfile.lock
generated
18
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "2e2fb4b609c10fc42db6bbd69ca73800629fbcaceec664e1fcc79d4b37bc0eb1"
|
||||
"sha256": "fcd984e26845bc82b41f2651b61696f8f19be81ea6fb770751596b53f7b2bd2b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -271,11 +271,11 @@
|
||||
},
|
||||
"django-ical": {
|
||||
"hashes": [
|
||||
"sha256:6df4dc61eb4abc55816bd16a949e497bea99828c7de648438ace7f1f85eeb405",
|
||||
"sha256:bd5c874d2eb81329f220174cc0dde7be385f4574ce6c8a2d1579d7fd564a94f3"
|
||||
"sha256:0d5595c5bc954e401b59b27a9a86962557f0d3b965e9f5860244cd6bc450e8ab",
|
||||
"sha256:d3f97d163c03ea795e0722d5031e7f3806037ac913c814b0cfee54464f06978e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.3"
|
||||
"version": "==1.8.3"
|
||||
},
|
||||
"django-mass-edit": {
|
||||
"hashes": [
|
||||
@@ -295,11 +295,10 @@
|
||||
},
|
||||
"django-recurrence": {
|
||||
"hashes": [
|
||||
"sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963",
|
||||
"sha256:9053b44b78b7fbfe3530673edfdd6d2f562105f8a192bc6a4b906a3df4f95f59"
|
||||
"sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5",
|
||||
"sha256:9c89444e651a78c587f352c5f63eda48ab2f53996347b9fcdff2d248f4fcff70"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.10.3"
|
||||
"version": "==1.11.1"
|
||||
},
|
||||
"django-registration-redux": {
|
||||
"hashes": [
|
||||
@@ -567,7 +566,6 @@
|
||||
"sha256:e3ff858d504312ea07519121d3ecdf1b5bc40541f957d2b85d271a2ca6284b70",
|
||||
"sha256:f721cd86d8ddf0624306fcd1fed3e086387fcb2e32baeafbd1ba92a7c40c5563"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.2.5"
|
||||
},
|
||||
"pillow": {
|
||||
@@ -1005,7 +1003,6 @@
|
||||
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.26.13"
|
||||
},
|
||||
"webencodings": {
|
||||
@@ -1572,7 +1569,6 @@
|
||||
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.26.13"
|
||||
},
|
||||
"wsproto": {
|
||||
|
||||
@@ -124,6 +124,22 @@ 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")
|
||||
|
||||
36
RIGS/migrations/0045_subhire.py
Normal file
36
RIGS/migrations/0045_subhire.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.2.12 on 2022-10-15 19:36
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import versioning.versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0044_profile_is_supervisor'),
|
||||
]
|
||||
|
||||
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)),
|
||||
('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={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0046_subhire_events.py
Normal file
18
RIGS/migrations/0046_subhire_events.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.16 on 2022-10-20 11:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0045_subhire'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subhire',
|
||||
name='events',
|
||||
field=models.ManyToManyField(to='RIGS.Event'),
|
||||
),
|
||||
]
|
||||
233
RIGS/models.py
233
RIGS/models.py
@@ -304,8 +304,29 @@ class EventManager(models.Manager):
|
||||
return qs
|
||||
|
||||
|
||||
@reversion.register(follow=['items'])
|
||||
class Event(models.Model, RevisionMixin):
|
||||
def find_earliest_event_time(event, datetime_list):
|
||||
# If there is no start time defined, pretend it's midnight
|
||||
startTimeFaked = False
|
||||
if event.has_start_time:
|
||||
startDateTime = datetime.datetime.combine(event.start_date, event.start_time)
|
||||
else:
|
||||
startDateTime = datetime.datetime.combine(event.start_date, datetime.time(00, 00))
|
||||
startTimeFaked = True
|
||||
|
||||
# timezoneIssues - apply the default timezone to the naiive datetime
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
startDateTime = tz.localize(startDateTime)
|
||||
datetime_list.append(startDateTime) # then add it to the list
|
||||
|
||||
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
||||
|
||||
# if we faked it & it's the earliest, better own up
|
||||
if startTimeFaked and earliest == startDateTime:
|
||||
return event.start_date
|
||||
return earliest
|
||||
|
||||
|
||||
class BaseEvent(models.Model, RevisionMixin):
|
||||
# Done to make it much nicer on the database
|
||||
PROVISIONAL = 0
|
||||
CONFIRMED = 1
|
||||
@@ -321,31 +342,97 @@ class Event(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)
|
||||
|
||||
# Crew management
|
||||
# Dry-hire only
|
||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||
on_delete=models.CASCADE)
|
||||
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
|
||||
@@ -395,26 +482,10 @@ class Event(models.Model, RevisionMixin):
|
||||
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"""
|
||||
@@ -428,73 +499,47 @@ class Event(models.Model, RevisionMixin):
|
||||
if self.meet_at:
|
||||
datetime_list.append(self.meet_at)
|
||||
|
||||
# If there is no start time defined, pretend it's midnight
|
||||
startTimeFaked = False
|
||||
if self.has_start_time:
|
||||
startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
|
||||
else:
|
||||
startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
|
||||
startTimeFaked = True
|
||||
|
||||
# timezoneIssues - apply the default timezone to the naiive datetime
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
startDateTime = tz.localize(startDateTime)
|
||||
datetime_list.append(startDateTime) # then add it to the list
|
||||
|
||||
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
||||
|
||||
# if we faked it & it's the earliest, better own up
|
||||
if startTimeFaked and earliest == startDateTime:
|
||||
return self.start_date
|
||||
earliest = find_earliest_event_time(self, datetime_list)
|
||||
|
||||
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:
|
||||
if self.internal and hasattr(self, 'authorisation'):
|
||||
return self.authorisation.amount == self.total
|
||||
else:
|
||||
return bool(self.purchase_order)
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
if self.cancelled:
|
||||
return "secondary"
|
||||
elif not self.is_rig:
|
||||
return "info"
|
||||
elif not self.mic:
|
||||
return "danger"
|
||||
elif self.confirmed and self.authorised:
|
||||
if self.dry_hire or self.riskassessment:
|
||||
return "success"
|
||||
else:
|
||||
return "warning"
|
||||
else:
|
||||
return "warning"
|
||||
|
||||
objects = EventManager()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_id}: {self.name}"
|
||||
def get_edit_url(self):
|
||||
return reverse('event_update', kwargs={'pk': self.pk})
|
||||
|
||||
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.']
|
||||
errdict = super.clean()
|
||||
|
||||
if self.access_at is not None:
|
||||
if self.access_at.date() > self.start_date:
|
||||
@@ -555,6 +600,44 @@ 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.filter(
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=timezone.now().date()) & ~models.Q(
|
||||
status=Event.CANCELLED)) # Ends after
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time').select_related('person', 'organisation')
|
||||
|
||||
return events
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
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 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
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
{% 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>
|
||||
|
||||
@@ -1,197 +1,131 @@
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Calendar{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
|
||||
{% endblock %}
|
||||
|
||||
{% 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');
|
||||
|
||||
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() {
|
||||
<script src="{% static 'js/moment.js' %}"></script>
|
||||
<script>
|
||||
$(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);
|
||||
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{
|
||||
$('#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 %}
|
||||
|
||||
{% block content %}
|
||||
<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 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>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary col-2" id="today-button">Today</button>
|
||||
<a class="btn btn-info mx-2 col-2" href="{% url 'web_calendar' %}?{{ next_month }}"><span class="fas fa-chevron-right"></span> Next Month</a>
|
||||
</div>
|
||||
|
||||
<div class="week" style="height: 2em;">
|
||||
<div class="week-day">Monday</div>
|
||||
<div class="week-day">Tuesday</div>
|
||||
<div class="week-day">Wednesday</div>
|
||||
<div class="week-day">Thursday</div>
|
||||
<div class="week-day">Friday</div>
|
||||
<div class="week-day">Saturday</div>
|
||||
<div class="week-day">Sunday</div>
|
||||
</div>
|
||||
{% for week in weeks %}
|
||||
<div class="week">
|
||||
{% for day in week %}
|
||||
{% if day.0 != 0 %}
|
||||
<div class="day" id="{{day.0}}">
|
||||
<h3 class="day-label text-muted">{{day.0}}</h3>
|
||||
{{ day.2|safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="day"><span style="grid-row-start: 1;"> <span></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,12 +25,10 @@
|
||||
{% include 'partials/hs_details.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||
<div class="col-sm-12 text-right">
|
||||
|
||||
@@ -106,6 +106,10 @@
|
||||
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>
|
||||
|
||||
@@ -12,21 +12,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<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">
|
||||
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||
<!---Number-->
|
||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||
<!--Dates & Times-->
|
||||
|
||||
27
RIGS/templates/subhire_detail.html
Normal file
27
RIGS/templates/subhire_detail.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
|
||||
{% load markdown_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3 py-3">
|
||||
<div class="col-md-6">
|
||||
{% include 'partials/contact_details.html' %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% include 'partials/event_details.html' %}
|
||||
</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 %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
192
RIGS/templates/subhire_form.html
Normal file
192
RIGS/templates/subhire_form.html
Normal file
@@ -0,0 +1,192 @@
|
||||
{% 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' %}"></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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 text-right my-3">
|
||||
{% button 'submit' %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
18
RIGS/urls.py
18
RIGS/urls.py
@@ -43,12 +43,8 @@ urlpatterns = [
|
||||
|
||||
# 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'),
|
||||
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')),
|
||||
|
||||
|
||||
@@ -70,6 +66,18 @@ 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>/', 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', views.SubhireEdit.as_view(),
|
||||
name='subhire_update'),
|
||||
path('subhire/upcoming', views.SubhireList.as_view(),
|
||||
name='subhire_list'),
|
||||
|
||||
|
||||
# Event H&S
|
||||
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
||||
|
||||
|
||||
53
RIGS/utils.py
Normal file
53
RIGS/utils.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from datetime import datetime, timedelta, date
|
||||
import calendar
|
||||
from calendar import HTMLCalendar
|
||||
from RIGS.models import BaseEvent, 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
|
||||
@@ -3,3 +3,4 @@ from .finance import *
|
||||
from .hs import *
|
||||
from .ical import *
|
||||
from .rigboard import *
|
||||
from .subhire import *
|
||||
|
||||
@@ -17,12 +17,13 @@ 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.html import mark_safe
|
||||
from django.utils.decorators import method_decorator
|
||||
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
|
||||
from RIGS import models, forms, utils
|
||||
|
||||
__author__ = 'ghost'
|
||||
|
||||
@@ -40,14 +41,25 @@ class RigboardIndex(generic.TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
class WebCalendar(generic.TemplateView):
|
||||
class WebCalendar(generic.ListView):
|
||||
model = models.Event
|
||||
template_name = 'calendar.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['view'] = kwargs.get('view', '')
|
||||
context['date'] = kwargs.get('date', '')
|
||||
# context['page_title'] = "Calendar"
|
||||
# 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")
|
||||
return context
|
||||
|
||||
|
||||
@@ -61,10 +73,6 @@ 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
|
||||
|
||||
|
||||
|
||||
58
RIGS/views/subhire.py
Normal file
58
RIGS/views/subhire.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, 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
|
||||
@@ -24,7 +24,6 @@ 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'
|
||||
@@ -59,7 +58,6 @@ 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',
|
||||
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -18,7 +18,6 @@
|
||||
"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",
|
||||
@@ -3062,11 +3061,6 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fullcalendar": {
|
||||
"version": "5.11.3",
|
||||
"resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-5.11.3.tgz",
|
||||
"integrity": "sha512-SgqiMEA+lWLyEd2jEwtIxdfx41j2CZr4KK00D2Gepj1MnGOjaEi13athnU6xvqMQXXjgJNj+vmlUP69QiuGncQ=="
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
@@ -11579,11 +11573,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fullcalendar": {
|
||||
"version": "5.11.3",
|
||||
"resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-5.11.3.tgz",
|
||||
"integrity": "sha512-SgqiMEA+lWLyEd2jEwtIxdfx41j2CZr4KK00D2Gepj1MnGOjaEi13athnU6xvqMQXXjgJNj+vmlUP69QiuGncQ=="
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"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",
|
||||
|
||||
@@ -73,7 +73,6 @@ function initPicker(obj) {
|
||||
return array;
|
||||
}
|
||||
};
|
||||
console.log(obj.data);
|
||||
if (!obj.data('noclear')) {
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
|
||||
@@ -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
|
||||
};
|
||||
jQuery(document).ready(function () {
|
||||
jQuery(document).on('click', '.modal-href', function (e) {
|
||||
$link = jQuery(this);
|
||||
$(document).ready(function () {
|
||||
$(document).on('click', '.modal-href', function (e) {
|
||||
$link = $(this);
|
||||
// Anti modal inception
|
||||
if ($link.parents('#modal').length == 0) {
|
||||
e.preventDefault();
|
||||
modaltarget = $link.data('target');
|
||||
modalobject = "";
|
||||
jQuery('#modal').load($link.attr('href'), function (e) {
|
||||
jQuery('#modal').modal();
|
||||
$('#modal').load($link.attr('href'), function (e) {
|
||||
$('#modal').modal();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -23,7 +23,6 @@ jQuery(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();
|
||||
});
|
||||
|
||||
@@ -281,3 +281,7 @@ html.embedded {
|
||||
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
.badge-purple, .bg-purple {
|
||||
background-color: #800080 !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user