From 25185ea901796316796fe7dd014c2bc5f14af01a Mon Sep 17 00:00:00 2001 From: Fabien Moreau Date: Tue, 11 Jun 2024 17:46:04 +0200 Subject: [PATCH] add shift and stop models --- padam_django/apps/common/utils.py | 2 + padam_django/apps/fleet/admin.py | 4 ++ padam_django/apps/fleet/factories.py | 21 +++++++ .../management/commands/create_shifts.py | 13 ++++ .../apps/fleet/migrations/0003_busshift.py | 29 +++++++++ padam_django/apps/fleet/models.py | 40 ++++++++++++ padam_django/apps/geography/admin.py | 13 ++++ padam_django/apps/geography/factories.py | 38 ++++++++++++ .../management/commands/create_lines.py | 13 ++++ .../management/commands/create_stops.py | 13 ++++ .../migrations/0002_auto_20240611_1444.py | 61 +++++++++++++++++++ padam_django/apps/geography/models.py | 50 +++++++++++++++ 12 files changed, 297 insertions(+) create mode 100644 padam_django/apps/common/utils.py create mode 100644 padam_django/apps/fleet/management/commands/create_shifts.py create mode 100644 padam_django/apps/fleet/migrations/0003_busshift.py create mode 100644 padam_django/apps/geography/management/commands/create_lines.py create mode 100644 padam_django/apps/geography/management/commands/create_stops.py create mode 100644 padam_django/apps/geography/migrations/0002_auto_20240611_1444.py diff --git a/padam_django/apps/common/utils.py b/padam_django/apps/common/utils.py new file mode 100644 index 00000000..a9f034af --- /dev/null +++ b/padam_django/apps/common/utils.py @@ -0,0 +1,2 @@ +def period_overlap(p1_start, p1_end, p2_start, p2_end): + return (min(p1_end, p2_end) - max(p1_start, p2_start)).days + 1 > 0 diff --git a/padam_django/apps/fleet/admin.py b/padam_django/apps/fleet/admin.py index 3fba5023..f7185f66 100644 --- a/padam_django/apps/fleet/admin.py +++ b/padam_django/apps/fleet/admin.py @@ -11,3 +11,7 @@ class BusAdmin(admin.ModelAdmin): @admin.register(models.Driver) class DriverAdmin(admin.ModelAdmin): pass + +@admin.register(models.BusShift) +class BusShiftAdmin(admin.ModelAdmin): + pass diff --git a/padam_django/apps/fleet/factories.py b/padam_django/apps/fleet/factories.py index c78c832e..4cc1cbf6 100644 --- a/padam_django/apps/fleet/factories.py +++ b/padam_django/apps/fleet/factories.py @@ -1,4 +1,5 @@ import factory +from datetime import date, datetime, timezone from faker import Faker from . import models @@ -7,6 +8,15 @@ fake = Faker(['fr']) +def random_time_zone_for_today(): + today = date.today() + start_of_day = datetime(today.year, today.month, today.day) + end_of_day = datetime(today.year, today.month, today.day, 23, 59) + + return fake.date_time_between( + start_date=start_of_day, end_date=end_of_day).replace( + tzinfo=timezone.utc).astimezone(tz=None) + class DriverFactory(factory.django.DjangoModelFactory): user = factory.SubFactory('padam_django.apps.users.factories.UserFactory') @@ -19,3 +29,14 @@ class BusFactory(factory.django.DjangoModelFactory): class Meta: model = models.Bus + +class BusShiftFactory(factory.django.DjangoModelFactory): + bus = factory.SubFactory(BusFactory) + driver = factory.SubFactory(DriverFactory) + line = factory.SubFactory('padam_django.apps.geography.factories.BusLineWith5StopFactory') + + departure_time = factory.LazyFunction(random_time_zone_for_today) + arrival_time = factory.LazyFunction(random_time_zone_for_today) + + class Meta: + model = models.BusShift diff --git a/padam_django/apps/fleet/management/commands/create_shifts.py b/padam_django/apps/fleet/management/commands/create_shifts.py new file mode 100644 index 00000000..46fc8571 --- /dev/null +++ b/padam_django/apps/fleet/management/commands/create_shifts.py @@ -0,0 +1,13 @@ +from padam_django.apps.common.management.base import CreateDataBaseCommand + +from padam_django.apps.fleet.factories import BusShiftFactory + + +class Command(CreateDataBaseCommand): + + help = 'Create few shifts' + + def handle(self, *args, **options): + super().handle(*args, **options) + self.stdout.write(f'Creating {self.number} shifts ...') + BusShiftFactory.create_batch(size=self.number) diff --git a/padam_django/apps/fleet/migrations/0003_busshift.py b/padam_django/apps/fleet/migrations/0003_busshift.py new file mode 100644 index 00000000..6dfa8c29 --- /dev/null +++ b/padam_django/apps/fleet/migrations/0003_busshift.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.5 on 2024-06-11 14:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('geography', '0002_auto_20240611_1444'), + ('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')), + ('departure_time', models.DateTimeField()), + ('arrival_time', models.DateTimeField()), + ('bus', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='fleet.bus')), + ('driver', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='fleet.driver')), + ('line', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='geography.busline')), + ], + options={ + 'verbose_name_plural': 'Bus shifts', + }, + ), + ] diff --git a/padam_django/apps/fleet/models.py b/padam_django/apps/fleet/models.py index 4cd3f19d..c272086c 100644 --- a/padam_django/apps/fleet/models.py +++ b/padam_django/apps/fleet/models.py @@ -1,4 +1,7 @@ from django.db import models +from padam_django.apps.common.utils import period_overlap +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ class Driver(models.Model): @@ -16,3 +19,40 @@ class Meta: def __str__(self): return f"Bus: {self.licence_plate} (id: {self.pk})" + + +class BusShift(models.Model): + bus = models.ForeignKey(Bus, null=True, on_delete=models.SET_NULL) + driver = models.ForeignKey(Driver, null=True, on_delete=models.SET_NULL) + line = models.ForeignKey('geography.BusLine', null=True, on_delete=models.SET_NULL) + departure_time = models.DateTimeField() + arrival_time = models.DateTimeField() + + def check_availability_for_entity(self, entity): + """Takes an entity (example: {'driver': driver_object}). + Raises a ValidationError if the entity is not available for this shift""" + start = self.departure_time + end = self.arrival_time + other_shifts_taken_by_entity = BusShift.objects.filter(**entity).exclude(pk=self.pk).all() + for shift in other_shifts_taken_by_entity: + if period_overlap(shift.departure_time, shift.arrival_time, start, end): + entity_type = list(entity.keys())[0] + raise ValidationError( + _(f"The {entity_type} already is on another shift (line: {shift.line}, departure: {shift.departure_time}, arrival: {shift.arrival_time})"), + code="taken_" + entity_type) + + def clean(self): + start = self.departure_time + end = self.arrival_time + if start and end and start > end: + raise ValidationError( + _(f"The departure time {start} must be inferior to the arrival time {end}.)"), + code="departure_time_inferior_to_arrival_time") + self.check_availability_for_entity({'driver': self.driver}) + self.check_availability_for_entity({'bus': self.bus}) + + class Meta: + verbose_name_plural = "Bus shifts" + + def __str__(self): + return f"Bus: {self.bus.licence_plate} - Driver: {self.driver.user} (id: {self.pk})" diff --git a/padam_django/apps/geography/admin.py b/padam_django/apps/geography/admin.py index e0334458..01190889 100644 --- a/padam_django/apps/geography/admin.py +++ b/padam_django/apps/geography/admin.py @@ -6,3 +6,16 @@ @admin.register(models.Place) class PlaceAdmin(admin.ModelAdmin): pass + +@admin.register(models.BusStop) +class BusStopAdmin(admin.ModelAdmin): + pass + +class BusLineStopInline(admin.TabularInline): + model = models.BusLineStop + extra = 0 + +@admin.register(models.BusLine) +class BusLineAdmin(admin.ModelAdmin): + inlines = (BusLineStopInline,) + pass diff --git a/padam_django/apps/geography/factories.py b/padam_django/apps/geography/factories.py index b134a30c..fe7139cb 100644 --- a/padam_django/apps/geography/factories.py +++ b/padam_django/apps/geography/factories.py @@ -15,3 +15,41 @@ class PlaceFactory(factory.django.DjangoModelFactory): class Meta: model = models.Place + +class BusStopFactory(factory.django.DjangoModelFactory): + name = factory.LazyFunction(fake.street_name) + place = factory.SubFactory(PlaceFactory) + + class Meta: + model = models.BusStop + +class BusLineFactory(factory.django.DjangoModelFactory): + number = factory.LazyFunction(lambda: fake.pyint(min_value=0, max_value=300)) + + class Meta: + model = models.BusLine + +class BusLineStopFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.BusLineStop + + stop = factory.SubFactory(BusStopFactory) + line = factory.SubFactory(BusLineFactory) + sequence = factory.LazyFunction(lambda: fake.pyint(min_value=0, max_value=100)) + +class BusLineWith5StopFactory(BusLineFactory): + line_stop1 = factory.RelatedFactory( + BusLineStopFactory, + factory_related_name='line') + line_stop2 = factory.RelatedFactory( + BusLineStopFactory, + factory_related_name='line') + line_stop3 = factory.RelatedFactory( + BusLineStopFactory, + factory_related_name='line') + line_stop4 = factory.RelatedFactory( + BusLineStopFactory, + factory_related_name='line') + line_stop5 = factory.RelatedFactory( + BusLineStopFactory, + factory_related_name='line') diff --git a/padam_django/apps/geography/management/commands/create_lines.py b/padam_django/apps/geography/management/commands/create_lines.py new file mode 100644 index 00000000..241c4ceb --- /dev/null +++ b/padam_django/apps/geography/management/commands/create_lines.py @@ -0,0 +1,13 @@ +from padam_django.apps.common.management.base import CreateDataBaseCommand + +from padam_django.apps.geography.factories import BusLineWith5StopFactory + + +class Command(CreateDataBaseCommand): + + help = 'Create few lines' + + def handle(self, *args, **options): + super().handle(*args, **options) + self.stdout.write(f'Creating {self.number} lines ...') + BusLineWith5StopFactory.create_batch(size=self.number) diff --git a/padam_django/apps/geography/management/commands/create_stops.py b/padam_django/apps/geography/management/commands/create_stops.py new file mode 100644 index 00000000..c7dbdc16 --- /dev/null +++ b/padam_django/apps/geography/management/commands/create_stops.py @@ -0,0 +1,13 @@ +from padam_django.apps.common.management.base import CreateDataBaseCommand + +from padam_django.apps.geography.factories import BusStopFactory + + +class Command(CreateDataBaseCommand): + + help = 'Create few stops' + + def handle(self, *args, **options): + super().handle(*args, **options) + self.stdout.write(f'Creating {self.number} stops ...') + BusStopFactory.create_batch(size=self.number) diff --git a/padam_django/apps/geography/migrations/0002_auto_20240611_1444.py b/padam_django/apps/geography/migrations/0002_auto_20240611_1444.py new file mode 100644 index 00000000..491c1bfa --- /dev/null +++ b/padam_django/apps/geography/migrations/0002_auto_20240611_1444.py @@ -0,0 +1,61 @@ +# Generated by Django 3.2.5 on 2024-06-11 14:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('geography', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='BusLine', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.IntegerField(default='0')), + ], + options={ + 'ordering': ['number'], + }, + ), + migrations.AlterModelOptions( + name='place', + options={'ordering': ['name']}, + ), + migrations.AlterUniqueTogether( + name='place', + unique_together=set(), + ), + migrations.CreateModel( + name='BusStop', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='Name of the place')), + ('place', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stop', to='geography.place')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='BusLineStop', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sequence', models.IntegerField(default='0')), + ('line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geography.busline')), + ('stop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geography.busstop')), + ], + options={ + 'ordering': ['sequence'], + 'unique_together': {('line', 'sequence')}, + }, + ), + migrations.AddField( + model_name='busline', + name='stops', + field=models.ManyToManyField(through='geography.BusLineStop', to='geography.BusStop'), + ), + ] diff --git a/padam_django/apps/geography/models.py b/padam_django/apps/geography/models.py index e566ee2b..a162574b 100644 --- a/padam_django/apps/geography/models.py +++ b/padam_django/apps/geography/models.py @@ -13,3 +13,53 @@ class Meta: def __str__(self): return f"Place: {self.name} (id: {self.pk})" + + class Meta: + ordering = ['name'] + + +class BusStop(models.Model): + name = models.CharField("Name of the place", max_length=50) + place = models.OneToOneField(Place, on_delete=models.CASCADE, related_name='stop', null=True) + + def __str__(self): + return f"Stop: {self.name} (id: {self.pk})" + + class Meta: + ordering = ['name'] + + +class BusLine(models.Model): + number = models.IntegerField(default='0') + stops = models.ManyToManyField(BusStop, through="BusLineStop") + + def stops_list(self): + """Returns the ordered list of the stops given by the line""" + return [x.stop for x in BusLineStop.objects.filter(line=self).order_by('sequence')] + + def __str__(self): + return f"Line: {self.number} - {self.first_stop} - {self.last_stop} (id: {self.pk})" + + @property + def first_stop(self): + return self.stops_list()[0].name if self.stops_list() else '' + + @property + def last_stop(self): + return self.stops_list()[-1].name if self.stops_list() else '' + + class Meta: + ordering = ['number'] + + +class BusLineStop(models.Model): + stop = models.ForeignKey(BusStop, on_delete=models.CASCADE) + line = models.ForeignKey(BusLine, on_delete=models.CASCADE) + sequence = models.IntegerField(default='0') # stops of a given line are ordered by this sequence + + class Meta: + ordering = ['sequence',] + unique_together = (("line", "sequence"), ) + + def __str__(self): + return f"{self.line.number} - {self.stop.name} (id: {self.pk})"