mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-22 14:28:23 +00:00
Compare commits
9 Commits
948a41f43a
...
combine-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b16cf6333 | ||
|
|
7798f5c368 | ||
|
|
5c2e8b391c | ||
|
|
548bc1df81 | ||
|
|
c1d2bce8fb | ||
|
c71beab278
|
|||
|
259932a548
|
|||
|
7526485837
|
|||
| 39ed5aefb4 |
16
Pipfile
16
Pipfile
@@ -11,7 +11,7 @@ asgiref = "~=3.3.1"
|
|||||||
beautifulsoup4 = "~=4.9.3"
|
beautifulsoup4 = "~=4.9.3"
|
||||||
Brotli = "~=1.0.9"
|
Brotli = "~=1.0.9"
|
||||||
cachetools = "~=4.2.1"
|
cachetools = "~=4.2.1"
|
||||||
certifi = "*"
|
certifi = "~=2020.12.5"
|
||||||
chardet = "~=4.0.0"
|
chardet = "~=4.0.0"
|
||||||
configparser = "~=5.0.1"
|
configparser = "~=5.0.1"
|
||||||
contextlib2 = "~=0.6.0.post1"
|
contextlib2 = "~=0.6.0.post1"
|
||||||
@@ -33,7 +33,7 @@ envparse = "~=0.2.0"
|
|||||||
gunicorn = "~=20.0.4"
|
gunicorn = "~=20.0.4"
|
||||||
icalendar = "~=4.0.7"
|
icalendar = "~=4.0.7"
|
||||||
idna = "~=2.10"
|
idna = "~=2.10"
|
||||||
lxml = "*"
|
lxml = "~=4.7.1"
|
||||||
Markdown = "~=3.3.3"
|
Markdown = "~=3.3.3"
|
||||||
msgpack = "~=1.0.2"
|
msgpack = "~=1.0.2"
|
||||||
pep517 = "~=0.9.1"
|
pep517 = "~=0.9.1"
|
||||||
@@ -45,6 +45,7 @@ psycopg2 = "~=2.8.6"
|
|||||||
Pygments = "~=2.7.4"
|
Pygments = "~=2.7.4"
|
||||||
pyparsing = "~=2.4.7"
|
pyparsing = "~=2.4.7"
|
||||||
PyPDF2 = "~=1.27.5"
|
PyPDF2 = "~=1.27.5"
|
||||||
|
PyPOM = "~=2.2.0"
|
||||||
python-dateutil = "~=2.8.1"
|
python-dateutil = "~=2.8.1"
|
||||||
pytoml = "~=0.1.21"
|
pytoml = "~=0.1.21"
|
||||||
pytz = "~=2020.5"
|
pytz = "~=2020.5"
|
||||||
@@ -93,11 +94,14 @@ pluggy = "*"
|
|||||||
pytest-splinter = "*"
|
pytest-splinter = "*"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-reverse = "*"
|
pytest-reverse = "*"
|
||||||
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
|
||||||
PyPOM = {extras = [ "splinter",], version = "*"}
|
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.9"
|
python_version = "3.9"
|
||||||
|
|
||||||
[pipenv]
|
[dev-packages.pytest-xdist]
|
||||||
allow_prereleases = true
|
extras = [ "psutil",]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dev-packages.PyPOM]
|
||||||
|
extras = [ "splinter",]
|
||||||
|
version = "*"
|
||||||
|
|||||||
1065
Pipfile.lock
generated
1065
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -124,22 +124,6 @@ class EventForm(forms.ModelForm):
|
|||||||
'purchase_order', 'collector']
|
'purchase_order', 'collector']
|
||||||
|
|
||||||
|
|
||||||
class SubhireForm(forms.ModelForm):
|
|
||||||
related_models = {
|
|
||||||
'person': models.Person,
|
|
||||||
'organisation': models.Organisation,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
|
||||||
self.fields['end_date'].widget.format = '%Y-%m-%d'
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Subhire
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||||
tos = forms.BooleanField(required=True, label="Terms of hire")
|
tos = forms.BooleanField(required=True, label="Terms of hire")
|
||||||
name = forms.CharField(label="Your Name")
|
name = forms.CharField(label="Your Name")
|
||||||
|
|||||||
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-10-20 23:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0044_profile_is_supervisor'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='is_approved',
|
||||||
|
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
173
RIGS/models.py
173
RIGS/models.py
@@ -36,7 +36,7 @@ class Profile(AbstractUser):
|
|||||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||||
phone = models.CharField(max_length=13, blank=True, default='')
|
phone = models.CharField(max_length=13, blank=True, default='')
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||||
is_approved = models.BooleanField(default=False)
|
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
||||||
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||||
dark_theme = models.BooleanField(default=False)
|
dark_theme = models.BooleanField(default=False)
|
||||||
@@ -304,29 +304,8 @@ class EventManager(models.Manager):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def find_earliest_event_time(event, datetime_list):
|
@reversion.register(follow=['items'])
|
||||||
# If there is no start time defined, pretend it's midnight
|
class Event(models.Model, RevisionMixin):
|
||||||
startTimeFaked = False
|
|
||||||
if event.has_start_time:
|
|
||||||
startDateTime = datetime.datetime.combine(event.start_date, event.start_time)
|
|
||||||
else:
|
|
||||||
startDateTime = datetime.datetime.combine(event.start_date, datetime.time(00, 00))
|
|
||||||
startTimeFaked = True
|
|
||||||
|
|
||||||
# timezoneIssues - apply the default timezone to the naiive datetime
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
startDateTime = tz.localize(startDateTime)
|
|
||||||
datetime_list.append(startDateTime) # then add it to the list
|
|
||||||
|
|
||||||
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
|
||||||
|
|
||||||
# if we faked it & it's the earliest, better own up
|
|
||||||
if startTimeFaked and earliest == startDateTime:
|
|
||||||
return event.start_date
|
|
||||||
return earliest
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEvent(models.Model, RevisionMixin):
|
|
||||||
# Done to make it much nicer on the database
|
# Done to make it much nicer on the database
|
||||||
PROVISIONAL = 0
|
PROVISIONAL = 0
|
||||||
CONFIRMED = 1
|
CONFIRMED = 1
|
||||||
@@ -342,85 +321,31 @@ class BaseEvent(models.Model, RevisionMixin):
|
|||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
|
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
description = models.TextField(blank=True, default='')
|
description = models.TextField(blank=True, default='')
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||||
|
dry_hire = models.BooleanField(default=False)
|
||||||
|
is_rig = models.BooleanField(default=True)
|
||||||
|
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
# Timing
|
# Timing
|
||||||
start_date = models.DateField()
|
start_date = models.DateField()
|
||||||
start_time = models.TimeField(blank=True, null=True)
|
start_time = models.TimeField(blank=True, null=True)
|
||||||
end_date = models.DateField(blank=True, null=True)
|
end_date = models.DateField(blank=True, null=True)
|
||||||
end_time = models.TimeField(blank=True, null=True)
|
end_time = models.TimeField(blank=True, null=True)
|
||||||
|
|
||||||
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cancelled(self):
|
|
||||||
return (self.status == self.CANCELLED)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def confirmed(self):
|
|
||||||
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_start_time(self):
|
|
||||||
return self.start_time is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_end_time(self):
|
|
||||||
return self.end_time is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_time(self):
|
|
||||||
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
endDate = self.end_date
|
|
||||||
if endDate is None:
|
|
||||||
endDate = self.start_date
|
|
||||||
|
|
||||||
if self.has_end_time:
|
|
||||||
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
endDateTime = tz.localize(endDateTime)
|
|
||||||
|
|
||||||
return endDateTime
|
|
||||||
|
|
||||||
else:
|
|
||||||
return endDate
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
|
||||||
class Event(BaseEvent):
|
|
||||||
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
|
||||||
verbose_name="MIC", on_delete=models.CASCADE)
|
|
||||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
dry_hire = models.BooleanField(default=False)
|
|
||||||
is_rig = models.BooleanField(default=True)
|
|
||||||
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
|
||||||
null=True)
|
|
||||||
|
|
||||||
access_at = models.DateTimeField(blank=True, null=True)
|
access_at = models.DateTimeField(blank=True, null=True)
|
||||||
meet_at = models.DateTimeField(blank=True, null=True)
|
meet_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
# Dry-hire only
|
# Crew management
|
||||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
|
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
||||||
|
verbose_name="MIC", on_delete=models.CASCADE)
|
||||||
|
|
||||||
# Monies
|
# Monies
|
||||||
|
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||||
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||||
|
|
||||||
# Authorisation request details
|
# Authorisation request details
|
||||||
@@ -470,10 +395,26 @@ class Event(BaseEvent):
|
|||||||
def total(self):
|
def total(self):
|
||||||
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancelled(self):
|
||||||
|
return (self.status == self.CANCELLED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def confirmed(self):
|
||||||
|
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hs_done(self):
|
def hs_done(self):
|
||||||
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_start_time(self):
|
||||||
|
return self.start_time is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_end_time(self):
|
||||||
|
return self.end_time is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def earliest_time(self):
|
def earliest_time(self):
|
||||||
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
@@ -487,10 +428,45 @@ class Event(BaseEvent):
|
|||||||
if self.meet_at:
|
if self.meet_at:
|
||||||
datetime_list.append(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
|
return earliest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_time(self):
|
||||||
|
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDate = self.end_date
|
||||||
|
if endDate is None:
|
||||||
|
endDate = self.start_date
|
||||||
|
|
||||||
|
if self.has_end_time:
|
||||||
|
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDateTime = tz.localize(endDateTime)
|
||||||
|
|
||||||
|
return endDateTime
|
||||||
|
|
||||||
|
else:
|
||||||
|
return endDate
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def internal(self):
|
def internal(self):
|
||||||
return bool(self.organisation and self.organisation.union_account)
|
return bool(self.organisation and self.organisation.union_account)
|
||||||
@@ -511,7 +487,14 @@ class Event(BaseEvent):
|
|||||||
return f"{self.display_id}: {self.name}"
|
return f"{self.display_id}: {self.name}"
|
||||||
|
|
||||||
def clean(self):
|
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 is not None:
|
||||||
if self.access_at.date() > self.start_date:
|
if self.access_at.date() > self.start_date:
|
||||||
@@ -572,16 +555,6 @@ class EventAuthorisation(models.Model, RevisionMixin):
|
|||||||
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return f"S{self.pk:05d}"
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceManager(models.Manager):
|
class InvoiceManager(models.Manager):
|
||||||
def outstanding_invoices(self):
|
def outstanding_invoices(self):
|
||||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
bulletFontSize="10"/>
|
bulletFontSize="10"/>
|
||||||
</stylesheet>
|
</stylesheet>
|
||||||
|
|
||||||
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||||
<pageTemplate id="Headed" >
|
<pageTemplate id="Headed" >
|
||||||
<pageGraphics>
|
<pageGraphics>
|
||||||
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
||||||
|
|||||||
@@ -30,8 +30,6 @@
|
|||||||
{% if perms.RIGS.add_event %}
|
{% if perms.RIGS.add_event %}
|
||||||
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
|
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
|
||||||
New Event</a>
|
New Event</a>
|
||||||
<a class="dropdown-item" href="{% url 'subhire_create' %}"><span class="fas fa-truck"></span>
|
|
||||||
New Subhire</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -25,10 +25,12 @@
|
|||||||
{% include 'partials/hs_details.html' %}
|
{% include 'partials/hs_details.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
{% if event.is_rig %}
|
||||||
<div class="col-md-8 py-3">
|
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
||||||
{% include 'partials/auth_details.html' %}
|
<div class="col-md-8 py-3">
|
||||||
</div>
|
{% include 'partials/auth_details.html' %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,188 +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">
|
|
||||||
<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>
|
|
||||||
{# 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="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 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" 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 class="col-sm-12 text-right my-3">
|
|
||||||
{% button 'submit' %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -216,6 +216,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
|||||||
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
|
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
|
||||||
elif type == 'submit':
|
elif type == 'submit':
|
||||||
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
|
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
|
||||||
|
elif type == 'today':
|
||||||
|
return {'today': True, 'id': id}
|
||||||
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
|
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
RIGS/urls.py
10
RIGS/urls.py
@@ -70,16 +70,6 @@ urlpatterns = [
|
|||||||
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
|
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
|
||||||
name='event_duplicate'),
|
name='event_duplicate'),
|
||||||
|
|
||||||
|
|
||||||
# Subhire
|
|
||||||
path('subhire/<int:pk>/', 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_edit'),
|
|
||||||
|
|
||||||
|
|
||||||
# Event H&S
|
# Event H&S
|
||||||
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ from .finance import *
|
|||||||
from .hs import *
|
from .hs import *
|
||||||
from .ical import *
|
from .ical import *
|
||||||
from .rigboard import *
|
from .rigboard import *
|
||||||
from .subhire import *
|
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ class EventDetail(generic.DetailView, ModalURLMixin):
|
|||||||
if self.object.dry_hire:
|
if self.object.dry_hire:
|
||||||
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
||||||
context['page_title'] = title
|
context['page_title'] = title
|
||||||
|
if is_ajax(self.request):
|
||||||
|
context['override'] = "base_ajax.html"
|
||||||
|
else:
|
||||||
|
context['override'] = 'base_assets.html'
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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})
|
|
||||||
@@ -28,6 +28,7 @@ def admin_user(admin_user):
|
|||||||
admin_user.last_name = "Test"
|
admin_user.last_name = "Test"
|
||||||
admin_user.initials = "ETU"
|
admin_user.initials = "ETU"
|
||||||
admin_user.is_approved = True
|
admin_user.is_approved = True
|
||||||
|
admin_user.is_supervisor = True
|
||||||
admin_user.save()
|
admin_user.save()
|
||||||
return admin_user
|
return admin_user
|
||||||
|
|
||||||
|
|||||||
1285
package-lock.json
generated
1285
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,14 +27,14 @@
|
|||||||
"html5sortable": "^0.13.3",
|
"html5sortable": "^0.13.3",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"konami": "^1.6.3",
|
"konami": "^1.6.3",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.4",
|
||||||
"node-sass": "^7.0.0",
|
"node-sass": "^7.0.0",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"uglify-js": "^3.14.5"
|
"uglify-js": "^3.14.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browser-sync": "^2.27.7"
|
"browser-sync": "^2.27.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gulp": "gulp",
|
"gulp": "gulp",
|
||||||
|
|||||||
@@ -47,14 +47,16 @@ function initPicker(obj) {
|
|||||||
//log: 3,
|
//log: 3,
|
||||||
preprocessData: function (data) {
|
preprocessData: function (data) {
|
||||||
var i, l = data.length, array = [];
|
var i, l = data.length, array = [];
|
||||||
array.push({
|
if (!obj.data('noclear')) {
|
||||||
text: clearSelectionLabel,
|
array.push({
|
||||||
value: '',
|
text: clearSelectionLabel,
|
||||||
data:{
|
value: '',
|
||||||
update_url: '',
|
data:{
|
||||||
subtext:''
|
update_url: '',
|
||||||
}
|
subtext:''
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (l) {
|
if (l) {
|
||||||
for(i = 0; i < l; i++){
|
for(i = 0; i < l; i++){
|
||||||
@@ -71,11 +73,13 @@ function initPicker(obj) {
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
console.log(obj.data);
|
||||||
obj.prepend($("<option></option>")
|
if (!obj.data('noclear')) {
|
||||||
.attr("value",'')
|
obj.prepend($("<option></option>")
|
||||||
.text(clearSelectionLabel)
|
.attr("value",'')
|
||||||
.data('update_url','')); //Add "clear selection" option
|
.text(clearSelectionLabel)
|
||||||
|
.data('update_url','')); //Add "clear selection" option
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
{% load nice_errors from filters %}
|
{% load nice_errors from filters %}
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<div class="alert alert-danger alert-dismissable">
|
<div class="alert alert-danger mb-0">
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
|
||||||
<dl>
|
<dl>
|
||||||
{% with form|nice_errors as qq %}
|
{% with form|nice_errors as qq %}
|
||||||
{% for error_name,desc in qq.items %}
|
{% for error_name,desc in qq.items %}
|
||||||
<span class="row">
|
<span class="row">
|
||||||
<dt class="col-4">{{error_name}}</dt>
|
<dt class="col-3">{{error_name}}</dt>
|
||||||
<dd class="col-8">{{desc}}</dd>
|
<dd class="col-9">{{desc}}</dd>
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
<a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
||||||
{% elif copy %}
|
{% elif copy %}
|
||||||
<button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button>
|
<button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button>
|
||||||
|
{% elif today %}
|
||||||
|
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#{{id}}').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
<a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% include 'form_errors.html' %}
|
{% include 'form_errors.html' %}
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<p><strong>Please note:</strong> If it has been more than a year since you last logged in, your account will have been automatically deactivated. Contact <a href="mailto:it@nottinghamtec.co.uk">it@nottinghamtec.co.uk</a> for assistance.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="col-sm-6 offset-sm-3 col-lg-4 offset-lg-4">
|
<div class="col-sm-6 offset-sm-3 col-lg-4 offset-lg-4">
|
||||||
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from PyRIGS.decorators import user_passes_test_with_403
|
from PyRIGS.decorators import user_passes_test_with_403
|
||||||
|
|
||||||
|
|
||||||
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
|
def is_supervisor(login_url=None, oembed_view=None):
|
||||||
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor))
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def activity_feed_string(self):
|
def activity_feed_string(self):
|
||||||
return f"{self.trainee} {self.get_depth_display().lower()} {self.get_depth_display()} in {self.item}"
|
return f"{self.trainee} {self.get_depth_display().lower()} in {self.item}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_colour_from_depth(cls, depth):
|
def get_colour_from_depth(cls, depth):
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
|
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.training.add_trainingitemqualification or request.user.is_supervisor %}
|
{% if request.user.is_supervisor %}
|
||||||
<li class="nav-item"><a class="nav-link" href="{% url 'session_log' %}"><span class="fas fa-users"></span> Log Session</a></li>
|
<li class="nav-item"><a class="nav-link" href="{% url 'session_log' %}"><span class="fas fa-users"></span> Log Session</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item"><a class="nav-link" href="{% url 'training_activity_table' %}"><span class="fas fa-random"></span> Recent Changes</a></li>
|
<li class="nav-item"><a class="nav-link" href="{% url 'training_activity_table' %}"><span class="fas fa-random"></span> Recent Changes</a></li>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
|
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#id_date').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
|
{% button 'today' id='id_date' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
|
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if request.user.is_supervisor or perms.training.add_traininglevelrequirement %}
|
{% if request.user.is_supervisor %}
|
||||||
<div class="col-sm-12 text-right pr-0">
|
<div class="col-sm-12 text-right pr-0">
|
||||||
<a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button">
|
<a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button">
|
||||||
<span class="fas fa-plus"></span> Add New Requirement
|
<span class="fas fa-plus"></span> Add New Requirement
|
||||||
@@ -79,9 +79,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||||
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||||
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
|
{% if request.user.is_supervisor %}
|
||||||
<a type="button" class="btn btn-success" href="{% url 'add_qualification' object.pk %}" id="add_record">
|
<a type="button" class="btn btn-success" href="{% url 'add_qualification' object.pk %}" id="add_record">
|
||||||
<span class="fas fa-plus"></span> Add New Training Record
|
<span class="fas fa-plus"></span> Add New Training Record
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
|
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
|
||||||
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required>
|
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required data-noclear="true">
|
||||||
{% if supervisor %}
|
{% if supervisor %}
|
||||||
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
|
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -28,25 +28,26 @@
|
|||||||
{% include 'form_errors.html' %}
|
{% include 'form_errors.html' %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h3>People</h3>
|
<h3>People</h3>
|
||||||
<div class="form-group row">
|
<div class="form-group row" id="supervisor_group">
|
||||||
{% include 'partials/supervisor_field.html' %}
|
{% include 'partials/supervisor_field.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row" id="trainees_group">
|
||||||
<label for="trainees_id" class="col-sm-2">Select Attendees</label>
|
<label for="trainees_id" class="col-sm-2">Select Attendees</label>
|
||||||
<select multiple name="trainees" id="trainees_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
<select multiple name="trainees" id="trainees_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" data-noclear="true">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<h3>Training Items</h3>
|
<h3>Training Items</h3>
|
||||||
{% for depth in depths %}
|
{% for depth in depths %}
|
||||||
<div class="form-group row">
|
<div class="form-group row" id="{{depth.0}}">
|
||||||
<label for="selectpicker" class="col-sm-2 text-{% colour_from_depth depth.0 %} py-1">{{ depth.1 }} Items</label>
|
<label for="selectpicker" class="col-sm-2 text-{% colour_from_depth depth.0 %} py-1">{{ depth.1 }} Items</label>
|
||||||
<select multiple name="items_{{depth.0}}" id="items_{{depth.0}}_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active">
|
<select multiple name="items_{{depth.0}}" id="items_{{depth.0}}_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active" data-noclear="true">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<h3>Session Information</h3>
|
<h3>Session Information</h3>
|
||||||
<div class="form-group">
|
<div class="form-group row">
|
||||||
{% include 'partials/form_field.html' with field=form.date %}
|
{% include 'partials/form_field.html' with field=form.date col='col-sm-6' %}
|
||||||
|
{% button 'today' id='id_date' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{% include 'partials/form_field.html' with field=form.notes %}
|
{% include 'partials/form_field.html' with field=form.notes %}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Supervisor</th>
|
<th>Supervisor</th>
|
||||||
<th>Notes</th>
|
<th>Notes</th>
|
||||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
{% if request.user.is_supervisor %}
|
||||||
<th></th>
|
<th></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<td>{{ object.date }}</td>
|
<td>{{ object.date }}</td>
|
||||||
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
||||||
<td>{{ object.notes }}</td>
|
<td>{{ object.notes }}</td>
|
||||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
{% if request.user.is_supervisor %}
|
||||||
<td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
|
<td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ def training_item(db):
|
|||||||
training_item.delete()
|
training_item.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def training_item_2(db):
|
||||||
|
training_category = models.TrainingCategory.objects.create(reference_number=2, name="Sound")
|
||||||
|
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="Fundamentals of Audio")
|
||||||
|
yield training_item
|
||||||
|
training_category.delete()
|
||||||
|
training_item.delete()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def level(db):
|
def level(db):
|
||||||
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
|
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
|
||||||
|
|||||||
@@ -40,3 +40,42 @@ class AddQualification(FormPage):
|
|||||||
@property
|
@property
|
||||||
def success(self):
|
def success(self):
|
||||||
return 'add' not in self.driver.current_url
|
return 'add' not in self.driver.current_url
|
||||||
|
|
||||||
|
|
||||||
|
class SessionLog(FormPage):
|
||||||
|
URL_TEMPLATE = 'training/session_log'
|
||||||
|
|
||||||
|
_supervisor_selector = (By.CSS_SELECTOR, 'div#supervisor_group>div.bootstrap-select')
|
||||||
|
_trainees_selector = (By.CSS_SELECTOR, 'div#trainees_group>div.bootstrap-select')
|
||||||
|
_training_started_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||||
|
_training_complete_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||||
|
_competency_assessed_selector = (By.XPATH, '//div[1]/div/div/form/div[5]/div')
|
||||||
|
|
||||||
|
form_items = {
|
||||||
|
'date': (regions.DatePicker, (By.ID, 'id_date')),
|
||||||
|
'notes': (regions.TextBox, (By.ID, 'id_notes')),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supervisor_selector(self):
|
||||||
|
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trainees_selector(self):
|
||||||
|
return regions.BootstrapSelectElement(self, self.find_element(*self._trainees_selector))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def training_started_selector(self):
|
||||||
|
return regions.BootstrapSelectElement(self, self.find_element(*self._training_started_selector))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def training_complete_selector(self):
|
||||||
|
return regions.BootstrapSelectElement(self, self.find_element(*self._training_complete_selector))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def competency_assessed_selector(self):
|
||||||
|
return regions.BootstrapSelectElement(self, self.find_element(*self._competency_assessed_selector))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self):
|
||||||
|
return 'log' not in self.driver.current_url
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ from training import models
|
|||||||
from training.tests import pages
|
from training.tests import pages
|
||||||
|
|
||||||
|
|
||||||
|
def select_super(page, supervisor):
|
||||||
|
page.supervisor_selector.toggle()
|
||||||
|
assert page.supervisor_selector.is_open
|
||||||
|
page.supervisor_selector.search(supervisor.name[:-6])
|
||||||
|
time.sleep(2) # Slow down for javascript
|
||||||
|
assert page.supervisor_selector.options[0].selected
|
||||||
|
page.supervisor_selector.toggle()
|
||||||
|
|
||||||
|
|
||||||
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
|
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
|
||||||
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
|
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
|
||||||
# assert page.name in str(trainee)
|
# assert page.name in str(trainee)
|
||||||
@@ -30,12 +39,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
|||||||
assert page.item_selector.options[0].selected
|
assert page.item_selector.options[0].selected
|
||||||
page.item_selector.toggle()
|
page.item_selector.toggle()
|
||||||
|
|
||||||
page.supervisor_selector.toggle()
|
select_super(page, supervisor)
|
||||||
assert page.supervisor_selector.is_open
|
|
||||||
page.supervisor_selector.search(supervisor.name[:-6])
|
|
||||||
time.sleep(2) # Slow down for javascript
|
|
||||||
assert page.supervisor_selector.options[0].selected
|
|
||||||
page.supervisor_selector.toggle()
|
|
||||||
|
|
||||||
page.submit()
|
page.submit()
|
||||||
assert page.success
|
assert page.success
|
||||||
@@ -44,3 +48,32 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
|||||||
assert qualification.date == date
|
assert qualification.date == date
|
||||||
assert qualification.notes == "A note"
|
assert qualification.notes == "A note"
|
||||||
assert qualification.depth == models.TrainingItemQualification.STARTED
|
assert qualification.depth == models.TrainingItemQualification.STARTED
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_log(logged_in_browser, live_server, trainee, supervisor, training_item, training_item_2):
|
||||||
|
page = pages.SessionLog(logged_in_browser.driver, live_server.url).open()
|
||||||
|
|
||||||
|
page.date = date = datetime.date(2001, 1, 10)
|
||||||
|
page.notes = note = "A general note"
|
||||||
|
|
||||||
|
time.sleep(2) # Slow down for javascript
|
||||||
|
|
||||||
|
select_super(page, supervisor)
|
||||||
|
|
||||||
|
page.trainees_selector.toggle()
|
||||||
|
assert page.trainees_selector.is_open
|
||||||
|
page.trainees_selector.search(trainee.first_name)
|
||||||
|
time.sleep(2) # Slow down for javascript
|
||||||
|
page.trainees_selector.set_option(trainee.name, True)
|
||||||
|
# assert page.trainees_selector.options[0].selected
|
||||||
|
page.trainees_selector.toggle()
|
||||||
|
|
||||||
|
page.training_started_selector.toggle()
|
||||||
|
assert page.training_started_selector.is_open
|
||||||
|
page.training_started_selector.search(training_item.description[:-2])
|
||||||
|
time.sleep(2) # Slow down for javascript
|
||||||
|
# assert page.training_started_selector.options[0].selected
|
||||||
|
page.training_started_selector.toggle()
|
||||||
|
|
||||||
|
page.submit()
|
||||||
|
assert page.success
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def test_add_qualification(admin_client, trainee, admin_user, training_item):
|
|||||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||||
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
|
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
|
||||||
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
|
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
|
||||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': admin_user.pk, 'item': training_item.pk})
|
response = admin_client.post(url, {'date': date, 'trainee': admin_user.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||||
print(response.content)
|
print(response.content)
|
||||||
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from training.decorators import has_perm_or_supervisor
|
from training.decorators import is_supervisor
|
||||||
|
from PyRIGS.decorators import permission_required_with_403
|
||||||
|
|
||||||
from training import views, models
|
from training import views, models
|
||||||
from versioning.views import VersionHistory
|
from versioning.views import VersionHistory
|
||||||
@@ -12,22 +13,22 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||||
path('trainee/<int:pk>/',
|
path('trainee/<int:pk>/',
|
||||||
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||||
name='trainee_detail'),
|
name='trainee_detail'),
|
||||||
path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
path('trainee/<int:pk>/history', 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/', has_perm_or_supervisor('training.add_trainingitemqualification')(views.AddQualification.as_view()),
|
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
|
||||||
name='add_qualification'),
|
name='add_qualification'),
|
||||||
path('trainee/edit_qualification/<int:pk>/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()),
|
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
|
||||||
name='edit_qualification'),
|
name='edit_qualification'),
|
||||||
|
|
||||||
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
|
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
|
||||||
path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
||||||
path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
||||||
path('level/<int:pk>/add_requirement/', login_required(views.AddLevelRequirement.as_view()), name='add_requirement'),
|
path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'),
|
||||||
path('level/remove_requirement/<int:pk>/', login_required(views.RemoveRequirement.as_view()), name='remove_requirement'),
|
path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'),
|
||||||
|
|
||||||
path('trainee/<int:pk>/level/<int:level_pk>/confirm', login_required(views.ConfirmLevel.as_view()), name='confirm_level'),
|
path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'),
|
||||||
path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'),
|
path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'),
|
||||||
|
|
||||||
path('session_log', has_perm_or_supervisor('training.add_trainingitemqualification')(views.SessionLog.as_view()), name='session_log'),
|
path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'),
|
||||||
]
|
]
|
||||||
|
|||||||
26
users/management/commands/usercleanup.py
Normal file
26
users/management/commands/usercleanup.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from RIGS.models import Profile
|
||||||
|
from training.models import TrainingLevel
|
||||||
|
|
||||||
|
|
||||||
|
# This is triggered nightly by Heroku Scheduler
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Performs perodic user maintenance tasks'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
for person in Profile.objects.all():
|
||||||
|
# Inactivate users that have not logged in for a year (or have never logged in)
|
||||||
|
if person.last_login is None or (timezone.now() - person.last_login).days > 365:
|
||||||
|
person.is_active = False
|
||||||
|
person.is_approved = False
|
||||||
|
person.save()
|
||||||
|
# Ensure everyone with a supervisor level has the flag correctly set in the database
|
||||||
|
if person.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||||
|
.filter(level__level__gte=TrainingLevel.SUPERVISOR) \
|
||||||
|
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||||
|
.exclude(level__department__isnull=True).exists():
|
||||||
|
person.is_supervisor = True
|
||||||
|
person.save()
|
||||||
Reference in New Issue
Block a user