From 0f951dcf029d4af284467543a3afdf5bf6581a20 Mon Sep 17 00:00:00 2001 From: Matthew Lemon Date: Tue, 23 Apr 2024 11:16:38 +0100 Subject: switched to Django --- engagements/__init__.py | 0 engagements/admin.py | 32 ++ engagements/apps.py | 6 + engagements/forms.py | 242 ++++++++++++ .../management/commands/create_engagement_data.py | 14 + engagements/migrations/0001_initial.py | 134 +++++++ engagements/migrations/0002_initial.py | 44 +++ .../migrations/0003_engagementeffort_notes.py | 18 + engagements/migrations/__init__.py | 0 engagements/models.py | 236 ++++++++++++ engagements/static/js/yoap.js | 12 + .../templates/engagements/engagement_create.html | 29 ++ .../templates/engagements/engagement_detail.html | 78 ++++ .../engagements/engagement_effort_create.html | 20 + .../templates/engagements/engagement_form.html | 32 ++ .../engagements/engagement_plan_for_site.html | 35 ++ engagements/templates/engagements/ep_org.html | 427 +++++++++++++++++++++ engagements/templates/engagements/index.html | 30 ++ .../templates/engagements/organisations.html | 50 +++ .../engagements/snippets/effort_summary_panel.html | 41 ++ engagements/tests/__init__.py | 0 engagements/tests/test_forms.py | 45 +++ engagements/tests/test_models.py | 30 ++ engagements/tests/test_views.py | 91 +++++ engagements/urls.py | 29 ++ engagements/utils.py | 186 +++++++++ engagements/views.py | 182 +++++++++ 27 files changed, 2043 insertions(+) create mode 100644 engagements/__init__.py create mode 100644 engagements/admin.py create mode 100644 engagements/apps.py create mode 100644 engagements/forms.py create mode 100644 engagements/management/commands/create_engagement_data.py create mode 100644 engagements/migrations/0001_initial.py create mode 100644 engagements/migrations/0002_initial.py create mode 100644 engagements/migrations/0003_engagementeffort_notes.py create mode 100644 engagements/migrations/__init__.py create mode 100644 engagements/models.py create mode 100644 engagements/static/js/yoap.js create mode 100644 engagements/templates/engagements/engagement_create.html create mode 100644 engagements/templates/engagements/engagement_detail.html create mode 100644 engagements/templates/engagements/engagement_effort_create.html create mode 100644 engagements/templates/engagements/engagement_form.html create mode 100644 engagements/templates/engagements/engagement_plan_for_site.html create mode 100644 engagements/templates/engagements/ep_org.html create mode 100644 engagements/templates/engagements/index.html create mode 100644 engagements/templates/engagements/organisations.html create mode 100644 engagements/templates/engagements/snippets/effort_summary_panel.html create mode 100644 engagements/tests/__init__.py create mode 100644 engagements/tests/test_forms.py create mode 100644 engagements/tests/test_models.py create mode 100644 engagements/tests/test_views.py create mode 100644 engagements/urls.py create mode 100644 engagements/utils.py create mode 100644 engagements/views.py (limited to 'engagements') diff --git a/engagements/__init__.py b/engagements/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/engagements/admin.py b/engagements/admin.py new file mode 100644 index 0000000..9861867 --- /dev/null +++ b/engagements/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin + +from .models import ( + Engagement, + EngagementEffort, + EngagementType, + Organisation, + Person, + RegulatedEntityType, + RegulatoryRole, +) + +site = admin.site + +site.site_header = "DefNucSyR Engagement Database (DED)" + + +class PersonAdmin(admin.ModelAdmin): + list_display = ("__str__", "organisation") + + @admin.display(description="fullname") + def fullname(self, obj): + return f"{obj.first_name} {obj.last_name}" + + +site.register(Person, PersonAdmin) +site.register(Organisation) +site.register(RegulatedEntityType) +site.register(RegulatoryRole) +site.register(EngagementType) +site.register(Engagement) +site.register(EngagementEffort) diff --git a/engagements/apps.py b/engagements/apps.py new file mode 100644 index 0000000..e2f8f69 --- /dev/null +++ b/engagements/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EngagementsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "engagements" diff --git a/engagements/forms.py b/engagements/forms.py new file mode 100644 index 0000000..ae01916 --- /dev/null +++ b/engagements/forms.py @@ -0,0 +1,242 @@ +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Field, Fieldset, Layout, Submit +from django import forms +from django.forms.widgets import HiddenInput + +from .models import Engagement, EngagementEffort + +# TODO - need to handle errors correctly in this form and in the template + + +class EngagementEffortReportingCreateForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + "This is a form", + Field("is_planned"), + Field("proposed_start_date", css_class="w3-input w3-border w3-round", type="date"), + Field("proposed_end_date", css_class="w3-input w3-border w3-round"), + "officers", + "notes", + ), + Submit("submit", "Submit", css_class="w3-button w3-green"), + ) + + class Meta: + model = EngagementEffort + fields = [ + "is_planned", + "proposed_start_date", + "proposed_end_date", + "officers", + "notes", + ] + help_texts = { + "is_planned": ("
To distinguish planned events from retrospective recording") + } + widgets = { + "proposed_start_date": forms.DateTimeInput( + attrs={ + "class": "w3-input w3-border w3-round", + "type": "datetime-local", + }, + format="j M y H:i", + ), + "proposed_end_date": forms.DateTimeInput( + attrs={ + "class": "w3-input w3-border w3-round", + "type": "datetime-local", + }, + format="j M y H:i", + ), + } + + +class EngagementEffortRegulationCreateForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + "This is a form", + Field("is_planned"), + Field("proposed_start_date", css_class="w3-input w3-border w3-round", type="date"), + Field("proposed_end_date", css_class="w3-input w3-border w3-round"), + "sub_instruments", + "officers", + ), + Submit("submit", "Submit", css_class="w3-button w3-green"), + ) + + class Meta: + model = EngagementEffort + fields = [ + "is_planned", + "proposed_start_date", + "proposed_end_date", + "officers", + "sub_instruments", + "notes", + ] + help_texts = { + "is_planned": ("
To distinguish planned events from retrospective recording") + } + widgets = { + "proposed_start_date": forms.DateTimeInput( + attrs={ + "class": "w3-input w3-border w3-round", + "type": "datetime-local", + }, + format="j M y H:i", + ), + "proposed_end_date": forms.DateTimeInput( + attrs={ + "class": "w3-input w3-border w3-round", + "type": "datetime-local", + }, + format="j M y H:i", + ), + } + + +class EngagementEffortPlanningCreateForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + "This is a form", + Field("is_planned"), + Field("proposed_start_date", css_class="w3-input w3-border w3-round", type="date"), + Field("proposed_end_date", css_class="w3-input w3-border w3-round"), + "officers", + "notes", + ), + Submit("submit", "Submit", css_class="w3-button w3-green"), + ) + + class Meta: + model = EngagementEffort + fields = [ + "is_planned", + "proposed_start_date", + "proposed_end_date", + "officers", + "notes", + ] + help_texts = { + "is_planned": ("
To distinguish planned events from retrospective recording") + } + widgets = { + "proposed_start_date": forms.DateTimeInput( + attrs={ + "class": "w3-input w3-border w3-round", + "type": "datetime-local", + }, + format="j M y H:i", + ), + "proposed_end_date": forms.DateTimeInput( + attrs={ + "class": "w3-input w3-border w3-round", + "type": "datetime-local", + }, + format="j M y H:i", + ), + } + + +class EngagementEffortTravelCreateForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + "This is a form", + Field("is_planned"), + Field("proposed_start_date", css_class="w3-input w3-border w3-round", type="date"), + Field("proposed_end_date", css_class="w3-input w3-border w3-round"), + "officers", + ), + Submit("submit", "Submit", css_class="w3-button w3-green"), + ) + + class Meta: + model = EngagementEffort + fields = ["is_planned", "proposed_start_date", "proposed_end_date", "officers"] + widgets = { + "proposed_start_date": forms.DateTimeInput(attrs={"type": "datetime-local"}), + "proposed_end_date": forms.DateTimeInput(attrs={"type": "datetime-local"}), + } + + +class EngagementEffortCreateForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["engagement"].widget = HiddenInput() + self.fields["effort_type"].widget = HiddenInput() + if kwargs.get("initial"): + if not kwargs["initial"]["effort_type"] == "REGULATION": + self.fields["sub_instruments"].widget = HiddenInput() + + class Meta: + model = EngagementEffort + fields = "__all__" + widgets = { + "is_planned": forms.Select( + choices=( + (True, "YES"), + (False, "NO"), + ), + attrs={"class": "w3-border"}, + ), + "proposed_start_date": forms.DateTimeInput( + attrs={"class": "w3-input w3-border w3-round", "type": "date"}, + format="j M y H:i", + ), + "proposed_end_date": forms.DateTimeInput( + attrs={"class": "w3-input w3-border w3-round", "type": "date"}, + format="j M y H:i", + ), + "effort_type": forms.Select(attrs={"class": "w3-select w3-border w3-round"}), + "officers": forms.SelectMultiple(attrs={"class": "w3-select w3-border w3-round"}), + "sub_instruments": forms.SelectMultiple(attrs={"class": "w3-select w3-border w3-round"}), + } + help_texts = { + "proposed_start_date": "YYYY-MM-DD HH:MM " + "(Please include time here)", + "proposed_end_date": "YYYY-MM-DD HH:MM " + "(Please include time here)", + } + + +class EngagementCreateForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + try: + self.fields["engagement_type"].queryset = kwargs["initial"]["engagement_type"] + except KeyError: + pass + + class Meta: + model = Engagement + fields = [ + "proposed_start_date", + "proposed_end_date", + "engagement_type", + "officers", + ] + labels = { + "officers": "Inspectors", + } + help_texts = { + "proposed_start_date": "YYYY-MM-DD", + "proposed_end_date": "YYYY-MM-DD", + } + widgets = { + "proposed_start_date": forms.DateInput(attrs={"class": "w3-input w3-border w3-round", "type": "date"}), + "proposed_end_date": forms.DateInput(attrs={"class": "w3-input w3-border w3-round", "type": "date"}), + "engagement_type": forms.Select(attrs={"class": "w3-select w3-input w3-border w3-round"}), + "officers": forms.SelectMultiple(attrs={"class": "w3-input w3-border w3-round"}), + } diff --git a/engagements/management/commands/create_engagement_data.py b/engagements/management/commands/create_engagement_data.py new file mode 100644 index 0000000..021d29b --- /dev/null +++ b/engagements/management/commands/create_engagement_data.py @@ -0,0 +1,14 @@ +from django.core.management.base import BaseCommand + +from engagements.utils import populate_database + + +class Command(BaseCommand): + help = "python manage.py create_engagement_data" + + # def add_arguments(self, parser): + # parser.add_argument("year", nargs="+", type=int) + + def handle(self, *args, **options): + populate_database() + self.stdout.write(self.style.SUCCESS("Created engagement objects.")) diff --git a/engagements/migrations/0001_initial.py b/engagements/migrations/0001_initial.py new file mode 100644 index 0000000..2516517 --- /dev/null +++ b/engagements/migrations/0001_initial.py @@ -0,0 +1,134 @@ +# Generated by Django 4.0.8 on 2022-11-02 09:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Engagement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('proposed_start_date', models.DateField()), + ('proposed_end_date', models.DateField(blank=True, null=True)), + ], + options={ + 'ordering': ('proposed_start_date',), + }, + ), + migrations.CreateModel( + name='EngagementType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=56)), + ('description', models.TextField(blank=True, null=True)), + ], + options={ + 'verbose_name_plural': 'Engagement Types', + }, + ), + migrations.CreateModel( + name='Organisation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=128)), + ('slug', models.SlugField(blank=True, max_length=128)), + ('is_regulated_entity', models.BooleanField(default=False)), + ], + options={ + 'verbose_name_plural': 'Organisations', + }, + ), + migrations.CreateModel( + name='RegulatedEntityType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=128)), + ], + options={ + 'verbose_name_plural': 'Regulated Entity Types', + }, + ), + migrations.CreateModel( + name='RegulatoryRole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=64)), + ('description', models.TextField(max_length=1024)), + ], + options={ + 'verbose_name_plural': 'Regulatory Roles', + }, + ), + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('first_name', models.CharField(max_length=64)), + ('last_name', models.CharField(max_length=64)), + ('email', models.EmailField(blank=True, max_length=254, null=True)), + ('mobile', models.CharField(blank=True, max_length=64, null=True)), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engagements.organisation')), + ('regulatory_role', models.ManyToManyField(to='engagements.regulatoryrole')), + ], + options={ + 'verbose_name_plural': 'People', + }, + ), + migrations.AddField( + model_name='organisation', + name='ap', + field=models.ManyToManyField(blank=True, related_name='accountable_person', to='engagements.person', verbose_name='Accountable Person'), + ), + migrations.AddField( + model_name='organisation', + name='entitytype', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='engagements.regulatedentitytype'), + ), + migrations.AddField( + model_name='organisation', + name='ih', + field=models.ManyToManyField(blank=True, related_name='information_holder', to='engagements.person', verbose_name='Information Holder'), + ), + migrations.AddField( + model_name='organisation', + name='rp', + field=models.ManyToManyField(blank=True, related_name='responsible_person', to='engagements.person', verbose_name='Responsible Person'), + ), + migrations.CreateModel( + name='EngagementEffort', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('is_planned', models.BooleanField(blank=True, default=True, null=True, verbose_name='Planned')), + ('effort_type', models.CharField(choices=[('TRAVEL', 'Travel'), ('PLANNING', 'Planning'), ('REGULATION', 'Regulation (On-site or Remote)'), ('DISCUSSION', 'Discussion'), ('REPORT', 'Reporting')], max_length=32, verbose_name='Effort Type')), + ('proposed_start_date', models.DateTimeField()), + ('proposed_end_date', models.DateTimeField()), + ('engagement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='effort', to='engagements.engagement')), + ], + options={ + 'verbose_name_plural': 'Engagement Effort', + 'ordering': ('proposed_start_date',), + }, + ), + ] diff --git a/engagements/migrations/0002_initial.py b/engagements/migrations/0002_initial.py new file mode 100644 index 0000000..1a55d23 --- /dev/null +++ b/engagements/migrations/0002_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.0.8 on 2022-11-02 09:00 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('instruments', '0001_initial'), + ('engagements', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='engagementeffort', + name='officers', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='engagementeffort', + name='sub_instruments', + field=models.ManyToManyField(blank=True, related_name='effort', to='instruments.subinstrument'), + ), + migrations.AddField( + model_name='engagement', + name='engagement_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engagements.engagementtype'), + ), + migrations.AddField( + model_name='engagement', + name='external_party', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engagements.organisation'), + ), + migrations.AddField( + model_name='engagement', + name='officers', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/engagements/migrations/0003_engagementeffort_notes.py b/engagements/migrations/0003_engagementeffort_notes.py new file mode 100644 index 0000000..c3071c2 --- /dev/null +++ b/engagements/migrations/0003_engagementeffort_notes.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.8 on 2023-04-19 17:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("engagements", "0002_initial"), + ] + + operations = [ + migrations.AddField( + model_name="engagementeffort", + name="notes", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/engagements/migrations/__init__.py b/engagements/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/engagements/models.py b/engagements/models.py new file mode 100644 index 0000000..20bed8d --- /dev/null +++ b/engagements/models.py @@ -0,0 +1,236 @@ +from django.conf import settings +from django.db import models +from django.db.models import Q +from django.utils.text import slugify + +from myuser.models import TeamUser + + +class Common(models.Model): + date_created = models.DateTimeField(auto_now_add=True) + last_modified = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class RegulatoryRole(Common): + name = models.CharField(max_length=64, null=False, blank=False) + description = models.TextField(max_length=1024) + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "Regulatory Roles" + + +class Person(Common): + "External person, rather than MOD at this point." + first_name = models.CharField(max_length=64, null=False, blank=False) + last_name = models.CharField(max_length=64, null=False, blank=False) + organisation = models.ForeignKey("Organisation", null=False, blank=False, on_delete=models.CASCADE) + email = models.EmailField(null=True, blank=True) + mobile = models.CharField(max_length=64, null=True, blank=True) + regulatory_role = models.ManyToManyField(RegulatoryRole) + + def __str__(self): + return f"{self.first_name} {self.last_name}" + + class Meta: + verbose_name_plural = "People" + + +class RegulatedEntityType(Common): + name = models.CharField(max_length=128, null=False, blank=False) + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "Regulated Entity Types" + + +class Organisation(Common): + name = models.CharField(max_length=128, blank=False) + slug = models.SlugField(max_length=128, blank=True) + is_regulated_entity = models.BooleanField(default=False) + entitytype = models.ForeignKey(RegulatedEntityType, null=True, blank=True, on_delete=models.CASCADE) + # Responsible Person + rp = models.ManyToManyField( + Person, + blank=True, + related_name="responsible_person", + verbose_name="Responsible Person", + ) + # Accountable Person + ap = models.ManyToManyField( + Person, + blank=True, + related_name="accountable_person", + verbose_name="Accountable Person", + ) + # Information Holder + ih = models.ManyToManyField( + Person, + blank=True, + related_name="information_holder", + verbose_name="Information Holder", + ) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def lead_team(self): + teams = {ot.team for ot in self.lead_inspector.all()} + if not teams: + return "NA" + if len(teams) > 1: + if len(set([t.name for t in teams])) > 1: + return "Shared Lead" + else: + return teams.pop() + else: + return teams.pop() + + class Meta: + verbose_name_plural = "Organisations" + + +class EngagementType(Common): + name = models.CharField(max_length=56) + description = models.TextField(null=True, blank=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "Engagement Types" + + +class EngagementManager(models.Manager): + def sp(self): + # TODO fix this - returns duplicates + qs = self.filter(officers__team__name="Submarines and Propulsion") + return qs.exclude(Q(engagement_type__name="INSPECTION") | Q(engagement_type__name="ASSESSMENT")) + + def sp_regulatory(self): + "Assessments and Inspectons only" + qs = self.filter(officers__team__name="Submarines and Propulsion") + return qs.filter( + Q(engagement_type__name="INSPECTION") + | Q(engagement_type__name="ASSESSMENT") + | Q(engagement_type__name="L1RIF") + | Q(engagement_type__name="L2RIF") + | Q(engagement_type__name="L3RIF") + | Q(engagement_type__name="L4RIF") + | Q(engagement_type__name="SAMPLING") + ) + + def tr(self): + return self.filter(officers__team__name="Transport") + + +class Engagement(Common): + proposed_start_date = models.DateField(null=False, blank=False) + proposed_end_date = models.DateField(null=True, blank=True) + engagement_type = models.ForeignKey(EngagementType, on_delete=models.CASCADE) + external_party = models.ForeignKey(Organisation, on_delete=models.CASCADE) + officers = models.ManyToManyField(TeamUser) + + objects = EngagementManager() + + friendly_types = { + "INSPECTION": "Inspection", + "ASSESSMENT": "Assessment", + "SAMPLING": "Sampling", + "L1RIF": "L1 RIF", + "L2RIF": "L2 RIF", + "L3RIF": "L3 RIF", + "L4RIF": "L4 RIF", + } + + def total_planning_effort(self): + p_effort = self.engagementeffort_set.all().filter(is_planned=True) + return sum([x.effort_total_hours() for x in p_effort]) + + def total_planning_effort_per_officer(self): + # TODO - check this algorithm. It's not quite right is it? + p_effort = self.engagementeffort_set.all().filter(is_planned=True) + offs = sum([x.officers.count() for x in p_effort]) + return sum([x.effort_total_hours() for x in p_effort]) / offs + + def total_effort(self): + return 0 + + def friendly_type(self): + return self.friendly_types[self.engagement_type.name] + + def dscs(self): + "Return all declared DSCs as part of REGULATION effort" + dscs = set() + for ee in EngagementEffort.objects.filter(engagement=self, effort_type="REGULATION"): + for si in ee.sub_instruments.all(): + dscs.add(si) + return dscs + + def __str__(self): + return f"{self.engagement_type.name} at {self.external_party} ({self.proposed_start_date})" + + class Meta: + ordering = ("proposed_start_date",) + + +class EngagementEffort(Common): + choices = ( + ("TRAVEL", "Travel"), + ("PLANNING", "Planning"), + ("REGULATION", "Regulation (On-site or Remote)"), + ("DISCUSSION", "Discussion"), + ("REPORT", "Reporting"), + ) + is_planned = models.BooleanField(null=True, blank=True, verbose_name="Planned", default=True) + effort_type = models.CharField(max_length=32, choices=choices, verbose_name="Effort Type") + proposed_start_date = models.DateTimeField() + proposed_end_date = models.DateTimeField() + engagement = models.ForeignKey(Engagement, on_delete=models.CASCADE, related_name="effort") + officers = models.ManyToManyField(settings.AUTH_USER_MODEL) + sub_instruments = models.ManyToManyField("instruments.SubInstrument", blank=True, related_name="effort") + notes = models.TextField(null=True, blank=True) + + class Meta: + verbose_name_plural = "Engagement Effort" + ordering = ("proposed_start_date",) + + def effort_total_hours(self): + "Returns total effort for this engagement." + delta = self.proposed_end_date - self.proposed_start_date + return delta.seconds / 60 / 60 + + def effort_total_planned_hours(self): + "Returns total planned effort for this engagement." + if self.is_planned: + delta = self.proposed_end_date - self.proposed_start_date + return delta.seconds / 60 / 60 + else: + return 0 + + def effort_actual(self): + "Returns effort that where is_planned is false." + if not self.is_planned: + delta = self.proposed_end_date - self.proposed_start_date + return delta.seconds / 60 / 60 + else: + return 0 + + def effort_per_officer_hours(self): + "Returns effort in hours per officer" + delta = (self.proposed_end_date - self.proposed_start_date) / self.officers.count() + return delta.seconds / 60 / 60 + + def __str__(self): + return f"{self.effort_type} effort for {self.engagement}: {self.proposed_end_date - self.proposed_start_date}" diff --git a/engagements/static/js/yoap.js b/engagements/static/js/yoap.js new file mode 100644 index 0000000..84d5a2f --- /dev/null +++ b/engagements/static/js/yoap.js @@ -0,0 +1,12 @@ +// bllopcks +const heading = document.createElement("h3"); +const headingText = document.createTextNode("Snatch grab"); +heading.appendChild(headingText); + +const mydiv = document.body.querySelector("#test-container") +mydiv.appendChild(heading); + +var i; +for (i = 0; i < 15; i++) { + +} diff --git a/engagements/templates/engagements/engagement_create.html b/engagements/templates/engagements/engagement_create.html new file mode 100644 index 0000000..1e72ca0 --- /dev/null +++ b/engagements/templates/engagements/engagement_create.html @@ -0,0 +1,29 @@ +{% extends "core/base.html" %} + +{% block title %}Create new engagement{% endblock title %} + +{% block content %} + +
+

