summaryrefslogtreecommitdiffstats
path: root/engagements
diff options
context:
space:
mode:
authorMatthew Lemon <y@yulqen.org>2024-04-23 11:16:38 +0100
committerMatthew Lemon <y@yulqen.org>2024-04-23 11:16:38 +0100
commit0f951dcf029d4af284467543a3afdf5bf6581a20 (patch)
treea48384210cdc168e3bd3ccff6d6d516eeed9e748 /engagements
parent8b084e9fe7a5f3a04c32daf9a24f7f2cf67300f9 (diff)
switched to Django
Diffstat (limited to 'engagements')
-rw-r--r--engagements/__init__.py0
-rw-r--r--engagements/admin.py32
-rw-r--r--engagements/apps.py6
-rw-r--r--engagements/forms.py242
-rw-r--r--engagements/management/commands/create_engagement_data.py14
-rw-r--r--engagements/migrations/0001_initial.py134
-rw-r--r--engagements/migrations/0002_initial.py44
-rw-r--r--engagements/migrations/0003_engagementeffort_notes.py18
-rw-r--r--engagements/migrations/__init__.py0
-rw-r--r--engagements/models.py236
-rw-r--r--engagements/static/js/yoap.js12
-rw-r--r--engagements/templates/engagements/engagement_create.html29
-rw-r--r--engagements/templates/engagements/engagement_detail.html78
-rw-r--r--engagements/templates/engagements/engagement_effort_create.html20
-rw-r--r--engagements/templates/engagements/engagement_form.html32
-rw-r--r--engagements/templates/engagements/engagement_plan_for_site.html35
-rw-r--r--engagements/templates/engagements/ep_org.html427
-rw-r--r--engagements/templates/engagements/index.html30
-rw-r--r--engagements/templates/engagements/organisations.html50
-rw-r--r--engagements/templates/engagements/snippets/effort_summary_panel.html41
-rw-r--r--engagements/tests/__init__.py0
-rw-r--r--engagements/tests/test_forms.py45
-rw-r--r--engagements/tests/test_models.py30
-rw-r--r--engagements/tests/test_views.py91
-rw-r--r--engagements/urls.py29
-rw-r--r--engagements/utils.py186
-rw-r--r--engagements/views.py182
27 files changed, 2043 insertions, 0 deletions
diff --git a/engagements/__init__.py b/engagements/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/engagements/__init__.py
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": ("<br><small><em>To distinguish planned events from retrospective recording</em></small>")
+ }
+ 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": ("<br><small><em>To distinguish planned events from retrospective recording</em></small>")
+ }
+ 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": ("<br><small><em>To distinguish planned events from retrospective recording</em></small>")
+ }
+ 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": "<small><em>YYYY-MM-DD HH:MM</em> "
+ "(<strong>Please include time here</strong>)</small>",
+ "proposed_end_date": "<small><em>YYYY-MM-DD HH:MM</em> "
+ "(<strong>Please include time here</strong>)</small>",
+ }
+
+
+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": "<small><em>YYYY-MM-DD</em></small>",
+ "proposed_end_date": "<small><em>YYYY-MM-DD</em></small>",
+ }
+ 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
--- /dev/null
+++ b/engagements/migrations/__init__.py
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 %}
+
+<div class="w3-container w3-cell-row w3-margin-bottom">
+ <h2>{{ title }}</h2>
+ <div class="w3-panel w3-light-blue w3-round w3-leftbar w3-border-blue w3-display-container">
+
+ <span onclick="this.parentElement.style.display='none'"
+ class="w3-button w3-display-topright">&times;</span>
+ <h4>Step 1</h4>
+ <p>To roughly plan out future events, you provide the minimal details here: <strong>start date</strong>, <strong>end date</strong> (optional),the <strong>type of Engagement</strong> (Assessment, Inspection or Sampling), the <strong>external site</strong> or operation and finally the <strong>inspectors</strong> who are carrying out the work.</p>
+ <h4>Step 2</h4>
+ <p>So that we can track the finer details involved with an Assessment or Inspection, each Engagement comprises additional <em>components</em>, such as <strong>Planning</strong>, <strong>On-site</strong> and <strong>Reporting</strong>. Inspector time can be allocated to these components. In addition, each compontent can be associated with <strong>Instruments</strong>, such as <strong>DSCs</strong>, etc. After you create the overarching Engagement using this form, you will have the opportunity to add components.</p>
+ </div>
+
+ <hr>
+ <div class="w3-container w3-cell-middle">
+ <h4>Enter main details:</h4>
+ <form method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" class="w3-btn w3-green" value="Save">
+ </form>
+ </div>
+</div>
+
+{% 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 %}
+
+<div class="w3-container">
+ <div class="w3-container w3-center">
+ <h2>{{ engagement.friendly_type }} at {{ engagement.external_party }}</h2>
+ </div>
+ <div class="w3-row">
+ <div class="w3-container">
+ <div>
+ <a href="{% url 'engagements:edit' engagement.pk %}">Edit Engagement</a>
+ </div>
+ <section class="w3-container w3-blue">
+ <h3>Details</h3>
+ </section>
+ <table class="w3-table w3-border w3-light-gray w3-bordered">
+ <tr>
+ <td><strong>Date</strong></td>
+ <td>{{ engagement.proposed_start_date|date:'l' }} - {{ engagement.proposed_start_date|date:'j M Y' }}</td>
+ </tr>
+ <tr>
+ <td><strong>Site/Operation:</strong></td>
+ <td>{{ engagement.external_party }} </td>
+ </tr>
+ <tr>
+ <td><strong>Subject of Activity</strong></td>
+ <td>
+ <p>Summmary text</p>
+ <ul class="w3-ul">
+ {% for t in dscs %}
+ <li><a href='#'>{{ t }}</a></li>
+ {% endfor %}
+ </ul>
+ </td>
+ </tr>
+ <tr>
+ <td><strong>Inspectors:</strong></td>
+ <td>{{ engagement.officers.all|commalist }}</td>
+ </tr>
+ <tr>
+ <td><strong>Planned Effort:</strong></td>
+ <td>{{ effort_planned|floatformat }} hrs</td>
+ </tr>
+ <tr>
+ <td><strong>Actual Effort:</strong></td>
+ <td>{{ effort_actual|floatformat }} hrs</td>
+ </tr>
+ <tr>
+ <td><strong>Total Effort:</strong></td>
+ <td>{{ effort_total|floatformat }} hrs</td>
+ </tr>
+ </table>
+ <hr>
+ </div>
+
+ <div class="w3-container w3-light-gray">
+ <h3>Effort for this engagement</h3>
+ Add:
+ <a href="{% url 'engagements:effort_create' engagement.pk "TRAVEL" %}">Travel</a> |
+ <a href="{% url 'engagements:effort_create' engagement.pk "PLANNING" %}">Planning</a> |
+ <a href="{% url 'engagements:effort_create' engagement.pk "REGULATION" %}">Regulation</a> |
+ <a href="{% url 'engagements:effort_create' engagement.pk "REPORTING" %}">Reporting</a>
+
+ {% if effort %}
+ {% for e in effort.all %}
+ <div id="planned_swap_{{ e.id }}">
+ {% include "engagements/snippets/effort_summary_panel.html" with e=e %}
+ </div>
+ {% endfor %}
+ {% endif %}
+ </div>
+ </div>
+
+ {% 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 %}
+
+ <div class="w3-container w3-cell-row w3-margin-bottom">
+ <h2>Register your {{ etype|lower }} effort for the {{ engagement.engagement_type.name|title }} event at {{ engagement.external_party }} on {{ engagement.proposed_start_date }}</h2>
+
+ <div class="w3-container w3-cell-middle">
+ <h4>Enter details:</h4>
+ <form method="post">{% csrf_token %}
+ {% crispy form form.helper %}
+ </form>
+ </div>
+ </div>
+
+{% 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 %}
+<link rel="stylesheet" href="{% static 'css/styles.css' %}"/>
+{% endblock extra_head_tags %}
+
+{% block content %}
+
+<div class="format-container">
+ <div class="form-thing">
+ <h2>{{ title }}</h2>
+ <p>TAKE THIS OUT</p>
+ <span onclick="this.parentElement.style.display='none'"
+ class="w3-button w3-display-topright">&times;</span>
+ <h4>Step 1</h4>
+ <p>To roughly plan out future events, you provide the minimal details here: <strong>start date</strong>, <strong>end date</strong> (optional),the <strong>type of Engagement</strong> (Assessment, Inspection or Sampling), the <strong>external site</strong> or operation and finally the <strong>inspectors</strong> who are carrying out the work.</p>
+ <h4>Step 2</h4>
+ <p>So that we can track the finer details involved with an Assessment or Inspection, each Engagement comprises additional <em>components</em>, such as <strong>Planning</strong>, <strong>On-site</strong> and <strong>Reporting</strong>. Inspector time can be allocated to these components. In addition, each compontent can be associated with <strong>Instruments</strong>, such as <strong>DSCs</strong>, etc. After you create the overarching Engagement using this form, you will have the opportunity to add components.</p>
+ </div>
+ <div class="form-thing">
+ <h4>Enter main details:</h4>
+ <form method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" class="w3-btn w3-green" value="Save">
+ </form>
+ </div>
+</div>
+
+{% 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 %}
+
+<div class="w3-container w3-cell-row w3-margin-bottom">
+ <h2>{{ title }}</h2>
+ <div class="w3-panel w3-light-blue w3-round w3-leftbar w3-border-blue w3-display-container">
+
+ <span onclick="this.parentElement.style.display='none'"
+ class="w3-button w3-display-topright">&times;</span>
+ <h4>Step 1</h4>
+ <p>To roughly plan out future events, you provide the minimal details here: <strong>start date</strong>, <strong>end date</strong> (optional),the <strong>type of Engagement</strong> (Assessment, Inspection or Sampling), the <strong>external site</strong> or operation and finally the <strong>inspectors</strong> who are carrying out the work.</p>
+ <h4>Step 2</h4>
+ <p>So that we can track the finer details involved with an Assessment or Inspection, each Engagement comprises additional <em>components</em>, such as <strong>Planning</strong>, <strong>On-site</strong> and <strong>Reporting</strong>. Inspector time can be allocated to these components. In addition, each compontent can be associated with <strong>Instruments</strong>, such as <strong>DSCs</strong>, etc. After you create the overarching Engagement using this form, you will have the opportunity to add components.</p>
+ </div>
+
+ <hr>
+ <div class="w3-cell-row">
+ <div class="w3-container w3-cell">
+ <h4>Enter main details:</h4>
+ <form method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" class="w3-btn w3-green" value="Save">
+ </form>
+ </div>
+ <div class="w3-container w3-cell">
+ <h4>Current Engagement Plan for X</h4>
+ </div>
+
+ </div>
+</div>
+
+{% 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 %}
+
+<div class="w3-container">
+ <h2>Engagement Plans for {{ entity.name }}</h2>
+ <a href="{% url 'engagements:create' entity.slug 'reg' %}">Add New Regulatory Engagement</a> | <a href="{% url 'engagements:create' entity.slug %}">Add New Engagement</a>
+</div>
+
+<div class="w3-container">
+ <h3>2023</h3>
+ <table class="ep-table">
+ <thead>
+ <tr>
+ <th>Start Date</th>
+ <th>End Date</th>
+ <th>Engagement</th>
+ <th>Engagement Type</th>
+ <th>Area/Theme</th>
+ <th>Inspectors Involved</th>
+ <th>DSCs</th>
+ <th>Travel Effort</th>
+ <th>Planning and Regulation Effort</th>
+ <th>Completion Status</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for e in engagements %}
+ <tr>
+ <td>{{ e.proposed_start_date|date:'j M Y' }}</td>
+ <td>{{ e.proposed_end_date|date:'j M Y' }}</td>
+ <td><a href="{% url 'engagements:engagement_detail' e.pk %}">{{ e }}</a></td>
+ <td>{{ e.friendly_type }}</td>
+ <td>Area Theme</td>
+ <td>
+ <ul class="w3-ul">
+ {% for inspector in e.officers.all %}
+ <li>{{ inspector }} </li>
+ {% endfor %}
+ </ul>
+ </td>
+ <td>
+ <ul class="w3-ul">
+ {% for dsc in e.dscs %}
+ <li><a href="#">{{ dsc.short }}</a></li>
+ {% endfor %}
+ </ul>
+ </td>
+ <td>TBC</td>
+ <td>TBC</td>
+ <td class="ep-table">Incomplete</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+
+</div>
+
+<hr>
+
+<div class="w3-container">
+ <div class="w3-cell-row">
+ <div class="w3-container w3-cell">
+ <h3>Total DSC coverage</h3>
+ <table class="summary-table">
+ <tr>
+ <th>DSC</th>
+ <th>Allocated time</th>
+ </tr>
+ {% for dsc in dscs %}
+ <tr>
+ <td>{{ dsc }} </td>
+ <td>{{ dsc | effort_for_org:entity }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ <div class="w3-container w3-cell">
+ <h3>Another table of data</h3>
+ <table class="summary-table">
+ <tr>
+ <th>DSC</th>
+ <th>Allocated time</th>
+ </tr>
+ {% for dsc in dscs %}
+ <tr>
+ <td>{{ dsc }} </td>
+ <td>{{ dsc | effort_for_org:entity }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+
+ </div>
+</div>
+
+<style type="text/css" media="screen">
+
+ /* (A) CONTAINER */
+ /* https://code-boxx.com/beginner-create-grid-html-css/ */
+ #grid-col {
+
+ /* (A1) GRID LAYOUT */
+ display: grid;
+
+ /* (A2) SPECIFY COLUMNS */
+ grid-template-columns: 75px repeat(31, 40px);
+
+ /* we can also specify exact pixels, percentage, repeat
+ grid-template-columns: 50px 100px 150px;
+ grid-template-columns: 25% 50% 25%;
+ grid-template-columns: 100px 20% auto;
+ grih-template-columns: repeat(3, auto); */
+ }
+
+ /* (B) GRID CELLS */
+ div.cell {
+ background: white;
+ border: 1px solid lightgray;
+ padding: 10px;
+ }
+
+ div.cellred{
+ background: red;
+ border: 1px solid lightgray;
+ padding: 10px;
+ }
+
+ div.cellyellow{
+ background: yellow;
+ border: 1px solid lightgray;
+ padding: 10px;
+ }
+ div.cellorange{
+ background: orange;
+ border: 1px solid lightgray;
+ padding: 10px;
+ }
+ /* (C) RESPONSIVE - 1 COLUMN ON SMALL SCREENS */
+ /* @media screen and (max-width: 640px) {
+ #grid-col { grid-template-columns: 100%; }
+ }
+ */
+/* </style>
+<!-- <div class="w3-container w3-padding-16"> -->
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell"></div><div class="cell">1</div> -->
+<!-- <div class="cell">2</div><div class="cell">3</div> -->
+<!-- <div class="cell">4</div><div class="cell">5</div> -->
+<!-- <div class="cell">6</div><div class="cell">7</div> -->
+<!-- <div class="cell">8</div><div class="cell">9</div> -->
+<!-- <div class="cell">10</div><div class="cell">11</div> -->
+<!-- <div class="cell">12</div><div class="cell">13</div> -->
+<!-- <div class="cell">14</div><div class="cell">15</div> -->
+<!-- <div class="cell">16</div><div class="cell">17</div> -->
+<!-- <div class="cell">18</div><div class="cell">19</div> -->
+<!-- <div class="cell">20</div><div class="cell">21</div> -->
+<!-- <div class="cell">22</div><div class="cell">23</div> -->
+<!-- <div class="cell">24</div><div class="cell">25</div> -->
+<!-- <div class="cell">26</div><div class="cell">27</div> -->
+<!-- <div class="cell">28</div><div class="cell">29</div> -->
+<!-- <div class="cell">30</div><div class="cell">31</div> -->
+<!-- </div> -->
+<!-- </div> -->
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Jan</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cellred"><small></small></div><div class="cellred"><small></small></div> -->
+<!-- <div class="cellred"><small></small></div><div class="cellred"><small></small></div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Feb</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cellyellow"><small></small></div><div class="cellyellow"><small></small></div> -->
+<!-- <div class="cellyellow"><small></small></div><div class="cellyellow"><small></small></div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Mar</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cellorange"><small></small></div><div class="cellorange"><small></small></div> -->
+<!-- <div class="cellorange"><small></small></div><div class="cellorange"><small></small></div> -->
+<!-- <div class="cellorange"><small></small></div><div class="cellorange"><small></small></div> -->
+<!-- <div class="cellorange"><small></small></div><div class="cellorange"><small></small></div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- <div class="cell">-</div><div class="cell">-</div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Apr</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">May</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Jun</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Jul</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Aug</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Sep</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Oct</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Nov</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+<!-- <div class="grid"> -->
+<!-- <div id="grid-col"> -->
+<!-- <div class="cell">Dec</div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- <div class="cell"></div><div class="cell"></div> -->
+<!-- </div> -->
+<!-- </div> -->
+
+
+<script src="{% static 'js/yoap.js' %}"></script>
+
+{% 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 %}
+
+<div class="w3-container">
+ <h2>Engagement Planning</h2>
+ <div class="w3-container">
+
+ <h3>Your entities</h3>
+
+ <table class="w3-table w3-bordered">
+ <tr>
+ <th>Engagement Plan</th>
+ <th>Scheduled Engagements</th>
+ <th>Total time</th>
+ </tr>
+ {% for e in entities %}
+ <tr>
+ <td><a href="{% url 'engagements:plan_for_org' e.slug %}">{{ e.name }}</a></td>
+ <td>{{ e.engagement_set.all.count }}</td>
+ <td>NA</td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+</div>
+
+{% 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 %}
+
+<div class="w3-container">
+ <h1>Regulated Entities</h1>
+ <div class="w3-container">
+ <table class="w3-table w3-bordered w3-striped">
+ <tr>
+ <th>Entity</th>
+ <th>Team</th>
+ <th>Lead Inspector[s]</th>
+ <th>Responsible Person[s]</th>
+ <th>Accountable Person[s]</th>
+ </tr>
+ {% for e in entities %}
+ <tr>
+ <td>{{ e.name }}</td>
+ <td>{{ e.lead_team }}</td>
+ <td>
+ {% if e.lead_inspector.all %}
+ {{ e.lead_inspector.all|commalist }}
+ {% endif %}
+ </td>
+ <td>
+ <!-- rp -->
+ {% if e.rp.all %}
+ {% for p in e.rp.all %}
+ {{ p }}
+ {% endfor %}
+ {% endif %}
+ </td>
+ <td>
+ <!-- ap -->
+ {% if e.ap.all %}
+ {% for p in e.ap.all %}
+ {{ p }}
+ {% endfor %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+</div>
+
+{% 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 %}
+ <div class="w3-panel w3-pale-green w3-topbar w3-border-green">
+{% else %}
+ <div class="w3-panel w3-light-gray w3-topbar w3-border-green">
+{% endif %}
+ <div class="w3-cell-row">
+ <div class="w3-cell w3-align-left">
+ <h5>{{ e.proposed_start_date|date:'j M Y' }} - {{ e.effort_type }}</h5>
+ </div>
+ <div class="w3-cell w3-right-align">
+ <p> {{ e.effort_total_hours|floatformat }} hrs</p>
+ Planned: {{ e.is_planned }}
+ <button hx-get="{% url 'engagements:htmx-effort-planned' e.id %}" hx-target="#planned_swap_{{ e.id }}">
+ Flip
+ </button>
+ </div>
+ </div>
+ <div>
+ <strong>Inspectors:</strong> {{ e.officers.all|commalist }}
+ </div>
+ <div>
+ <strong>Start time</strong>: {{ e.proposed_start_date|date:'H:i' }}
+ {% if e.proposed_end_date %}
+ - {{ e.proposed_end_date|date:'H:i' }}
+ {% endif %}
+ </div>
+
+ <div>
+ {% if e.sub_instruments.all %}
+ <strong>DSCs</strong>:
+ <ul>
+ {% for dsc in e.sub_instruments.all %}
+ <li><a href="#">{{ dsc.title }}</a></li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+
+ </div>
+</div>
diff --git a/engagements/tests/__init__.py b/engagements/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/engagements/tests/__init__.py
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("<int:pk>", views.engagement_detail, name="engagement_detail"),
+ path("plan/<slug:orgslug>/", views.engagement_plan_for, name="plan_for_org"),
+ path(
+ "regulatedentities/",
+ views.RegulatedEntitiesView.as_view(),
+ name="regulatedentities",
+ ),
+ path("edit/<int:eid>", views.engagement_edit, name="edit"),
+ path("create/<slug:slug>/", views.engagement_create, name="create"),
+ path("create/<slug:slug>/<str:reg>", views.engagement_create, name="create"),
+ path(
+ "effort/create/<int:eid>/<str:etype>",
+ views.engagement_effort_create,
+ name="effort_create",
+ ),
+]
+
+htmx_urls = [
+ path("htmx-effort-planned/<int:effid>", 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"},
+ )