From 01a0b8f831fa50821ce06f2e4eda93f1d61cce94 Mon Sep 17 00:00:00 2001 From: Arona Jones Date: Fri, 19 May 2023 10:28:51 +0000 Subject: [PATCH] Add event checkin functionality, seperate power tests into a different form (#536) * Split power related parts of event checklist into a seperate form * Revamp H&S overview, remove individual lists. They were not a good thing. * Remove old 'vehicle/crew' stuff * Very initial version of checkin form * Further work on checkin, add role field etc * Fix tests after form split * Add ability to edit checkins, more validation * Basic checkin/out logic complete * Add homepage checkin for events happening now * Minor improvement to homepage UI * Checkin button turns into checkout button where applicable * UI work * Clicking check out does not redirect the user * Register check in model with the admin site * Add power record status chip, checklist status chip displays number of checklists * Minor fixes * Implement codedoctor suggestions * pep8 * Add data migration for crew/vehicles * Checkin only requires login (no perms) and block users from editing other checkins at Django level --- PyRIGS/views.py | 1 + RIGS/admin.py | 6 + RIGS/forms.py | 111 +++----- .../commands/generateSampleRIGSData.py | 4 +- RIGS/migrations/0046_create_powertests.py | 71 +++++ RIGS/migrations/0047_auto_20230517_0944.py | 41 +++ RIGS/migrations/0048_auto_20230518_1256.py | 156 +++++++++++ RIGS/models.py | 152 ++++++---- RIGS/templates/base_rigs.html | 11 +- RIGS/templates/event_detail.html | 39 +++ RIGS/templates/hs/event_checklist_detail.html | 173 ------------ RIGS/templates/hs/event_checklist_form.html | 262 +----------------- RIGS/templates/hs/eventcheckin_form.html | 105 +++++++ RIGS/templates/hs/hs_list.html | 9 + RIGS/templates/hs/hs_object_list.html | 59 ---- RIGS/templates/hs/power_detail.html | 175 ++++++++++++ RIGS/templates/hs/power_form.html | 203 ++++++++++++++ RIGS/templates/partials/ec_power_info.html | 2 +- .../partials/event_detail_buttons.html | 8 + RIGS/templates/partials/event_status.html | 14 +- RIGS/templates/partials/hs_details.html | 18 +- RIGS/tests/conftest.py | 11 +- RIGS/tests/pages.py | 67 +---- RIGS/tests/test_interaction.py | 66 +---- RIGS/urls.py | 30 +- RIGS/views/hs.py | 237 ++++++++++------ pipeline/source_assets/js/autocompleter.js | 2 +- templates/base.html | 3 + templates/index.html | 8 +- .../commands/generateSampleUserData.py | 5 +- 30 files changed, 1179 insertions(+), 870 deletions(-) create mode 100644 RIGS/migrations/0046_create_powertests.py create mode 100644 RIGS/migrations/0047_auto_20230517_0944.py create mode 100644 RIGS/migrations/0048_auto_20230518_1256.py create mode 100644 RIGS/templates/hs/eventcheckin_form.html delete mode 100644 RIGS/templates/hs/hs_object_list.html create mode 100644 RIGS/templates/hs/power_detail.html create mode 100644 RIGS/templates/hs/power_form.html diff --git a/PyRIGS/views.py b/PyRIGS/views.py index fe7a44e0..372e1af0 100644 --- a/PyRIGS/views.py +++ b/PyRIGS/views.py @@ -48,6 +48,7 @@ class Index(generic.TemplateView): # Displays the current rig count along with def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['rig_count'] = models.Event.objects.rig_count() + context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()) return context diff --git a/RIGS/admin.py b/RIGS/admin.py index a46a0eee..9fff16ca 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -20,6 +20,7 @@ admin.site.register(models.VatRate, VersionAdmin) admin.site.register(models.Event, VersionAdmin) admin.site.register(models.EventItem, VersionAdmin) admin.site.register(models.Invoice, VersionAdmin) +admin.site.register(models.EventCheckIn) @transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary. @@ -206,3 +207,8 @@ class RiskAssessmentAdmin(VersionAdmin): @admin.register(models.EventChecklist) class EventChecklistAdmin(VersionAdmin): list_display = ('id', 'event', 'reviewed_at', 'reviewed_by') + + +@admin.register(models.PowerTestRecord) +class EventChecklistAdmin(VersionAdmin): + list_display = ('id', 'event', 'reviewed_at', 'reviewed_by') diff --git a/RIGS/forms.py b/RIGS/forms.py index 0664875d..c5e5de66 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -44,7 +44,7 @@ class EventForm(forms.ModelForm): return simplejson.dumps(items) def __init__(self, *args, **kwargs): - super(EventForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['items_json'].initial = self._get_items_json self.fields['start_date'].widget.format = '%Y-%m-%d' @@ -200,86 +200,41 @@ class EventChecklistForm(forms.ModelForm): related_models = { 'venue': models.Venue, - 'power_mic': models.Profile, } - # Two possible formats - def parsedatetime(self, date_string): - try: - return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')) - except ValueError: - return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M')) - - # There's probably a thousand better ways to do this, but this one is mine - def clean(self): - vehicles = {key: val for key, val in self.data.items() - if key.startswith('vehicle')} - for key in vehicles: - pk = int(key.split('_')[1]) - driver_key = 'driver_' + str(pk) - if (self.data[driver_key] == ''): - raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch') - else: - try: - item = models.EventChecklistVehicle.objects.get(pk=pk) - except models.EventChecklistVehicle.DoesNotExist: - item = models.EventChecklistVehicle() - - item.vehicle = vehicles['vehicle_' + str(pk)] - item.driver = models.Profile.objects.get(pk=self.data[driver_key]) - item.full_clean('checklist') - - # item does not have a database pk yet as it isn't saved - self.items['v' + str(pk)] = item - - crewmembers = {key: val for key, val in self.data.items() - if key.startswith('crewmember')} - other_fields = ['start', 'role', 'end'] - for key in crewmembers: - pk = int(key.split('_')[1]) - - for field in other_fields: - value = self.data[f'{field}_{pk}'] - if value == '': - raise forms.ValidationError(f'Add a {field} to crewmember {pk}', code=f'{field}_mismatch') - - try: - item = models.EventChecklistCrew.objects.get(pk=pk) - except models.EventChecklistCrew.DoesNotExist: - item = models.EventChecklistCrew() - - item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)]) - item.start = self.parsedatetime(self.data['start_' + str(pk)]) - item.role = self.data['role_' + str(pk)] - item.end = self.parsedatetime(self.data['end_' + str(pk)]) - item.full_clean('checklist') - - # item does not have a database pk yet as it isn't saved - self.items['c' + str(pk)] = item - - return super(EventChecklistForm, self).clean() - - def save(self, commit=True): - checklist = super(EventChecklistForm, self).save(commit=False) - if (commit): - # Remove all existing, to be recreated from the form - checklist.vehicles.all().delete() - checklist.crew.all().delete() - checklist.save() - - for key in self.items: - item = self.items[key] - reversion.add_to_revision(item) - # finish and save new database items - item.checklist = checklist - item.full_clean() - item.save() - - self.items.clear() - - return checklist - class Meta: model = models.EventChecklist fields = '__all__' exclude = ['reviewed_at', 'reviewed_by'] + + +class PowerTestRecordForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for name, field in self.fields.items(): + if field.__class__ == forms.NullBooleanField: + # Only display yes/no to user, the 'none' is only ever set in the background + field.widget = forms.CheckboxInput() + + class Meta: + model = models.PowerTestRecord + fields = '__all__' + exclude = ['reviewed_at', 'reviewed_by'] + + +class EventCheckInForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['time'].initial = timezone.now() + self.fields['role'].initial = "Crew" + + class Meta: + model = models.EventCheckIn + fields = '__all__' + exclude = ['end_time'] + + +class EditCheckInForm(forms.ModelForm): + class Meta: + model = models.EventCheckIn + fields = '__all__' diff --git a/RIGS/management/commands/generateSampleRIGSData.py b/RIGS/management/commands/generateSampleRIGSData.py index df0258c6..0f89cc07 100644 --- a/RIGS/management/commands/generateSampleRIGSData.py +++ b/RIGS/management/commands/generateSampleRIGSData.py @@ -278,7 +278,7 @@ class Command(BaseCommand): suspended_structures=bool(random.getrandbits(1)), outside=bool(random.getrandbits(1))) if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist - models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles), + models.EventChecklist.objects.create(event=new_event, safe_parking=bool(random.getrandbits(1)), safe_packing=bool(random.getrandbits(1)), exits=bool(random.getrandbits(1)), @@ -287,6 +287,4 @@ class Command(BaseCommand): ear_plugs=bool(random.getrandbits(1)), hs_location="Locked away safely", extinguishers_location="Somewhere, I forgot", - earthing=bool(random.getrandbits(1)), - pat=bool(random.getrandbits(1)), date=timezone.now(), venue=random.choice(self.venues)) diff --git a/RIGS/migrations/0046_create_powertests.py b/RIGS/migrations/0046_create_powertests.py new file mode 100644 index 00000000..95f1c8e4 --- /dev/null +++ b/RIGS/migrations/0046_create_powertests.py @@ -0,0 +1,71 @@ +# Generated by Django 3.2.16 on 2023-05-08 15:58 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import versioning.versioning + +def migrate_old_data(apps, schema_editor): + EventChecklist = apps.get_model('RIGS', 'EventChecklist') + PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord') + for ec in EventChecklist.objects.all(): + # New highscore for the most pythonic BS I've ever written. + PowerTestRecord.objects.create(event=ec.event, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created)}) + + +def revert(apps, schema_editor): + apps.get_model('RIGS', 'PowerTestRecord').objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0045_alter_profile_is_approved'), + ] + + operations = [ + migrations.CreateModel( + name='PowerTestRecord', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_tests', to='RIGS.event')), + ('notes', models.TextField(blank=True, default='')), + ('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue')), + ('reviewed_at', models.DateTimeField(null=True)), + ('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)), + ('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?
(using socket tester)', null=True)), + ('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?
(truss, stage, generators etc)', null=True)), + ('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)), + ('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?
(if cable is more than 3m long) ', null=True)), + ('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)), + ('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')), + ('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')), + ('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')), + ('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation
(if required)', null=True, verbose_name='Phase Rotation')), + ('fd_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (ZS)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')), + ('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')), + ('w1_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)), + ('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)), + ('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)), + ('w1_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (ZS)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')), + ('w2_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)), + ('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)), + ('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)), + ('w2_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (ZS)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')), + ('w3_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)), + ('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)), + ('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)), + ('w3_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (ZS)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')), + ('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?
(using test button)', null=True)), + ('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?
(using socket tester)', null=True)), + ('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')), + ], + options={ + 'abstract': False, + 'ordering': ['event'], + 'permissions': [('review_power', 'Can review Power Test Records')], + }, + bases=(models.Model, versioning.versioning.RevisionMixin), + ), + migrations.RunPython(migrate_old_data, reverse_code=revert), + ] diff --git a/RIGS/migrations/0047_auto_20230517_0944.py b/RIGS/migrations/0047_auto_20230517_0944.py new file mode 100644 index 00000000..ac02413d --- /dev/null +++ b/RIGS/migrations/0047_auto_20230517_0944.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.19 on 2023-05-17 08:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_old_data(apps, schema_editor): + EventChecklist = apps.get_model('RIGS', 'EventChecklist') + EventCheckIn = apps.get_model('RIGS', 'EventCheckIn') + for ec in EventChecklist.objects.all(): + for crew in ec.crew.all(): + vehicle = ec.vehicles.get(driver=crew.crewmember) or None + EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=vehicle.vehicle) + + +def revert(apps, schema_editor): + apps.get_model('RIGS', 'EventCheckIn').objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0046_create_powertests'), + ] + + operations = [ + migrations.CreateModel( + name='EventCheckIn', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField()), + ('role', models.CharField(blank=True, max_length=50)), + ('vehicle', models.CharField(blank=True, max_length=100)), + ('end_time', models.DateTimeField(blank=True, null=True)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.event')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.RunPython(migrate_old_data, reverse_code=revert), + ] diff --git a/RIGS/migrations/0048_auto_20230518_1256.py b/RIGS/migrations/0048_auto_20230518_1256.py new file mode 100644 index 00000000..ccd35a82 --- /dev/null +++ b/RIGS/migrations/0048_auto_20230518_1256.py @@ -0,0 +1,156 @@ +# Generated by Django 3.2.19 on 2023-05-18 11:56 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0047_auto_20230517_0944'), + ] + + operations = [ + migrations.RemoveField( + model_name='eventchecklistvehicle', + name='checklist', + ), + migrations.RemoveField( + model_name='eventchecklistvehicle', + name='driver', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='all_rcds_tested', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='earthing', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='fd_earth_fault', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='fd_phase_rotation', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='fd_pssc', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='fd_voltage_l1', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='fd_voltage_l2', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='fd_voltage_l3', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='labelling', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='pat', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='power_mic', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='public_sockets_tested', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='rcds', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='source_rcd', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='supply_test', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w1_description', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w1_earth_fault', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w1_polarity', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w1_voltage', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w2_description', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w2_earth_fault', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w2_polarity', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w2_voltage', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w3_description', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w3_earth_fault', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w3_polarity', + ), + migrations.RemoveField( + model_name='eventchecklist', + name='w3_voltage', + ), + migrations.AddField( + model_name='powertestrecord', + name='power_mic', + field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'), + ), + migrations.AlterField( + model_name='eventchecklist', + name='reviewed_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='powertestrecord', + name='reviewed_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='riskassessment', + name='reviewed_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.DeleteModel( + name='EventChecklistCrew', + ), + migrations.DeleteModel( + name='EventChecklistVehicle', + ), + ] diff --git a/RIGS/models.py b/RIGS/models.py index 30fdecd8..550b4df5 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -81,6 +81,10 @@ class Profile(AbstractUser): def __str__(self): return self.name + def current_event(self): + q = EventCheckIn.objects.filter(person=self, end_time=None) + return q.latest('time') if q.exists() else None + class ContactableManager(models.Manager): def search(self, query=None): @@ -405,7 +409,15 @@ class Event(models.Model, RevisionMixin): @property def hs_done(self): - return self.riskassessment is not None and len(self.checklists.all()) > 0 + return self.riskassessment is not None and self.has_checklist and self.has_power + + @property + def has_checklist(self): + return self.checklists.exists() + + @property + def has_power(self): + return self.power_tests.exists() @property def has_start_time(self): @@ -478,6 +490,15 @@ class Event(models.Model, RevisionMixin): else: return bool(self.purchase_order) + @property + def can_check_in(self): + earliest = self.earliest_time + if isinstance(self.earliest_time, datetime.date): + earliest = datetime.datetime.combine(self.start_date, datetime.time(00, 00)) + tz = pytz.timezone(settings.TIME_ZONE) + earliest = tz.localize(earliest) + return not self.dry_hire and earliest <= timezone.now() + objects = EventManager() def get_absolute_url(self): @@ -689,8 +710,21 @@ def validate_url(value): raise ValidationError('URL must point to a location on the TEC Sharepoint') +class ReviewableModel(models.Model): + reviewed_at = models.DateTimeField(null=True, blank=True) + reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, + verbose_name="Reviewer", on_delete=models.CASCADE) + + class Meta: + abstract = True + + @cached_property + def fieldz(self): + return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created] + + @reversion.register -class RiskAssessment(models.Model, RevisionMixin): +class RiskAssessment(ReviewableModel, RevisionMixin): SMALL = (0, 'Small') MEDIUM = (1, 'Medium') LARGE = (2, 'Large') @@ -738,10 +772,6 @@ class RiskAssessment(models.Model, RevisionMixin): # Blimey that was a lot of options - reviewed_at = models.DateTimeField(null=True) - reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, - verbose_name="Reviewer", on_delete=models.CASCADE) - supervisor_consulted = models.BooleanField(null=True) expected_values = { @@ -778,10 +808,6 @@ class RiskAssessment(models.Model, RevisionMixin): ('review_riskassessment', 'Can review Risk Assessments') ] - @cached_property - def fieldz(self): - return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created] - @property def event_size(self): # Confirm event size. Check all except generators, since generators entails outside @@ -795,6 +821,12 @@ class RiskAssessment(models.Model, RevisionMixin): def get_event_size_display(self): return self.SIZES[self.event_size][1] + " Event" + def __str__(self): + return f"{self.pk} | {self.event}" + + def get_absolute_url(self): + return reverse('ra_detail', kwargs={'pk': self.pk}) + @property def activity_feed_string(self): return str(self.event) @@ -803,20 +835,12 @@ class RiskAssessment(models.Model, RevisionMixin): def name(self): return str(self) - def get_absolute_url(self): - return reverse('ra_detail', kwargs={'pk': self.pk}) - def __str__(self): - return f"{self.pk} | {self.event}" - - -@reversion.register(follow=['vehicles', 'crew']) -class EventChecklist(models.Model, RevisionMixin): +@reversion.register +class EventChecklist(ReviewableModel, RevisionMixin): event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE) # General - power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists', - verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?") venue = models.ForeignKey('Venue', on_delete=models.CASCADE) date = models.DateField() @@ -830,6 +854,32 @@ class EventChecklist(models.Model, RevisionMixin): hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box") extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers") + inverted_fields = [] + + class Meta: + ordering = ['event'] + permissions = [ + ('review_eventchecklist', 'Can review Event Checklists') + ] + + def __str__(self): + return f"{self.pk} - {self.event}" + + @property + def activity_feed_string(self): + return str(self.event) + + def get_absolute_url(self): + return reverse('ec_detail', kwargs={'pk': self.pk}) + + +@reversion.register +class PowerTestRecord(ReviewableModel, RevisionMixin): + event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE) + power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists', + verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?") + venue = models.ForeignKey('Venue', on_delete=models.CASCADE) + notes = models.TextField(blank=True, default='') # Small Electrical Checks rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?") supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?
(using socket tester)") @@ -865,58 +915,40 @@ class EventChecklist(models.Model, RevisionMixin): all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?
(using test button)") public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?
(using socket tester)") - reviewed_at = models.DateTimeField(null=True) - reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, - verbose_name="Reviewer", on_delete=models.CASCADE) - - inverted_fields = [] - class Meta: ordering = ['event'] permissions = [ - ('review_eventchecklist', 'Can review Event Checklists') + ('review_power', 'Can review Power Test Records') ] - @cached_property - def fieldz(self): - return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created] + def __str__(self): + return f"{self.pk} - {self.event}" @property def activity_feed_string(self): return str(self.event) - def get_absolute_url(self): - return reverse('ec_detail', kwargs={'pk': self.pk}) + +class EventCheckIn(models.Model): + event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE) + person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkins', on_delete=models.CASCADE) + time = models.DateTimeField() + role = models.CharField(max_length=50, blank=True) + vehicle = models.CharField(max_length=100, blank=True) + end_time = models.DateTimeField(null=True, blank=True) def __str__(self): - return f"{self.pk} - {self.event}" - - -@reversion.register -class EventChecklistVehicle(models.Model, RevisionMixin): - checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE) - vehicle = models.CharField(max_length=255) - driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE) - - reversion_hide = True - - def __str__(self): - return f"{self.vehicle} driven by {self.driver}" - - -@reversion.register -class EventChecklistCrew(models.Model, RevisionMixin): - checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE) - crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE) - role = models.CharField(max_length=255) - start = models.DateTimeField() - end = models.DateTimeField() - - reversion_hide = True + return f"{self.person} on {self.event}" def clean(self): - if self.start > self.end: - raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.') + sass = " Please invent time travel and retry." + if self.time > timezone.now(): + raise ValidationError("May not check in in the future." + sass) + if self.end_time and self.end_time < self.time: + raise ValidationError("May not check out before you've checked in." + sass) - def __str__(self): - return f"{self.crewmember} ({self.role})" + def get_absolute_url(self): + return reverse('event_detail', kwargs={'pk': self.event_id}) + + def active(self): + return end_time is not None diff --git a/RIGS/templates/base_rigs.html b/RIGS/templates/base_rigs.html index 0ae1a62f..16d06ba9 100644 --- a/RIGS/templates/base_rigs.html +++ b/RIGS/templates/base_rigs.html @@ -34,16 +34,7 @@ {% if perms.RIGS.view_riskassessment %} - + {% endif %} {% if perms.RIGS.view_invoice %}