{{ title }}

+
+ + × +

Step 1

+

To roughly plan out future events, you provide the minimal details here: start date, end date (optional),the type of Engagement (Assessment, Inspection or Sampling), the external site or operation and finally the inspectors who are carrying out the work.

+

Step 2

+

So that we can track the finer details involved with an Assessment or Inspection, each Engagement comprises additional components, such as Planning, On-site and Reporting. Inspector time can be allocated to these components. In addition, each compontent can be associated with Instruments, such as DSCs, etc. After you create the overarching Engagement using this form, you will have the opportunity to add components.

+
+ +
+
+

Enter main details:

+
{% csrf_token %} + {{ form.as_p }} + +
+
+
+ +{% endblock content %} diff --git a/engagements/templates/engagements/engagement_detail.html b/engagements/templates/engagements/engagement_detail.html new file mode 100644 index 0000000..85ef220 --- /dev/null +++ b/engagements/templates/engagements/engagement_detail.html @@ -0,0 +1,78 @@ +{% extends "core/base.html" %} +{% load table_extras %} + +{% block title %}{{ engagement }}{% endblock title %} + +{% block content %} + +
+
+

{{ engagement.friendly_type }} at {{ engagement.external_party }}

+
+
+
+
+ Edit Engagement +
+
+

Details

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Date{{ engagement.proposed_start_date|date:'l' }} - {{ engagement.proposed_start_date|date:'j M Y' }}
Site/Operation:{{ engagement.external_party }}
Subject of Activity +

