From 90f64435413bcbe21a9889c87742b7c2bb10b3eb Mon Sep 17 00:00:00 2001 From: GaspardHome Date: Thu, 31 Oct 2024 12:04:34 +0100 Subject: [PATCH 1/7] initial commit --- .gitignore | 2 + padam_django/apps/trips/__init__.py | 0 padam_django/apps/trips/admin.py | 13 ++++ padam_django/apps/trips/apps.py | 5 ++ padam_django/apps/trips/forms.py | 94 +++++++++++++++++++++++++++++ padam_django/apps/trips/models.py | 73 ++++++++++++++++++++++ padam_django/settings.py | 1 + 7 files changed, 188 insertions(+) create mode 100644 padam_django/apps/trips/__init__.py create mode 100644 padam_django/apps/trips/admin.py create mode 100644 padam_django/apps/trips/apps.py create mode 100644 padam_django/apps/trips/forms.py create mode 100644 padam_django/apps/trips/models.py diff --git a/.gitignore b/.gitignore index d7d26693..32975960 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ ENV/ # Editors stuff .idea .vscode + +myenv \ No newline at end of file diff --git a/padam_django/apps/trips/__init__.py b/padam_django/apps/trips/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padam_django/apps/trips/admin.py b/padam_django/apps/trips/admin.py new file mode 100644 index 00000000..8b2f6bf8 --- /dev/null +++ b/padam_django/apps/trips/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from .models import BusStop, BusShift +from .forms import BusShiftForm + + +class BusShiftAdmin(admin.ModelAdmin): + form = BusShiftForm + list_display = ["bus", "driver", "start_time", "end_time", "duration"] + search_fields = ["bus__licence_plate", "driver__user__username"] + + +admin.site.register(BusStop) +admin.site.register(BusShift, BusShiftAdmin) diff --git a/padam_django/apps/trips/apps.py b/padam_django/apps/trips/apps.py new file mode 100644 index 00000000..acf40bca --- /dev/null +++ b/padam_django/apps/trips/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TripsConfig(AppConfig): + name = "padam_django.apps.trips" diff --git a/padam_django/apps/trips/forms.py b/padam_django/apps/trips/forms.py new file mode 100644 index 00000000..f8fe007a --- /dev/null +++ b/padam_django/apps/trips/forms.py @@ -0,0 +1,94 @@ +from django import forms +from django.core.exceptions import ValidationError +from django.db.models import Q +from .models import BusShift +from datetime import timedelta + + +class BusShiftForm(forms.ModelForm): + """ + A Django form for creating or updating `BusShift` instances, which represent + individual bus shifts that must contain a unique sequence of at least two bus stops. + + This form performs validation on the set of bus stops provided to ensure: + - No duplicate stops are included within the shift. + - At least two unique bus stops are specified for a valid shift. + + Attributes: + model: The model associated with this form, set to `BusShift`. + fields: Specifies that all fields in `BusShift` should be included in the form. + """ + + class Meta: + model = BusShift + fields = "__all__" + + def clean(self): + """ + Override the clean method to perform custom validation on bus stops. + + This method checks: + - No duplicate bus stops are included in the shift. + - At least two unique bus stops are specified. + + Raises: + ValidationError: If duplicate stops are found or fewer than two unique stops are provided. + + Returns: + cleaned_data: The validated form data. + """ + cleaned_data = super().clean() + stops = self.cleaned_data.get("stops") + + if stops: + unique_stops = set() + for stop in stops: + unique_stops.add(stop) + + if len(unique_stops) < 2: + raise ValidationError("At least two bus stops are required.") + + self._calculate_shift_times(unique_stops) + self._validate_unique_shift() + return cleaned_data + + def _calculate_shift_times(self, unique_stops): + """ + Calculate start_time, end_time, and duration based on unique bus stops. + Assigns None values if no stops are assigned, and duration as zero. + """ + if unique_stops: + sorted_stops = sorted(unique_stops, key=lambda x: x.stop_time) + first_stop = sorted_stops[0] + last_stop = sorted_stops[-1] + + self.instance.start_time = first_stop.stop_time + self.instance.end_time = last_stop.stop_time + self.instance.duration = self.instance.end_time - self.instance.start_time + else: + self.instance.start_time = self.instance.end_time = None + self.instance.duration = timedelta(0) + + def _validate_unique_shift(self): + """ + Ensure there are no overlapping shifts for the same bus or driver. + + Raises: + ValidationError: If any overlap exists with another shift's start or end times. + """ + + if not self.instance.start_time or not self.instance.end_time: + return + + bus = self.cleaned_data.get("bus") + driver = self.cleaned_data.get("driver") + if bus and driver and self.instance.start_time and self.instance.end_time: + overlapping_shifts = BusShift.objects.exclude(pk=self.instance.pk).filter( + Q(bus=bus) | Q(driver=driver), + Q(start_time__lt=self.instance.end_time), + Q(end_time__gt=self.instance.start_time), + ) + if overlapping_shifts.exists(): + raise ValidationError( + "This shift overlaps with an existing shift for the same bus or driver." + ) diff --git a/padam_django/apps/trips/models.py b/padam_django/apps/trips/models.py new file mode 100644 index 00000000..572e6568 --- /dev/null +++ b/padam_django/apps/trips/models.py @@ -0,0 +1,73 @@ +from django.db import models +from django.core.exceptions import ValidationError +from datetime import timedelta + + +class BusStop(models.Model): + """ + Represents a bus stop with a specific place, stop time, and unique identifier (name). + + Attributes: + name (str): The name of the bus stop. + place (ForeignKey): The geographical location of the stop. + stop_time (DateTimeField): The scheduled stop time at this place. + + Meta: + constraints: Ensures that a unique combination of name, place, and stop_time exists. + ordering: Orders bus stops by their stop time by default. + """ + + name = models.CharField(max_length=100) + place = models.ForeignKey( + "geography.Place", on_delete=models.CASCADE, related_name="bus_stops" + ) + stop_time = models.DateTimeField("Stop Time") + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["name", "place", "stop_time"], + name="unique_place_stop_time", + ) + ] + ordering = ["stop_time"] + + def __str__(self): + return f"{self.place} at {self.stop_time}" + + +class BusShift(models.Model): + """ + Represents a shift for a specific bus and driver with assigned stops, start and end times, and a duration. + + Attributes: + bus (ForeignKey): The bus assigned to this shift. + driver (ForeignKey): The driver assigned to this shift. + stops (ManyToManyField): The sequence of bus stops for the shift. + duration (DurationField): Calculated total duration of the shift. + start_time (DateTimeField): Calculated start time of the shift. + end_time (DateTimeField): Calculated end time of the shift. + + Methods: + clean(): Validates the shift before saving, ensuring unique times, at least two stops, and non-overlapping times. + _calculate_shift_times(): Sets start_time, end_time, and duration based on stops. + _validate_unique_shift(): Validates the shift does not overlap with another for the same bus or driver. + _validate_minimal_busstops_number(): Ensures a minimum of two stops are assigned. + save(): Overrides save to include validations before saving. + """ + + bus = models.ForeignKey( + "fleet.Bus", on_delete=models.CASCADE, related_name="shifts" + ) + driver = models.ForeignKey( + "fleet.Driver", on_delete=models.CASCADE, related_name="shifts" + ) + stops = models.ManyToManyField("BusStop") + duration = models.DurationField("Duration", null=True, blank=True, editable=False) + start_time = models.DateTimeField( + "Start Time", null=True, blank=True, editable=False + ) + end_time = models.DateTimeField("End Time", null=True, blank=True, editable=False) + + def __str__(self): + return f"Bus shift with Bus {self.bus} and driver {self.driver}" diff --git a/padam_django/settings.py b/padam_django/settings.py index 129e922c..8725047d 100644 --- a/padam_django/settings.py +++ b/padam_django/settings.py @@ -45,6 +45,7 @@ 'padam_django.apps.fleet', 'padam_django.apps.geography', 'padam_django.apps.users', + 'padam_django.apps.trips', ] MIDDLEWARE = [ From 1a70b8abba172dd29287f0728ca7cdc4406103e2 Mon Sep 17 00:00:00 2001 From: GaspardHome Date: Thu, 31 Oct 2024 12:06:20 +0100 Subject: [PATCH 2/7] add tests --- padam_django/apps/trips/tests/__init__.py | 0 padam_django/apps/trips/tests/test_trips.py | 166 ++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 padam_django/apps/trips/tests/__init__.py create mode 100644 padam_django/apps/trips/tests/test_trips.py diff --git a/padam_django/apps/trips/tests/__init__.py b/padam_django/apps/trips/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padam_django/apps/trips/tests/test_trips.py b/padam_django/apps/trips/tests/test_trips.py new file mode 100644 index 00000000..15aa9f2c --- /dev/null +++ b/padam_django/apps/trips/tests/test_trips.py @@ -0,0 +1,166 @@ +from django.test import TestCase +from django.utils import timezone +from django.db import IntegrityError +from django.core.exceptions import ValidationError +from ..models import BusStop, BusShift +from ...geography.models import Place +from ...fleet.models import Bus, Driver +from ..forms import BusShiftForm +from django.contrib.auth import get_user_model +from datetime import timedelta + + +class BusShiftModelTests(TestCase): + + def setUp(self): + self.bus = Bus.objects.create(licence_plate="BUS1") + self.driver = Driver.objects.create(user=self.create_user()) + self.place1 = Place.objects.create(name="Stop 1", longitude=0.0, latitude=0.0) + self.place2 = Place.objects.create(name="Stop 2", longitude=0.1, latitude=0.1) + self.stop_time1 = timezone.now() + self.stop_time2 = self.stop_time1 + timedelta(minutes=10) + + def create_user(self): + """Helper method to create a user for the driver.""" + User = get_user_model() + return User.objects.create_user(username="driver_user", password="password") + + def test_create_bus_stop(self): + """Test that a BusStop can be created successfully.""" + unique_place = Place.objects.create(name="Stop A Unique", longitude=0.2, latitude=0.2) + bus_stop = BusStop.objects.create(place=unique_place, stop_time=timezone.now()) + self.assertIsInstance(bus_stop, BusStop) + self.assertEqual(bus_stop.place, unique_place) + + def test_create_bus_shift_with_valid_stops(self): + """Test that creating a BusShift with two stops is valid.""" + bus_shift = BusShift(bus=self.bus, driver=self.driver) + bus_shift.save() + + bus_stop1 = BusStop.objects.create(place=self.place1, stop_time=self.stop_time1) + bus_stop2 = BusStop.objects.create(place=self.place2, stop_time=self.stop_time2) + bus_shift.stops.add(bus_stop1, bus_stop2) + + try: + bus_shift.clean() + bus_shift.save() + self.assertIsNotNone(bus_shift.pk) + except ValidationError as e: + self.fail(f"BusShift creation failed: {str(e)}") + + def test_bus_shift_with_two_stops(self): + """Test that a BusShift with exactly two stops passes validation.""" + bus_stop1 = BusStop.objects.create(place=self.place1, stop_time=self.stop_time1) + bus_stop2 = BusStop.objects.create(place=self.place2, stop_time=self.stop_time2) + + bus_shift = BusShift(bus=self.bus, driver=self.driver) + bus_shift.save() + bus_shift.stops.add(bus_stop1, bus_stop2) + + try: + bus_shift.clean() + except ValidationError: + self.fail("BusShift.clean() raised ValidationError unexpectedly with two stops.") + + def test_unique_bus_stop_constraint(self): + """Test that creating duplicate BusStops raises an IntegrityError.""" + place = Place.objects.create(name="Unique Place", longitude=1.0, latitude=2.0) + + BusStop.objects.create(name="Stop A", place=place, stop_time=timezone.now()) + + with self.assertRaises(IntegrityError): + BusStop.objects.create(name="Stop A", place=place, stop_time=timezone.now()) + + +class BusShiftFormTests(TestCase): + + def setUp(self): + self.bus = Bus.objects.create(licence_plate="BUS1") + self.driver = Driver.objects.create(user=self.create_user()) + self.place1 = Place.objects.create(name="Stop 1", longitude=0.0, latitude=0.0) + self.place2 = Place.objects.create(name="Stop 2", longitude=0.1, latitude=0.1) + self.stop_time1 = timezone.now() + self.stop_time2 = self.stop_time1 + timedelta(minutes=10) + self.start_time1 = timezone.now() + timedelta(hours=2) + self.end_time1 = self.start_time1 + timedelta(hours=1) + self.bus_shift1 = BusShift.objects.create( + bus=self.bus, + driver=self.driver, + start_time=self.start_time1, + end_time=self.end_time1 + ) + + def create_user(self): + """Helper method to create a user for the driver.""" + User = get_user_model() + return User.objects.create_user(username="driver_user", password="password") + + def test_valid_form_with_two_unique_stops(self): + """Test that a form with two unique stops is valid.""" + bus_stop1 = BusStop.objects.create(place=self.place1, stop_time=self.stop_time1) + bus_stop2 = BusStop.objects.create(place=self.place2, stop_time=self.stop_time2) + + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'stops': [bus_stop1.id, bus_stop2.id], + } + form = BusShiftForm(data=form_data) + self.assertTrue(form.is_valid()) + + def test_form_with_one_stop(self): + """Test that a form with one stop raises a ValidationError.""" + bus_stop1 = BusStop.objects.create(place=self.place1, stop_time=self.stop_time1) + + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'stops': [bus_stop1.id], + } + form = BusShiftForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('__all__', form.errors) + self.assertEqual(form.errors['__all__'], ["At least two bus stops are required."]) + + def test_form_with_no_stops(self): + """Test that a form with no stops raises a ValidationError.""" + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'stops': [], + } + form = BusShiftForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('stops', form.errors) + self.assertEqual(form.errors['stops'], ["This field is required."]) + + def test_calculate_shift_times(self): + """Test that the shift times are calculated correctly.""" + bus_stop1 = BusStop.objects.create(place=self.place1, stop_time=self.stop_time1) + bus_stop2 = BusStop.objects.create(place=self.place2, stop_time=self.stop_time2) + + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'stops': [bus_stop1.id, bus_stop2.id], + } + form = BusShiftForm(data=form_data) + if form.is_valid(): + bus_shift = form.save() + self.assertEqual(bus_shift.start_time, self.stop_time1) + self.assertEqual(bus_shift.end_time, self.stop_time2) + self.assertEqual(bus_shift.duration, self.stop_time2 - self.stop_time1) + + def test_no_overlap_with_another_shift(self): + """Test that non-overlapping shifts do not raise a ValidationError.""" + bus_shift3 = BusShift( + bus=self.bus, + driver=self.driver, + start_time=self.start_time1, + end_time=self.end_time1 + ) + + try: + bus_shift3.clean() + except ValidationError: + self.fail("clean() raised ValidationError unexpectedly!") From 8a80b8c4537992013c31a6713199c5dd51f4dcc6 Mon Sep 17 00:00:00 2001 From: GaspardHome Date: Thu, 31 Oct 2024 12:06:59 +0100 Subject: [PATCH 3/7] add migrations --- .../apps/trips/migrations/0001_initial.py | 99 +++++++++++++++++++ .../apps/trips/migrations/__init__.py | 0 2 files changed, 99 insertions(+) create mode 100644 padam_django/apps/trips/migrations/0001_initial.py create mode 100644 padam_django/apps/trips/migrations/__init__.py diff --git a/padam_django/apps/trips/migrations/0001_initial.py b/padam_django/apps/trips/migrations/0001_initial.py new file mode 100644 index 00000000..c3a84515 --- /dev/null +++ b/padam_django/apps/trips/migrations/0001_initial.py @@ -0,0 +1,99 @@ +# Generated by Django 3.2.5 on 2024-10-30 22:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("geography", "0001_initial"), + ("fleet", "0002_auto_20211109_1456"), + ] + + operations = [ + migrations.CreateModel( + name="BusStop", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("stop_time", models.DateTimeField(verbose_name="Stop Time")), + ( + "place", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="bus_stops", + to="geography.place", + ), + ), + ], + options={ + "ordering": ["stop_time"], + }, + ), + migrations.CreateModel( + name="BusShift", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "duration", + models.DurationField( + blank=True, editable=False, null=True, verbose_name="Duration" + ), + ), + ( + "start_time", + models.DateTimeField( + blank=True, editable=False, null=True, verbose_name="Start Time" + ), + ), + ( + "end_time", + models.DateTimeField( + blank=True, editable=False, null=True, verbose_name="End Time" + ), + ), + ( + "bus", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="shifts", + to="fleet.bus", + ), + ), + ( + "driver", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="shifts", + to="fleet.driver", + ), + ), + ("stops", models.ManyToManyField(to="trips.BusStop")), + ], + ), + migrations.AddConstraint( + model_name="busstop", + constraint=models.UniqueConstraint( + fields=("name", "place", "stop_time"), name="unique_place_stop_time" + ), + ), + ] diff --git a/padam_django/apps/trips/migrations/__init__.py b/padam_django/apps/trips/migrations/__init__.py new file mode 100644 index 00000000..e69de29b From 687bca8f5e60b9321437200ff93a1faba341839c Mon Sep 17 00:00:00 2001 From: GaspardHome Date: Thu, 31 Oct 2024 12:44:33 +0100 Subject: [PATCH 4/7] update docstring --- padam_django/apps/trips/models.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/padam_django/apps/trips/models.py b/padam_django/apps/trips/models.py index 572e6568..fd416e96 100644 --- a/padam_django/apps/trips/models.py +++ b/padam_django/apps/trips/models.py @@ -47,13 +47,6 @@ class BusShift(models.Model): duration (DurationField): Calculated total duration of the shift. start_time (DateTimeField): Calculated start time of the shift. end_time (DateTimeField): Calculated end time of the shift. - - Methods: - clean(): Validates the shift before saving, ensuring unique times, at least two stops, and non-overlapping times. - _calculate_shift_times(): Sets start_time, end_time, and duration based on stops. - _validate_unique_shift(): Validates the shift does not overlap with another for the same bus or driver. - _validate_minimal_busstops_number(): Ensures a minimum of two stops are assigned. - save(): Overrides save to include validations before saving. """ bus = models.ForeignKey( From 36fec722dc27bcb1d5d6e208ace8c15c9747a0b6 Mon Sep 17 00:00:00 2001 From: GaspardHome Date: Thu, 31 Oct 2024 12:46:58 +0100 Subject: [PATCH 5/7] remove unused imports --- padam_django/apps/trips/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/padam_django/apps/trips/models.py b/padam_django/apps/trips/models.py index fd416e96..e0915000 100644 --- a/padam_django/apps/trips/models.py +++ b/padam_django/apps/trips/models.py @@ -1,6 +1,4 @@ from django.db import models -from django.core.exceptions import ValidationError -from datetime import timedelta class BusStop(models.Model): From 2dcdc05403e5f698bf883c5663c2f2b543942bc8 Mon Sep 17 00:00:00 2001 From: GaspardHome Date: Mon, 4 Nov 2024 12:46:18 +0100 Subject: [PATCH 6/7] fix test, set trip duration as a property --- padam_django/apps/trips/forms.py | 2 - .../apps/trips/migrations/0001_initial.py | 92 ++++--------------- padam_django/apps/trips/models.py | 6 +- padam_django/apps/trips/tests/test_trips.py | 3 +- 4 files changed, 26 insertions(+), 77 deletions(-) diff --git a/padam_django/apps/trips/forms.py b/padam_django/apps/trips/forms.py index f8fe007a..845c7255 100644 --- a/padam_django/apps/trips/forms.py +++ b/padam_django/apps/trips/forms.py @@ -64,10 +64,8 @@ def _calculate_shift_times(self, unique_stops): self.instance.start_time = first_stop.stop_time self.instance.end_time = last_stop.stop_time - self.instance.duration = self.instance.end_time - self.instance.start_time else: self.instance.start_time = self.instance.end_time = None - self.instance.duration = timedelta(0) def _validate_unique_shift(self): """ diff --git a/padam_django/apps/trips/migrations/0001_initial.py b/padam_django/apps/trips/migrations/0001_initial.py index c3a84515..25a7b140 100644 --- a/padam_django/apps/trips/migrations/0001_initial.py +++ b/padam_django/apps/trips/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.5 on 2024-10-30 22:59 +# Generated by Django 3.2.5 on 2024-11-04 11:24 from django.db import migrations, models import django.db.models.deletion @@ -9,91 +9,37 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("geography", "0001_initial"), - ("fleet", "0002_auto_20211109_1456"), + ('fleet', '0002_auto_20211109_1456'), + ('geography', '0001_initial'), ] operations = [ migrations.CreateModel( - name="BusStop", + name='BusStop', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=100)), - ("stop_time", models.DateTimeField(verbose_name="Stop Time")), - ( - "place", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="bus_stops", - to="geography.place", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('stop_time', models.DateTimeField(verbose_name='Stop Time')), + ('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bus_stops', to='geography.place')), ], options={ - "ordering": ["stop_time"], + 'ordering': ['stop_time'], }, ), migrations.CreateModel( - name="BusShift", + name='BusShift', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "duration", - models.DurationField( - blank=True, editable=False, null=True, verbose_name="Duration" - ), - ), - ( - "start_time", - models.DateTimeField( - blank=True, editable=False, null=True, verbose_name="Start Time" - ), - ), - ( - "end_time", - models.DateTimeField( - blank=True, editable=False, null=True, verbose_name="End Time" - ), - ), - ( - "bus", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="shifts", - to="fleet.bus", - ), - ), - ( - "driver", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="shifts", - to="fleet.driver", - ), - ), - ("stops", models.ManyToManyField(to="trips.BusStop")), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('duration', models.DurationField(blank=True, editable=False, null=True, verbose_name='Duration')), + ('start_time', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Start Time')), + ('end_time', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='End Time')), + ('bus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shifts', to='fleet.bus')), + ('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shifts', to='fleet.driver')), + ('stops', models.ManyToManyField(to='trips.BusStop')), ], ), migrations.AddConstraint( - model_name="busstop", - constraint=models.UniqueConstraint( - fields=("name", "place", "stop_time"), name="unique_place_stop_time" - ), + model_name='busstop', + constraint=models.UniqueConstraint(fields=('name', 'place', 'stop_time'), name='unique_place_stop_time'), ), ] diff --git a/padam_django/apps/trips/models.py b/padam_django/apps/trips/models.py index e0915000..6e4f569a 100644 --- a/padam_django/apps/trips/models.py +++ b/padam_django/apps/trips/models.py @@ -54,11 +54,15 @@ class BusShift(models.Model): "fleet.Driver", on_delete=models.CASCADE, related_name="shifts" ) stops = models.ManyToManyField("BusStop") - duration = models.DurationField("Duration", null=True, blank=True, editable=False) start_time = models.DateTimeField( "Start Time", null=True, blank=True, editable=False ) end_time = models.DateTimeField("End Time", null=True, blank=True, editable=False) + @property + def duration(self): + if self.end_time and self.start_time: + return self.end_time - self.start_time + def __str__(self): return f"Bus shift with Bus {self.bus} and driver {self.driver}" diff --git a/padam_django/apps/trips/tests/test_trips.py b/padam_django/apps/trips/tests/test_trips.py index 15aa9f2c..3351442e 100644 --- a/padam_django/apps/trips/tests/test_trips.py +++ b/padam_django/apps/trips/tests/test_trips.py @@ -19,6 +19,7 @@ def setUp(self): self.place2 = Place.objects.create(name="Stop 2", longitude=0.1, latitude=0.1) self.stop_time1 = timezone.now() self.stop_time2 = self.stop_time1 + timedelta(minutes=10) + self.stop_time3 = self.stop_time1 def create_user(self): """Helper method to create a user for the driver.""" @@ -69,7 +70,7 @@ def test_unique_bus_stop_constraint(self): BusStop.objects.create(name="Stop A", place=place, stop_time=timezone.now()) with self.assertRaises(IntegrityError): - BusStop.objects.create(name="Stop A", place=place, stop_time=timezone.now()) + BusStop.objects.create(name="Stop A", place=place, stop_time=self.stop_time3) class BusShiftFormTests(TestCase): From 18b8031f6c77fea5d49ffdb34d1abd8903635309 Mon Sep 17 00:00:00 2001 From: GaspardHome Date: Mon, 4 Nov 2024 12:48:23 +0100 Subject: [PATCH 7/7] remove unused imports, fix docstring --- padam_django/apps/trips/forms.py | 1 - padam_django/apps/trips/models.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/padam_django/apps/trips/forms.py b/padam_django/apps/trips/forms.py index 845c7255..d7276506 100644 --- a/padam_django/apps/trips/forms.py +++ b/padam_django/apps/trips/forms.py @@ -2,7 +2,6 @@ from django.core.exceptions import ValidationError from django.db.models import Q from .models import BusShift -from datetime import timedelta class BusShiftForm(forms.ModelForm): diff --git a/padam_django/apps/trips/models.py b/padam_django/apps/trips/models.py index 6e4f569a..83049c5d 100644 --- a/padam_django/apps/trips/models.py +++ b/padam_django/apps/trips/models.py @@ -42,7 +42,6 @@ class BusShift(models.Model): bus (ForeignKey): The bus assigned to this shift. driver (ForeignKey): The driver assigned to this shift. stops (ManyToManyField): The sequence of bus stops for the shift. - duration (DurationField): Calculated total duration of the shift. start_time (DateTimeField): Calculated start time of the shift. end_time (DateTimeField): Calculated end time of the shift. """ @@ -61,6 +60,13 @@ class BusShift(models.Model): @property def duration(self): + """ + Calculate the duration of the shift based on the start and end times. + + Returns: + timedelta: The duration of the shift if both start_time and end_time are defined. + None: If either start_time or end_time is None, duration cannot be calculated. + """ if self.end_time and self.start_time: return self.end_time - self.start_time