From 4fb2a55cdd4e8608c5c376ba854d8172c46bc1f8 Mon Sep 17 00:00:00 2001 From: Giuseppe De Ponte Date: Tue, 24 Sep 2024 16:31:32 +0200 Subject: [PATCH] BusStop and BusShift implementation --- .gitignore | 3 + .idea/.gitignore | 8 - .idea/inspectionProfiles/Project_Default.xml | 6 - .../inspectionProfiles/profiles_settings.xml | 6 - .idea/misc.xml | 4 - .idea/modules.xml | 8 - .idea/padam-django-tech-test.iml | 23 --- .../common/management/commands/create_data.py | 12 +- padam_django/apps/fleet/admin.py | 13 -- padam_django/apps/fleet/admin/__init__.py | 1 + padam_django/apps/fleet/admin/formsets.py | 38 +++++ padam_django/apps/fleet/admin/inlines.py | 14 ++ padam_django/apps/fleet/admin/panels.py | 21 +++ padam_django/apps/fleet/factories.py | 12 +- .../management/commands/create_bus_stops.py | 12 ++ .../0003_add_bus_stop_and_bus_shift_models.py | 140 ++++++++++++++++++ padam_django/apps/fleet/models.py | 89 ++++++++++- 17 files changed, 333 insertions(+), 77 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/padam-django-tech-test.iml delete mode 100644 padam_django/apps/fleet/admin.py create mode 100644 padam_django/apps/fleet/admin/__init__.py create mode 100644 padam_django/apps/fleet/admin/formsets.py create mode 100644 padam_django/apps/fleet/admin/inlines.py create mode 100644 padam_django/apps/fleet/admin/panels.py create mode 100644 padam_django/apps/fleet/management/commands/create_bus_stops.py create mode 100644 padam_django/apps/fleet/migrations/0003_add_bus_stop_and_bus_shift_models.py diff --git a/.gitignore b/.gitignore index d7d26693..54e1906b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ ENV/ # Editors stuff .idea .vscode + +# Local stuff +.python-version \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 73f69e09..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 03d9549e..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2da..00000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 574ec96e..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 2253d389..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/padam-django-tech-test.iml b/.idea/padam-django-tech-test.iml deleted file mode 100644 index c7ffe09b..00000000 --- a/.idea/padam-django-tech-test.iml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/padam_django/apps/common/management/commands/create_data.py b/padam_django/apps/common/management/commands/create_data.py index a149a937..68abfee8 100644 --- a/padam_django/apps/common/management/commands/create_data.py +++ b/padam_django/apps/common/management/commands/create_data.py @@ -4,11 +4,11 @@ class Command(BaseCommand): - - help = 'Create test data' + help = "Create test data" def handle(self, *args, **options): - management.call_command('create_users', number=5) - management.call_command('create_drivers', number=5) - management.call_command('create_buses', number=10) - management.call_command('create_places', number=30) + management.call_command("create_users", number=5) + management.call_command("create_drivers", number=5) + management.call_command("create_buses", number=10) + management.call_command("create_places", number=30) + management.call_command("create_bus_stops", number=40) diff --git a/padam_django/apps/fleet/admin.py b/padam_django/apps/fleet/admin.py deleted file mode 100644 index 3fba5023..00000000 --- a/padam_django/apps/fleet/admin.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.contrib import admin - -from . import models - - -@admin.register(models.Bus) -class BusAdmin(admin.ModelAdmin): - pass - - -@admin.register(models.Driver) -class DriverAdmin(admin.ModelAdmin): - pass diff --git a/padam_django/apps/fleet/admin/__init__.py b/padam_django/apps/fleet/admin/__init__.py new file mode 100644 index 00000000..c686947d --- /dev/null +++ b/padam_django/apps/fleet/admin/__init__.py @@ -0,0 +1 @@ +from .panels import * diff --git a/padam_django/apps/fleet/admin/formsets.py b/padam_django/apps/fleet/admin/formsets.py new file mode 100644 index 00000000..943d6ee5 --- /dev/null +++ b/padam_django/apps/fleet/admin/formsets.py @@ -0,0 +1,38 @@ +from django import forms +from django.core.exceptions import ValidationError + + +class ScheduledStopInlineFormSet(forms.models.BaseInlineFormSet): + def clean(self): + super().clean() + + # Validate non-overlapping shift schedule for current instance driver and bus + times = [ + form.cleaned_data["time"] + for form in self.forms + if form.cleaned_data.get("time", None) + ] + + if times and len(times) >= 2: + departure = min(times) + arrival = max(times) + + if ( + self.instance.driver + and self.instance.driver.shifts.exclude(pk=self.instance.pk) + .overlapping(departure, arrival) + .exists() + ): + raise ValidationError( + "The selected driver already has a shift overlapping this one" + ) + + if ( + self.instance.bus + and self.instance.bus.shifts.exclude(pk=self.instance.pk) + .overlapping(departure, arrival) + .exists() + ): + raise ValidationError( + "The selected bus already has a shift overlapping this one" + ) diff --git a/padam_django/apps/fleet/admin/inlines.py b/padam_django/apps/fleet/admin/inlines.py new file mode 100644 index 00000000..f5e3e306 --- /dev/null +++ b/padam_django/apps/fleet/admin/inlines.py @@ -0,0 +1,14 @@ +from django.contrib import admin + +from padam_django.apps.fleet import models +from padam_django.apps.fleet.admin.formsets import ScheduledStopInlineFormSet + + +class ScheduledStopInlineAdmin(admin.TabularInline): + formset = ScheduledStopInlineFormSet + model = models.BusShiftScheduledStop + min_num = 2 + + def get_formset(self, request, obj=None, **kwargs): + kwargs.update(validate_min=True) + return super().get_formset(request, obj, **kwargs) diff --git a/padam_django/apps/fleet/admin/panels.py b/padam_django/apps/fleet/admin/panels.py new file mode 100644 index 00000000..466f74ad --- /dev/null +++ b/padam_django/apps/fleet/admin/panels.py @@ -0,0 +1,21 @@ +from django.contrib import admin + +from padam_django.apps.fleet import models +from padam_django.apps.fleet.admin.inlines import ScheduledStopInlineAdmin + + +@admin.register(models.Bus) +class BusAdmin(admin.ModelAdmin): + pass + + +@admin.register(models.Driver) +class DriverAdmin(admin.ModelAdmin): + pass + + +@admin.register(models.BusShift) +class BusShiftAdmin(admin.ModelAdmin): + list_display = ("pk", "bus", "driver", "departure", "arrival") + fields = ("bus", "driver") + inlines = (ScheduledStopInlineAdmin,) diff --git a/padam_django/apps/fleet/factories.py b/padam_django/apps/fleet/factories.py index c78c832e..d5e69fb2 100644 --- a/padam_django/apps/fleet/factories.py +++ b/padam_django/apps/fleet/factories.py @@ -4,11 +4,11 @@ from . import models -fake = Faker(['fr']) +fake = Faker(["fr"]) class DriverFactory(factory.django.DjangoModelFactory): - user = factory.SubFactory('padam_django.apps.users.factories.UserFactory') + user = factory.SubFactory("padam_django.apps.users.factories.UserFactory") class Meta: model = models.Driver @@ -19,3 +19,11 @@ class BusFactory(factory.django.DjangoModelFactory): class Meta: model = models.Bus + + +class BusStopFactory(factory.django.DjangoModelFactory): + name = factory.LazyFunction(fake.address) + place = factory.SubFactory("padam_django.apps.geography.factories.PlaceFactory") + + class Meta: + model = models.BusStop diff --git a/padam_django/apps/fleet/management/commands/create_bus_stops.py b/padam_django/apps/fleet/management/commands/create_bus_stops.py new file mode 100644 index 00000000..c2fe47d0 --- /dev/null +++ b/padam_django/apps/fleet/management/commands/create_bus_stops.py @@ -0,0 +1,12 @@ +from padam_django.apps.common.management.base import CreateDataBaseCommand + +from padam_django.apps.fleet.factories import BusStopFactory + + +class Command(CreateDataBaseCommand): + help = "Create few bus stops" + + def handle(self, *args, **options): + super().handle(*args, **options) + self.stdout.write(f"Creating {self.number} bus stops ...") + BusStopFactory.create_batch(size=self.number) diff --git a/padam_django/apps/fleet/migrations/0003_add_bus_stop_and_bus_shift_models.py b/padam_django/apps/fleet/migrations/0003_add_bus_stop_and_bus_shift_models.py new file mode 100644 index 00000000..a08db74a --- /dev/null +++ b/padam_django/apps/fleet/migrations/0003_add_bus_stop_and_bus_shift_models.py @@ -0,0 +1,140 @@ +# Generated by Django 3.2.5 on 2024-09-24 13:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("geography", "0001_initial"), + ("fleet", "0002_auto_20211109_1456"), + ] + + operations = [ + migrations.CreateModel( + name="BusShift", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "bus", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="shifts", + related_query_name="shift", + to="fleet.bus", + ), + ), + ( + "driver", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="shifts", + related_query_name="shift", + to="fleet.driver", + ), + ), + ], + options={ + "verbose_name": "Bus shift", + "verbose_name_plural": "Bus shifts", + }, + ), + migrations.CreateModel( + name="BusStop", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=200, verbose_name="Name of the bus stop" + ), + ), + ( + "place", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="bus_stop", + to="geography.place", + ), + ), + ], + options={ + "verbose_name": "Bus stop", + "verbose_name_plural": "Bus stops", + }, + ), + migrations.CreateModel( + name="BusShiftScheduledStop", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "time", + models.TimeField(verbose_name="Time of passage through the stop"), + ), + ( + "shift", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="scheduled_stops", + related_query_name="scheduled_stop", + to="fleet.busshift", + ), + ), + ( + "stop", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="fleet.busstop" + ), + ), + ], + options={ + "ordering": ("time",), + }, + ), + migrations.AddField( + model_name="busshift", + name="stops", + field=models.ManyToManyField( + related_name="shifts", + related_query_name="shift", + through="fleet.BusShiftScheduledStop", + to="fleet.BusStop", + ), + ), + migrations.AddConstraint( + model_name="busshiftscheduledstop", + constraint=models.UniqueConstraint( + fields=("stop", "shift"), name="unique_for_stop_shift" + ), + ), + migrations.AddConstraint( + model_name="busshiftscheduledstop", + constraint=models.UniqueConstraint( + fields=("shift", "time"), name="unique_for_shift_time" + ), + ), + ] diff --git a/padam_django/apps/fleet/models.py b/padam_django/apps/fleet/models.py index 4cd3f19d..d0fbf3a6 100644 --- a/padam_django/apps/fleet/models.py +++ b/padam_django/apps/fleet/models.py @@ -2,7 +2,9 @@ class Driver(models.Model): - user = models.OneToOneField('users.User', on_delete=models.CASCADE, related_name='driver') + user = models.OneToOneField( + "users.User", on_delete=models.CASCADE, related_name="driver" + ) def __str__(self): return f"Driver: {self.user.username} (id: {self.pk})" @@ -16,3 +18,88 @@ class Meta: def __str__(self): return f"Bus: {self.licence_plate} (id: {self.pk})" + + +class BusStop(models.Model): + name = models.CharField("Name of the bus stop", max_length=200) + place = models.OneToOneField( + "geography.Place", on_delete=models.PROTECT, related_name="bus_stop" + ) + + class Meta: + verbose_name = "Bus stop" + verbose_name_plural = "Bus stops" + + def __str__(self): + return f"Bus stop: {self.name} (id: {self.pk})" + + +class BusShiftScheduledStop(models.Model): + stop = models.ForeignKey("fleet.BusStop", on_delete=models.CASCADE) + shift = models.ForeignKey( + "fleet.BusShift", + on_delete=models.CASCADE, + related_name="scheduled_stops", + related_query_name="scheduled_stop", + ) + time = models.TimeField("Time of passage through the stop") + + class Meta: + ordering = ("time",) + constraints = ( + models.UniqueConstraint( + name="unique_for_stop_shift", fields=("stop", "shift") + ), + models.UniqueConstraint( + name="unique_for_shift_time", fields=("shift", "time") + ), + ) + + def __str__(self): + return f"{self.stop.name} {self.time}" + + +class BusShiftQueryset(models.QuerySet): + def overlapping(self, departure, arrival): + return self.filter( + scheduled_stop__time__gte=departure, + scheduled_stop__time__lte=arrival, + ) + + +class BusShift(models.Model): + objects = BusShiftQueryset.as_manager() + bus = models.ForeignKey( + "fleet.Bus", + on_delete=models.PROTECT, + related_name="shifts", + related_query_name="shift", + ) + driver = models.ForeignKey( + "fleet.Driver", + on_delete=models.PROTECT, + related_name="shifts", + related_query_name="shift", + ) + stops = models.ManyToManyField( + "fleet.BusStop", + through=BusShiftScheduledStop, + related_name="shifts", + related_query_name="shift", + ) + + def departure(self): + return self.scheduled_stops.order_by("time").first() + + def arrival(self): + return self.scheduled_stops.order_by("time").last() + + def time_range(self): + return self.departure().time, self.arrival().time + + class Meta: + verbose_name = "Bus shift" + verbose_name_plural = "Bus shifts" + + def __str__(self): + return f"Bus shift: {self.departure()} — {self.arrival()} (id: {self.pk})"