Summmary text

+
    + {% for t in dscs %} +
  • {{ t }}
  • + {% endfor %} +
+
Inspectors:{{ engagement.officers.all|commalist }}
Planned Effort:{{ effort_planned|floatformat }} hrs
Actual Effort:{{ effort_actual|floatformat }} hrs
Total Effort:{{ effort_total|floatformat }} hrs
+
+
+ +
+

Effort for this engagement

+ Add: + Travel | + Planning | + Regulation | + Reporting + + {% if effort %} + {% for e in effort.all %} +
+ {% include "engagements/snippets/effort_summary_panel.html" with e=e %} +
+ {% endfor %} + {% endif %} +
+
+ + {% endblock content %} diff --git a/engagements/templates/engagements/engagement_effort_create.html b/engagements/templates/engagements/engagement_effort_create.html new file mode 100644 index 0000000..65e2e21 --- /dev/null +++ b/engagements/templates/engagements/engagement_effort_create.html @@ -0,0 +1,20 @@ +{% extends "core/base.html" %} + +{% load crispy_forms_tags %} + +{% block title %}Add {{ etype }} effort to Engagement{% endblock title %} + +{% block content %} + +
+

Register your {{ etype|lower }} effort for the {{ engagement.engagement_type.name|title }} event at {{ engagement.external_party }} on {{ engagement.proposed_start_date }}

+ +
+

Enter details:

+
{% csrf_token %} + {% crispy form form.helper %} +
+
+
+ +{% endblock content %} diff --git a/engagements/templates/engagements/engagement_form.html b/engagements/templates/engagements/engagement_form.html new file mode 100644 index 0000000..e05c639 --- /dev/null +++ b/engagements/templates/engagements/engagement_form.html @@ -0,0 +1,32 @@ +{% extends "core/base.html" %} + +{% load static %} + +{% block title %}Create new engagement{% endblock title %} +{% block extra_head_tags %} + +{% endblock extra_head_tags %} + +{% block content %} + +
+
+

{{ title }}

+

TAKE THIS OUT

+ × +

Step 1

+

To roughly plan out future events, you provide the minimal details here: start date, end date (optional),the type of Engagement (Assessment, Inspection or Sampling), the external site or operation and finally the inspectors who are carrying out the work.

+

Step 2

+

So that we can track the finer details involved with an Assessment or Inspection, each Engagement comprises additional components, such as Planning, On-site and Reporting. Inspector time can be allocated to these components. In addition, each compontent can be associated with Instruments, such as DSCs, etc. After you create the overarching Engagement using this form, you will have the opportunity to add components.

+
+
+

Enter main details:

+
{% csrf_token %} + {{ form.as_p }} + +
+
+
+ +{% endblock content %} diff --git a/engagements/templates/engagements/engagement_plan_for_site.html b/engagements/templates/engagements/engagement_plan_for_site.html new file mode 100644 index 0000000..55ab54b --- /dev/null +++ b/engagements/templates/engagements/engagement_plan_for_site.html @@ -0,0 +1,35 @@ +{% extends "core/base.html" %} + +{% block title %}Create new engagement{% endblock title %} + +{% block content %} + +
+

{{ title }}

+
+ + × +

Step 1

+

To roughly plan out future events, you provide the minimal details here: start date, end date (optional),the type of Engagement (Assessment, Inspection or Sampling), the external site or operation and finally the inspectors who are carrying out the work.

+

Step 2

+

So that we can track the finer details involved with an Assessment or Inspection, each Engagement comprises additional components, such as Planning, On-site and Reporting. Inspector time can be allocated to these components. In addition, each compontent can be associated with Instruments, such as DSCs, etc. After you create the overarching Engagement using this form, you will have the opportunity to add components.

+
+ +
+
+
+

Enter main details:

+
{% csrf_token %} + {{ form.as_p }} + +
+
+
+

Current Engagement Plan for X

+
+ +
+
+ +{% endblock content %} diff --git a/engagements/templates/engagements/ep_org.html b/engagements/templates/engagements/ep_org.html new file mode 100644 index 0000000..202100a --- /dev/null +++ b/engagements/templates/engagements/ep_org.html @@ -0,0 +1,427 @@ +{% extends "core/base.html" %} + +{% block title %}Create new engagement{% endblock title %} + +{% load table_extras %} + +{% load static %} + +{% block content %} + +
+

Engagement Plans for {{ entity.name }}

+ Add New Regulatory Engagement | Add New Engagement +
+ +
+

2023

+ + + + + + + + + + + + + + + + + {% for e in engagements %} + + + + + + + + + + + + + {% endfor %} + +
Start DateEnd DateEngagementEngagement TypeArea/ThemeInspectors InvolvedDSCsTravel EffortPlanning and Regulation EffortCompletion Status
{{ e.proposed_start_date|date:'j M Y' }}{{ e.proposed_end_date|date:'j M Y' }}{{ e }}{{ e.friendly_type }}Area Theme +
    + {% for inspector in e.officers.all %} +
  • {{ inspector }}
  • + {% endfor %} +
+
+ + TBCTBCIncomplete
+ +
+ +
+ +
+
+
+

Total DSC coverage

+ + + + + + {% for dsc in dscs %} + + + + + {% endfor %} +
DSCAllocated time
{{ dsc }} {{ dsc | effort_for_org:entity }}
+
+
+

Another table of data

+ + + + + + {% for dsc in dscs %} + + + + + {% endfor %} +
DSCAllocated time
{{ dsc }} {{ dsc | effort_for_org:entity }}
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock content %} diff --git a/engagements/templates/engagements/index.html b/engagements/templates/engagements/index.html new file mode 100644 index 0000000..8d5df36 --- /dev/null +++ b/engagements/templates/engagements/index.html @@ -0,0 +1,30 @@ +{% extends "core/base.html" %} + +{% block title %}Engagements{% endblock title %} + +{% block content %} + +
+

Engagement Planning

+
+ +

Your entities

+ + + + + + + + {% for e in entities %} + + + + + + {% endfor %} +
Engagement PlanScheduled EngagementsTotal time
{{ e.name }}{{ e.engagement_set.all.count }}NA
+
+
+ +{% endblock content %} diff --git a/engagements/templates/engagements/organisations.html b/engagements/templates/engagements/organisations.html new file mode 100644 index 0000000..5bbd1b5 --- /dev/null +++ b/engagements/templates/engagements/organisations.html @@ -0,0 +1,50 @@ +{% extends "core/base.html" %} +{% load table_extras %} + +{% block title %}Regulated Entities{% endblock title %} + +{% block content %} + +
+

Regulated Entities

+
+ + + + + + + + + {% for e in entities %} + + + + + + + + {% endfor %} +
EntityTeamLead Inspector[s]Responsible Person[s]Accountable Person[s]
{{ e.name }}{{ e.lead_team }} + {% if e.lead_inspector.all %} + {{ e.lead_inspector.all|commalist }} + {% endif %} + + + {% if e.rp.all %} + {% for p in e.rp.all %} + {{ p }} + {% endfor %} + {% endif %} + + + {% if e.ap.all %} + {% for p in e.ap.all %} + {{ p }} + {% endfor %} + {% endif %} +
+
+
+ +{% endblock content %} diff --git a/engagements/templates/engagements/snippets/effort_summary_panel.html b/engagements/templates/engagements/snippets/effort_summary_panel.html new file mode 100644 index 0000000..235a67a --- /dev/null +++ b/engagements/templates/engagements/snippets/effort_summary_panel.html @@ -0,0 +1,41 @@ +{% load table_extras %} + +{% if e.is_planned %} +
+{% else %} +
+{% endif %} +
+
+
{{ e.proposed_start_date|date:'j M Y' }} - {{ e.effort_type }}
+
+
+

{{ e.effort_total_hours|floatformat }} hrs

+ Planned: {{ e.is_planned }} + +
+
+
+ Inspectors: {{ e.officers.all|commalist }} +
+
+ Start time: {{ e.proposed_start_date|date:'H:i' }} + {% if e.proposed_end_date %} + - {{ e.proposed_end_date|date:'H:i' }} + {% endif %} +
+ +
+ {% if e.sub_instruments.all %} + DSCs: +
    + {% for dsc in e.sub_instruments.all %} +
  • {{ dsc.title }}
  • + {% endfor %} +
+ {% endif %} + +
+
diff --git a/engagements/tests/__init__.py b/engagements/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/engagements/tests/test_forms.py b/engagements/tests/test_forms.py new file mode 100644 index 0000000..b6aab9a --- /dev/null +++ b/engagements/tests/test_forms.py @@ -0,0 +1,45 @@ +from django.test import TestCase + +from engagements.forms import EngagementEffortCreateForm +from engagements.models import Engagement, EngagementType, Organisation +from myuser.models import TeamUser + + +class EngagementEffortCreate(TestCase): + def setUp(self): + data = { + "proposed_start_date": "2022-10-01", + "engagement_type": EngagementType.objects.create(name="ET1"), + "external_party": Organisation.objects.create(name="O1"), + } + self.e = Engagement.objects.create(**data) + self.user = TeamUser.objects.create_user(email="ming@ming.com") + + def test_basic_validation(self): + form = EngagementEffortCreateForm( + data={ + "is_planned": True, + "proposed_start_date": "2022-10-10 10:00", + "proposed_end_date": "2022-10-10 12:00", + "engagement": self.e, + "effort_type": "PLANNING", + "officers": [self.user], + } + ) + self.assertFalse(form.errors) + + def test_basic_validation_on_bad_entry(self): + form = EngagementEffortCreateForm( + data={ + "is_planned": True, + "proposed_start_date": "20240-10-10 10:00", + "proposed_end_date": "2022-10-10 12:00", + "engagement": self.e, + "effort_type": "bobbins", + "officers": [self.user], + "sub_instruments": [""], + } + ) + self.assertTrue(form.errors["effort_type"]) + self.assertTrue(form.errors["proposed_start_date"]) + self.assertTrue(form.errors["sub_instruments"]) diff --git a/engagements/tests/test_models.py b/engagements/tests/test_models.py new file mode 100644 index 0000000..08c5169 --- /dev/null +++ b/engagements/tests/test_models.py @@ -0,0 +1,30 @@ +import pytest +from django.test import TestCase + +from engagements.utils import populate_database + + +class TestModels(TestCase): + @classmethod + def setUpTestData(cls): + cls.data = populate_database() + + @pytest.mark.django_db + def test_check_all_dcs(self): + dscs = self.data.get("sub_instruments") + self.assertEqual(dscs[0].title, "DSC 1 - Title 1") + + @pytest.mark.django_db + def test_effort_by_type(self): + e = self.data["engagements"][0] + total_planning = sum([x.effort_total_planned_hours() for x in e.effort.filter(effort_type="PLANNING")]) + total_travel = sum([x.effort_total_planned_hours() for x in e.effort.filter(effort_type="TRAVEL")]) + total_regulation = sum([x.effort_total_planned_hours() for x in e.effort.filter(effort_type="REGULATION")]) + assert total_planning == 4.25 + assert total_regulation == 0 + assert total_travel == 1 + + # TODO finish this test! + def test_total_effort_for_engagement(self): + e = self.data["engagements"][0] + assert e.total_effort() == 5.25 diff --git a/engagements/tests/test_views.py b/engagements/tests/test_views.py new file mode 100644 index 0000000..f25eb3a --- /dev/null +++ b/engagements/tests/test_views.py @@ -0,0 +1,91 @@ +import datetime +from http import HTTPStatus + +from django.test import RequestFactory, TestCase +from django.urls import reverse + +from engagements import models, views +from engagements.utils import populate_database + + +class TestModels(TestCase): + @classmethod + def setUpTestData(cls): + cls.request = RequestFactory() # for use in _ep_request_factory test + cls.data = populate_database() + + def test_dscs_for_ep(self): + org = self.data["orgs"][0] + # we set up an engagement and effort for this org + et = models.EngagementType.objects.get(name="INSPECTION") + si = self.data["sub_instruments"][0] + si2 = self.data["sub_instruments"][2] + si3 = self.data["sub_instruments"][3] + # si_not = self.data["sub_instruments"][1] + engagement = models.Engagement.objects.create( + proposed_start_date=datetime.date(2022, 10, 10), + proposed_end_date=datetime.date(2022, 10, 10), + engagement_type=et, + external_party=org, + ) + ef1 = models.EngagementEffort.objects.create( + is_planned=True, + effort_type="REGULATION", + proposed_start_date=datetime.date(2022, 10, 10), + proposed_end_date=datetime.date(2022, 10, 10), + engagement=engagement, + ) + ef1.sub_instruments.add(si) # DSC 1 + ef1.sub_instruments.add(si2) # DSC 3 + ef1.sub_instruments.add(si3) # DSC 4 + ef1.save() + url = reverse("engagements:plan_for_org", kwargs={"orgslug": org.slug}) + self.client.force_login(self.data["superuser"]) + response = self.client.get(url) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertTrue(response.context["entity"]) + self.assertEqual(response.context["entity"].name, org.name) + self.assertIn(si, response.context["dscs"]) + self.assertIn(si2, response.context["dscs"]) + self.assertIn(si3, response.context["dscs"]) + self.assertEqual(response.context["dscs"].count(), 3) + # self.assertNotIn(si_not, response.context["dscs"]) + + def test_dscs_for_ep_request_factory(self): + """ + On the EP page, we expect to see a list of all DSCs related to effort + for this organisation. + + Included this here for reference + """ + org = self.data["orgs"][0] + url = reverse("engagements:plan_for_org", kwargs={"orgslug": org.slug}) + request = self.request.get(url) + request.user = self.data["superuser"] + response = views.engagement_plan_for(request, org.slug) + self.assertEqual(response.status_code, HTTPStatus.OK) + + +class TestEngagementEffortView(TestCase): + @classmethod + def setUpTestData(cls): + cls.request = RequestFactory() # for use in _ep_request_factory test + cls.data = populate_database() + + def test_get_blank_form(self): + url = reverse("engagements:effort_create", kwargs={"eid": 1, "etype": "PLANNING"}) + self.client.force_login(self.data["superuser"]) + request = self.request.get(url) + request.user = self.data["superuser"] + response = views.engagement_effort_create(request, eid=1, etype="PLANNING") + self.assertEqual(response.status_code, HTTPStatus.OK) + + # def test_post_data(self): + # url = reverse( + # "engagements:effort_create", kwargs={"eid": 1, "etype": "PLANNING"} + # ) + # self.client.force_login(self.data["superuser"]) + # request = self.request.post(url, {"proposed_start_date": "toss"}) + # request.user = self.data["superuser"] + # response = views.engagement_effort_create(request, eid=1, etype="PLANNING") + # self.assertEqual(response.status_code, HTTPStatus.OK) diff --git a/engagements/urls.py b/engagements/urls.py new file mode 100644 index 0000000..7b6ddd3 --- /dev/null +++ b/engagements/urls.py @@ -0,0 +1,29 @@ +from django.urls import path + +from . import views + +app_name = "engagements" +urlpatterns = [ + path("", views.engagement_planning, name="home"), + path("", views.engagement_detail, name="engagement_detail"), + path("plan//", views.engagement_plan_for, name="plan_for_org"), + path( + "regulatedentities/", + views.RegulatedEntitiesView.as_view(), + name="regulatedentities", + ), + path("edit/", views.engagement_edit, name="edit"), + path("create//", views.engagement_create, name="create"), + path("create//", views.engagement_create, name="create"), + path( + "effort/create//", + views.engagement_effort_create, + name="effort_create", + ), +] + +htmx_urls = [ + path("htmx-effort-planned/", views.htmx_effort_planned, name="htmx-effort-planned") +] + +urlpatterns = urlpatterns + htmx_urls diff --git a/engagements/utils.py b/engagements/utils.py new file mode 100644 index 0000000..2c4ff72 --- /dev/null +++ b/engagements/utils.py @@ -0,0 +1,186 @@ +import random +from collections import defaultdict +from datetime import date, datetime + +from faker import Faker + +from engagements.models import ( + Engagement, + EngagementEffort, + EngagementType, + Organisation, + Person, + RegulatedEntityType, + RegulatoryRole, +) +from instruments.models import Instrument, SubInstrument +from myuser.models import Team, TeamUser + + +def populate_database(): + out = defaultdict(list) + fake = Faker(locale="en_GB") + + # Users and teams + TeamUser.objects.all().delete() + Team.objects.all().delete() + teams = ["Submarines and Propulsion", "Transport"] + Team.objects.all().delete() + tdefnuc = Team.objects.create(name=teams[0]) + u1 = TeamUser.objects.create_superuser( + email="lemon@lemon.com", + password="lemonlemon", + ) + u1.first_name = "Matthew" + u1.last_name = "Lemon" + u1.team = tdefnuc + u1.designation = "LI8" + u1.is_active = True + u1.save() + out["superuser"] = u1 + + desigs = ["LI1", "LI2", "LI3", "LI4", "LI5", "LI6"] + for p in range(6): + first_name = fake.first_name() + last_name = fake.last_name() + u = TeamUser.objects.create_superuser( + email=f"{first_name.lower()}@theregulator.com", + password="fakepassword", + ) + u.first_name = first_name + u.last_name = last_name + u.team = Team.objects.create(name=random.choice(teams)) + u.designation = desigs[p] + out["users"].append(u) + u.save() + + # RegulatoryRoles + RegulatoryRole.objects.all().delete() + RegulatoryRole.objects.create( + name="Responsible Person", + description="The Regulated Person charged with managing, etc", + ) + RegulatoryRole.objects.create(name="Accountable Person") + RegulatoryRole.objects.create( + name="Information Holder", + description="A regulated person who must ensure etc.", + ) + # RegulatedEntityTypes + RegulatedEntityType.objects.all().delete() + ret1 = RegulatedEntityType.objects.create(name="Site") + RegulatedEntityType.objects.create(name="Operation") + RegulatedEntityType.objects.create(name="Distrubuted Site") + # Organisations + Organisation.objects.all().delete() + for _ in range(10): + o = Organisation.objects.create(name=fake.company(), is_regulated_entity=True, entitytype=ret1) + if random.choice([1, 2, 3]) == 2: + u1.lead_for.add(o) + u1.save() + out["orgs"].append(o) + o1 = Organisation.objects.create(name="MOD", is_regulated_entity=False) + out["orgs"].append(o1) + # Instruments + Instrument.objects.all().delete() + j = Instrument.objects.create( + name="JSP 628", + long_title="Security Regulation of the DNE", + designator="JSP628", + owner=o1, + ) + # Create the DSCs + Faker.seed(0) + SubInstrument.objects.all().delete() + for n in range(1, 17): + si = SubInstrument.objects.create( + title=f"DSC {str(n)} - Title {n}", + itype="DSC", + parent=j, + short=f"DSC {n}", + description=fake.paragraph(nb_sentences=2), + rationale=fake.paragraph(nb_sentences=3), + ) + out["sub_instruments"].append(si) + for d in SubInstrument.objects.filter(itype="DSC"): + i = SubInstrument.objects.create( + title=f"DSTAIG {d.pk} - Title {d.pk}", + is_guidance=True, + itype="DSTAIG", + parent=j, + description=fake.paragraph(nb_sentences=2), + rationale=fake.paragraph(nb_sentences=3), + ) + out["sub_instruments"].append(i) + i.relative.add(d) + + # EngagementType + EngagementType.objects.all().delete() + EngagementType.objects.create(name="EMAIL", description=fake.paragraph(4)) + EngagementType.objects.create(name="MEETING", description=fake.paragraph(4)) + EngagementType.objects.create(name="BRIEFING", description=fake.paragraph(4)) + EngagementType.objects.create(name="TEAMSCALL", description=fake.paragraph(4)) + EngagementType.objects.create(name="PHONECALL", description=fake.paragraph(4)) + EngagementType.objects.create(name="L1RIF", description=fake.paragraph(4)) + EngagementType.objects.create(name="L2RIF", description=fake.paragraph(4)) + EngagementType.objects.create(name="L3RIF", description=fake.paragraph(4)) + EngagementType.objects.create(name="L4RIF", description=fake.paragraph(4)) + EngagementType.objects.create(name="SAMPLING", description=fake.paragraph(4)) + EngagementType.objects.create(name="INSPECTION", description=fake.paragraph(4)) + EngagementType.objects.create(name="ASSESSMENT", description=fake.paragraph(4)) + + # People + Person.objects.all().delete() + o_pks = [o.pk for o in Organisation.objects.all()] + for _ in range(5): + p = Person( + first_name=fake.first_name(), + last_name=fake.last_name(), + organisation=Organisation.objects.get(pk=random.choice(o_pks)), + mobile=fake.cellphone_number(), + ) + p.email = f"{p.first_name.lower()}@{p.organisation.slug}.com" + p.save() + out["people"].append(p) + + # Engagement + Engagement.objects.all().delete() + d1 = date(2022, 5, 10) + d2 = date(2022, 5, 12) + # users = get_user_model() + et = EngagementType.objects.get(name="INSPECTION") + ep = Organisation.objects.first() + e = Engagement.objects.create( + proposed_start_date=d1, + proposed_end_date=d2, + engagement_type=et, + external_party=ep, + ) + e.officers.add(u1) + e.save() + out["engagements"].append(e) + + # Effort + EngagementEffort.objects.all().delete() + d1 = datetime(2022, 4, 10, 10, 0, 0) + d2 = datetime(2022, 4, 10, 14, 15, 0) + d3 = datetime(2022, 4, 10, 12, 0, 0) + d4 = datetime(2022, 4, 10, 13, 0, 0) # 1 hour between d3 and d4 + ef = EngagementEffort.objects.create( + is_planned=True, + effort_type="PLANNING", + proposed_start_date=d1, + proposed_end_date=d2, + engagement=e, + ) + EngagementEffort.objects.create( + is_planned=True, + effort_type="TRAVEL", + proposed_start_date=d3, + proposed_end_date=d4, + engagement=e, + ) + ef.officers.add(u1) + ef.sub_instruments.add(out["sub_instruments"][0]) + ef.save() + out["engagement_effort"].append(ef) + return out diff --git a/engagements/views.py b/engagements/views.py new file mode 100644 index 0000000..fb804df --- /dev/null +++ b/engagements/views.py @@ -0,0 +1,182 @@ +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q +from django.shortcuts import get_object_or_404, redirect, render +from django.views.generic import ListView + +from instruments.models import SubInstrument + +from .forms import ( + EngagementCreateForm, + EngagementEffortPlanningCreateForm, + EngagementEffortRegulationCreateForm, + EngagementEffortReportingCreateForm, + EngagementEffortTravelCreateForm, +) +from .models import Engagement, EngagementEffort, EngagementType, Organisation + + +class RegulatedEntitiesView(ListView, LoginRequiredMixin): + context_object_name = "entities" + queryset = Organisation.objects.filter(is_regulated_entity=True).order_by("name") + template_name = "engagements/organisations.html" + + +@login_required +def engagement_planning(request): + user = request.user + reg_orgs = Organisation.objects.filter(is_regulated_entity=True) + my_orgs = reg_orgs.filter(lead_inspector=user) + return render(request, "engagements/index.html", {"entities": my_orgs}) + # display the list and ask which one? + + +@login_required +def htmx_effort_planned(request, effid): + if request.method == "GET": + effort = EngagementEffort.objects.get(id=effid) + if effort.is_planned is True: + effort.is_planned = False + else: + effort.is_planned = True + effort.save() + return render(request, "engagements/snippets/effort_summary_panel.html", {"e" : effort}) + + +@login_required +def engagement_detail(request, pk): + engagement = Engagement.objects.get(pk=pk) + subinstruments = SubInstrument.objects.filter(title__icontains="DSC") + effort = EngagementEffort.objects.filter(engagement=engagement).order_by("proposed_start_date") + dscs = [] + for e in effort: + subs = e.sub_instruments.all() + for s in subs: + dscs.append(s) + dscs = set(dscs) + effort_total = sum(e.effort_total_hours() for e in effort) + effort_planned = sum(e.effort_total_planned_hours() for e in effort) + effort_actual = sum(e.effort_actual() for e in effort) + context = { + "engagement": engagement, + "subinstruments": subinstruments, + "effort": effort, + "effort_total": effort_total, + "effort_planned": effort_planned, + "effort_actual": effort_actual, + "dscs": dscs, + } + return render(request, "engagements/engagement_detail.html", context) + + +@login_required +def engagement_plan_for(request, orgslug): + org = Organisation.objects.get(slug=orgslug) + engagements = Engagement.objects.filter(external_party=org) + dscs = SubInstrument.objects.filter(itype="DSC").filter(effort__engagement__external_party=org).distinct() + context = {"entity": org, "engagements": engagements, "dscs": dscs} + return render(request, "engagements/ep_org.html", context) + + +# class EngagementView(ListView, LoginRequiredMixin): +# context_object_name = "engagements" +# queryset = Engagement.objects.filter(engagement_type__name="INSPECTION") +# template_name = "engagements/index.html" + + +@login_required +def engagement_effort_create(request, eid, etype=None): + forms = { + "TRAVEL": EngagementEffortTravelCreateForm, + "PLANNING": EngagementEffortPlanningCreateForm, + "REGULATION": EngagementEffortRegulationCreateForm, + "REPORTING": EngagementEffortReportingCreateForm, + } + + if request.method == "POST": + engagement = Engagement.objects.get(pk=eid) + # use the specialised type of form on POST - it has the correct fields + form = forms[etype](request.POST) + if form.is_valid(): + eff = form.save(commit=False) + eff.engagement = engagement + eff.save() + eff.officers.add(request.user) + eff.effort_type = etype + eff.save() + form.save_m2m() + return redirect("engagements:engagement_detail", pk=eid) + else: + engagement = Engagement.objects.get(pk=eid) + form = forms.get(etype, EngagementCreateForm)( + initial={ + "proposed_start_date": engagement.proposed_start_date.isoformat(), + "engagement": engagement, + "effort_type": etype, + "officers": request.user, + }, + ) + return render( + request, + "engagements/engagement_effort_create.html", + {"form": form, "engagement": engagement, "etype": etype}, + ) + + +@login_required +def engagement_edit(request, eid): + e = get_object_or_404(Engagement, pk=eid) + form = EngagementCreateForm(request.POST or None, instance=e) + if request.method == "POST": + if form.is_valid(): + form.save() + return redirect("engagements:engagement_detail", pk=e.pk) + return render( + request, + "engagements/engagement_form.html", + {"form": form, "title": f"Edit Engagement {e}"}, + ) + + +@login_required +def engagement_create(request, slug, reg=None): + if request.method == "POST": + form = EngagementCreateForm(request.POST) + if form.is_valid(): + ef = form.save(commit=False) + ef.external_party = Organisation.objects.get(slug=slug) + ef.save() + return redirect("engagements:plan_for_org", orgslug=slug) + else: + if reg: + form = EngagementCreateForm( + initial={ + "engagement_type": EngagementType.objects.filter( + Q(name="ASSESSMENT") + | Q(name="INSPECTION") + | Q(name="SAMPLING") + | Q(name="L1RIF") + | Q(name="L2RIF") + | Q(name="L3RIF") + | Q(name="L1RIF") + ), + "external_party": Organisation.objects.get(slug=slug), + "officers": request.user, + } + ) + return render( + request, + "engagements/engagement_form.html", + {"form": form, "title": "Add New Regulatory Engagement"}, + ) + else: + form = EngagementCreateForm( + initial={ + "officers": request.user, + } + ) + return render( + request, + "engagements/engagement_form.html", + {"form": form, "title": "Add New Engagement"}, + ) -- cgit v1.2.3