From 67b9ca2f589ebdb22e12723f4f596079fd701310 Mon Sep 17 00:00:00 2001 From: aaarrouf Date: Tue, 25 Nov 2025 11:49:35 +0100 Subject: [PATCH 1/3] Create BusShift and BusStop models --- Makefile | 10 +++ padam_django/apps/fleet/admin.py | 6 +- padam_django/apps/fleet/factories.py | 25 ++++++++ padam_django/apps/fleet/forms.py | 17 +++++ .../apps/fleet/migrations/0003_busshift.py | 50 +++++++++++++++ padam_django/apps/fleet/models.py | 51 +++++++++++++++ padam_django/apps/fleet/tests.py | 63 +++++++++++++++++++ padam_django/apps/geography/admin.py | 4 ++ padam_django/apps/geography/factories.py | 6 ++ .../apps/geography/migrations/0002_busstop.py | 38 +++++++++++ padam_django/apps/geography/models.py | 9 +++ requirements.txt | 2 +- 12 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 padam_django/apps/fleet/forms.py create mode 100644 padam_django/apps/fleet/migrations/0003_busshift.py create mode 100644 padam_django/apps/fleet/tests.py create mode 100644 padam_django/apps/geography/migrations/0002_busstop.py diff --git a/Makefile b/Makefile index 4062f4c4..b0f0be0c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,15 @@ run: ## Run the test server. python manage.py runserver_plus +migrate: ## Migrate django models. + python manage.py makemigrations + python manage.py migrate + +superuser: ## Create a superuser. + python manage.py createsuperuser + install: ## Install the python requirements. pip install -r requirements.txt + +test: ## Run the tests. + python manage.py test diff --git a/padam_django/apps/fleet/admin.py b/padam_django/apps/fleet/admin.py index 3fba5023..01bac052 100644 --- a/padam_django/apps/fleet/admin.py +++ b/padam_django/apps/fleet/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from . import models - +from . import forms @admin.register(models.Bus) class BusAdmin(admin.ModelAdmin): @@ -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): + form = forms.BusShiftForm \ No newline at end of file diff --git a/padam_django/apps/fleet/factories.py b/padam_django/apps/fleet/factories.py index c78c832e..b5634514 100644 --- a/padam_django/apps/fleet/factories.py +++ b/padam_django/apps/fleet/factories.py @@ -1,5 +1,9 @@ import factory from faker import Faker +from datetime import timedelta +from django.utils import timezone +from . import models +from ..geography.factories import BusStopFactory from . import models @@ -19,3 +23,24 @@ class BusFactory(factory.django.DjangoModelFactory): class Meta: model = models.Bus + +class BusShiftFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.BusShift + + bus = factory.SubFactory(BusFactory) + driver = factory.SubFactory(DriverFactory) + departure_time = factory.LazyFunction(lambda: timezone.now()) + arrival_time = factory.LazyAttribute( + lambda obj: obj.departure_time + timedelta(hours=2) + ) + + @factory.post_generation + def bus_stop(self,create,extracted,**kwargs): + if not create: + return + if extracted: + for stop in extracted: + self.bus_stop.add(stop) + else: + self.bus_stop.add(BusStopFactory(),BusStopFactory()) \ No newline at end of file diff --git a/padam_django/apps/fleet/forms.py b/padam_django/apps/fleet/forms.py new file mode 100644 index 00000000..d1acb71d --- /dev/null +++ b/padam_django/apps/fleet/forms.py @@ -0,0 +1,17 @@ +from django import forms +from .models import BusShift + +class BusShiftForm(forms.ModelForm): + class Meta: + model = BusShift + fields = "__all__" + + def clean_bus_stop(self): + bus_stops = self.cleaned_data["bus_stop"] + + if len(bus_stops) < 2: + raise forms.ValidationError( + "A shift must have at least 2 bus stops." + ) + + return bus_stops \ No newline at end of file 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..ac26863e --- /dev/null +++ b/padam_django/apps/fleet/migrations/0003_busshift.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.16 on 2025-11-25 10:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("geography", "0002_busstop"), + ("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( + on_delete=django.db.models.deletion.CASCADE, + related_name="shifts", + to="fleet.bus", + ), + ), + ("bus_stop", models.ManyToManyField(to="geography.busstop")), + ( + "driver", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="shifts", + to="fleet.driver", + ), + ), + ], + options={ + "verbose_name_plural": "Buses shifts", + }, + ), + ] diff --git a/padam_django/apps/fleet/models.py b/padam_django/apps/fleet/models.py index 4cd3f19d..4d1c27b3 100644 --- a/padam_django/apps/fleet/models.py +++ b/padam_django/apps/fleet/models.py @@ -1,3 +1,4 @@ +from django.core.validators import ValidationError from django.db import models @@ -16,3 +17,53 @@ class Meta: def __str__(self): return f"Bus: {self.licence_plate} (id: {self.pk})" + +class BusShift(models.Model): + bus = models.ForeignKey(Bus,on_delete=models.CASCADE,related_name="shifts") + driver = models.ForeignKey(Driver,on_delete=models.CASCADE,related_name="shifts") + bus_stop = models.ManyToManyField("geography.BusStop") + departure_time = models.DateTimeField() + arrival_time = models.DateTimeField() + + class Meta: + verbose_name_plural = "Buses shifts" + + def __str__(self): + return f"BusShift: {self.bus.licence_plate}, Driver: {self.driver.user.username} (id: {self.pk})" + + def clean(self): + """Validate the rules before saving """ + + errors = {} + + # Departure time must be before arrival + if self.departure_time >= self.arrival_time: + errors['departure_time'] = "Departure time must be before arrival time." + + # Check overlapping for bus shifts + bus_overlap = BusShift.objects.filter( + bus=self.bus, + departure_time__lt=self.arrival_time, + arrival_time__gt=self.departure_time + ).exclude(pk=self.pk) + + # Check overlapping for driver shifts + driver_overlap = BusShift.objects.filter( + driver=self.driver, + departure_time__lt=self.arrival_time, + arrival_time__gt=self.departure_time + ).exclude(pk=self.pk) + + + if bus_overlap.exists(): + errors["bus"] = "This bus already has a shift in this time interval." + + if driver_overlap.exists(): + errors["driver"] = "This driver already has a shift in this time interval." + + if errors: + raise ValidationError(errors) + + def save(self, *args, **kwargs): + self.full_clean() + super().save(*args, **kwargs) \ No newline at end of file diff --git a/padam_django/apps/fleet/tests.py b/padam_django/apps/fleet/tests.py new file mode 100644 index 00000000..e78e6e8d --- /dev/null +++ b/padam_django/apps/fleet/tests.py @@ -0,0 +1,63 @@ +from datetime import timedelta +from django.utils import timezone + +from django.test import TestCase +from .factories import DriverFactory,BusFactory,BusShiftFactory + + +class BusShiftTestCase(TestCase): + """ + Tets for the BusShift model and its rules. + """ + + def setUp(self): + """ + Objects for all tests: + - Two buses + - Two drivers + - Common base time + """ + self.bus1 = BusFactory() + self.bus2 = BusFactory() + self.driver1 = DriverFactory() + self.driver2 = DriverFactory() + + self.departure = timezone.now() + self.arrival = self.departure + timedelta(hours=2) + + def test_bus_shift_creation(self): + # Test that a BusShift can be created successfully + # Also check that arrival_time is after departure_time + # and that two bus stops are assigned by default + bus_shift = BusShiftFactory() + + self.assertIsNotNone(bus_shift.pk) + self.assertGreater(bus_shift.arrival_time, bus_shift.departure_time) + self.assertEqual(bus_shift.bus_stop.count(),2) + + def test_bus_shift_no_overlap_different_bus(self): + # Test that shifts with the same driver but different buses + # and non-overlapping times are allowed + + shift1 = BusShiftFactory(bus=self.bus1,driver=self.driver1,departure_time=self.departure,arrival_time=self.arrival) + # Shift2 is on a different bus, starting after shift1 ends + shift2 = BusShiftFactory(bus=self.bus2,driver=self.driver1,departure_time=self.arrival + timedelta(hours=1),arrival_time=self.arrival + timedelta(hours=4)) + + self.assertIsNotNone(shift1.pk) + self.assertIsNotNone(shift2.pk) + + def test_bus_shift_no_overlap_different_driver(self): + # Test that shifts with the same bus but different drivers + # and non-overlapping times are allowed + + shift1 = BusShiftFactory(bus=self.bus1,driver=self.driver1,departure_time=self.departure,arrival_time=self.arrival) + # Shift2 uses the same bus but a different driver, Starting after shift1 ends + shift2 = BusShiftFactory(bus=self.bus1,driver=self.driver2,departure_time=self.arrival + timedelta(hours=1),arrival_time=self.arrival + timedelta(hours=6)) + + self.assertIsNotNone(shift1.pk) + self.assertIsNotNone(shift2.pk) + + def test_bus_shift_has_at_least_two_stops(self): + # BusShift must have at least two BusStop + busshift = BusShiftFactory() + self.assertEqual(busshift.bus_stop.count(),2) \ No newline at end of file diff --git a/padam_django/apps/geography/admin.py b/padam_django/apps/geography/admin.py index e0334458..22cacab6 100644 --- a/padam_django/apps/geography/admin.py +++ b/padam_django/apps/geography/admin.py @@ -6,3 +6,7 @@ @admin.register(models.Place) class PlaceAdmin(admin.ModelAdmin): pass + +@admin.register(models.BusStop) +class BusStopAdmin(admin.ModelAdmin): + pass \ No newline at end of file diff --git a/padam_django/apps/geography/factories.py b/padam_django/apps/geography/factories.py index b134a30c..9d84cba8 100644 --- a/padam_django/apps/geography/factories.py +++ b/padam_django/apps/geography/factories.py @@ -15,3 +15,9 @@ class PlaceFactory(factory.django.DjangoModelFactory): class Meta: model = models.Place + +class BusStopFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.BusStop + + place = factory.SubFactory(PlaceFactory) \ No newline at end of file diff --git a/padam_django/apps/geography/migrations/0002_busstop.py b/padam_django/apps/geography/migrations/0002_busstop.py new file mode 100644 index 00000000..68dadbbe --- /dev/null +++ b/padam_django/apps/geography/migrations/0002_busstop.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.16 on 2025-11-25 10:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("geography", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="BusStop", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "place", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="bus_stops", + to="geography.place", + ), + ), + ], + options={ + "verbose_name_plural": "Buses stops", + }, + ), + ] diff --git a/padam_django/apps/geography/models.py b/padam_django/apps/geography/models.py index e566ee2b..58c80a1a 100644 --- a/padam_django/apps/geography/models.py +++ b/padam_django/apps/geography/models.py @@ -13,3 +13,12 @@ class Meta: def __str__(self): return f"Place: {self.name} (id: {self.pk})" + +class BusStop(models.Model): + place = models.ForeignKey(Place,on_delete=models.CASCADE, related_name="bus_stops") + + class Meta: + verbose_name_plural = "Buses stops" + + def __str__(self): + return f"BusStop: {self.place.name} (id: {self.pk})" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 863fd63d..c8aecd97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Django==4.2.16 django-extensions==3.2.1 Werkzeug==3.1.3 -ipython==8.29.0 +ipython==8.18.1 factory-boy==3.2.0 Faker==8.10.1 From 3d4e74e2405d447cba3709cc90aad4dbe0e0f931 Mon Sep 17 00:00:00 2001 From: aaarrouf Date: Wed, 26 Nov 2025 11:20:04 +0100 Subject: [PATCH 2/3] add Busshift form to admin page --- padam_django/apps/fleet/admin.py | 2 +- padam_django/apps/fleet/forms.py | 69 ++++++++++++++++--- .../apps/fleet/migrations/0003_busshift.py | 8 +-- padam_django/apps/fleet/models.py | 54 ++++----------- ...busstop_busstop_unique_place_stop_time.py} | 13 +++- padam_django/apps/geography/models.py | 19 +++-- 6 files changed, 102 insertions(+), 63 deletions(-) rename padam_django/apps/geography/migrations/{0002_busstop.py => 0002_busstop_busstop_unique_place_stop_time.py} (65%) diff --git a/padam_django/apps/fleet/admin.py b/padam_django/apps/fleet/admin.py index 01bac052..bb387e72 100644 --- a/padam_django/apps/fleet/admin.py +++ b/padam_django/apps/fleet/admin.py @@ -14,4 +14,4 @@ class DriverAdmin(admin.ModelAdmin): @admin.register(models.BusShift) class BusShiftAdmin(admin.ModelAdmin): - form = forms.BusShiftForm \ No newline at end of file + form = forms.BusShiftAdminForm \ No newline at end of file diff --git a/padam_django/apps/fleet/forms.py b/padam_django/apps/fleet/forms.py index d1acb71d..67311367 100644 --- a/padam_django/apps/fleet/forms.py +++ b/padam_django/apps/fleet/forms.py @@ -1,17 +1,68 @@ from django import forms +from django.core.validators import ValidationError from .models import BusShift -class BusShiftForm(forms.ModelForm): +class BusShiftAdminForm(forms.ModelForm): class Meta: model = BusShift fields = "__all__" - - def clean_bus_stop(self): - bus_stops = self.cleaned_data["bus_stop"] - - if len(bus_stops) < 2: - raise forms.ValidationError( + + def clean(self): + cleaned_data = super().clean() + bus = cleaned_data.get("bus") + driver = cleaned_data.get("driver") + bus_stops = cleaned_data.get("bus_stops") + + # Validate minimum number of stops + self._validate_bus_minimum_stops(bus_stops) + + # Get departure and arrival time of the bus shift + departure_at, arrival_at=self._get_bus_shift_first_and_last_stop_time(bus_stops) + + # Departure time must be before arrival + self._validate_shift_times(departure_at,arrival_at) + + # Validate no overlapping shifts + self._validate_no_overlapping_shift(bus, driver, departure_at, arrival_at) + + return cleaned_data + + def _validate_bus_minimum_stops(self,bus_stops): + if not bus_stops or bus_stops.count() < 2: + raise ValidationError( "A shift must have at least 2 bus stops." ) - - return bus_stops \ No newline at end of file + + def _get_bus_shift_first_and_last_stop_time(self,bus_stops): + departure_at = bus_stops.first().arrival_at + arrival_at = bus_stops.last().arrival_at + + if not departure_at or not arrival_at: + raise ValidationError("Invalid bus shift time") + + return departure_at, arrival_at + + def _validate_shift_times(self,departure_at,arrival_at): + if departure_at >= arrival_at : + raise ValidationError( + "Departure time must be before arrival time." + ) + + def _validate_no_overlapping_shift(self, bus, driver, departure_at, arrival_at): + self._check_overlapping_shifts(bus,departure_at,arrival_at,"bus") + self._check_overlapping_shifts(driver,departure_at,arrival_at,"driver") + + def _check_overlapping_shifts(self,entity,departure_at,arrival_at,entity_type): + overlapping_shifts = BusShift.objects.filter( + **{entity_type: entity} + ).exclude(pk=self.instance.pk) + + for shift in overlapping_shifts: + shift_arrival_time = shift.arrival_at + shift_departure_time = shift.departure_at + + if shift_arrival_time and shift_departure_time: + if shift_departure_time < arrival_at and shift_arrival_time > departure_at : + raise ValidationError( + f"This {entity_type} already has a shift in this time interval." + ) \ No newline at end of file diff --git a/padam_django/apps/fleet/migrations/0003_busshift.py b/padam_django/apps/fleet/migrations/0003_busshift.py index ac26863e..62d7ce64 100644 --- a/padam_django/apps/fleet/migrations/0003_busshift.py +++ b/padam_django/apps/fleet/migrations/0003_busshift.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2025-11-25 10:37 +# Generated by Django 4.2.16 on 2025-11-26 09:04 from django.db import migrations, models import django.db.models.deletion @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("geography", "0002_busstop"), + ("geography", "0002_busstop_busstop_unique_place_stop_time"), ("fleet", "0002_auto_20211109_1456"), ] @@ -23,8 +23,6 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("departure_time", models.DateTimeField()), - ("arrival_time", models.DateTimeField()), ( "bus", models.ForeignKey( @@ -33,7 +31,7 @@ class Migration(migrations.Migration): to="fleet.bus", ), ), - ("bus_stop", models.ManyToManyField(to="geography.busstop")), + ("bus_stops", models.ManyToManyField(to="geography.busstop")), ( "driver", models.ForeignKey( diff --git a/padam_django/apps/fleet/models.py b/padam_django/apps/fleet/models.py index 4d1c27b3..61615d71 100644 --- a/padam_django/apps/fleet/models.py +++ b/padam_django/apps/fleet/models.py @@ -21,49 +21,21 @@ def __str__(self): class BusShift(models.Model): bus = models.ForeignKey(Bus,on_delete=models.CASCADE,related_name="shifts") driver = models.ForeignKey(Driver,on_delete=models.CASCADE,related_name="shifts") - bus_stop = models.ManyToManyField("geography.BusStop") - departure_time = models.DateTimeField() - arrival_time = models.DateTimeField() + bus_stops = models.ManyToManyField("geography.BusStop") class Meta: verbose_name_plural = "Buses shifts" - def __str__(self): - return f"BusShift: {self.bus.licence_plate}, Driver: {self.driver.user.username} (id: {self.pk})" - def clean(self): - """Validate the rules before saving """ - - errors = {} - - # Departure time must be before arrival - if self.departure_time >= self.arrival_time: - errors['departure_time'] = "Departure time must be before arrival time." - - # Check overlapping for bus shifts - bus_overlap = BusShift.objects.filter( - bus=self.bus, - departure_time__lt=self.arrival_time, - arrival_time__gt=self.departure_time - ).exclude(pk=self.pk) - - # Check overlapping for driver shifts - driver_overlap = BusShift.objects.filter( - driver=self.driver, - departure_time__lt=self.arrival_time, - arrival_time__gt=self.departure_time - ).exclude(pk=self.pk) - - - if bus_overlap.exists(): - errors["bus"] = "This bus already has a shift in this time interval." - - if driver_overlap.exists(): - errors["driver"] = "This driver already has a shift in this time interval." - - if errors: - raise ValidationError(errors) - - def save(self, *args, **kwargs): - self.full_clean() - super().save(*args, **kwargs) \ No newline at end of file + @property + def departure_at(self): + first_stop = self.bus_stops.first() + return first_stop.arrival_at if first_stop else None + + @property + def arrival_at(self): + last_stop = self.bus_stops.last() + return last_stop.arrival_at if last_stop else None + + def __str__(self): + return f"BusShift: {self.bus.licence_plate}, Driver: {self.driver.user.username} (id: {self.pk})" \ No newline at end of file diff --git a/padam_django/apps/geography/migrations/0002_busstop.py b/padam_django/apps/geography/migrations/0002_busstop_busstop_unique_place_stop_time.py similarity index 65% rename from padam_django/apps/geography/migrations/0002_busstop.py rename to padam_django/apps/geography/migrations/0002_busstop_busstop_unique_place_stop_time.py index 68dadbbe..b2043bad 100644 --- a/padam_django/apps/geography/migrations/0002_busstop.py +++ b/padam_django/apps/geography/migrations/0002_busstop_busstop_unique_place_stop_time.py @@ -1,7 +1,8 @@ -# Generated by Django 4.2.16 on 2025-11-25 10:37 +# Generated by Django 4.2.16 on 2025-11-26 09:04 from django.db import migrations, models import django.db.models.deletion +import django.utils.timezone class Migration(migrations.Migration): @@ -22,6 +23,8 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("name", models.CharField(max_length=100)), + ("arrival_at", models.DateTimeField(default=django.utils.timezone.now)), ( "place", models.ForeignKey( @@ -32,7 +35,13 @@ class Migration(migrations.Migration): ), ], options={ - "verbose_name_plural": "Buses stops", + "ordering": ["arrival_at"], }, ), + migrations.AddConstraint( + model_name="busstop", + constraint=models.UniqueConstraint( + fields=("name", "place", "arrival_at"), name="unique_place_stop_time" + ), + ), ] diff --git a/padam_django/apps/geography/models.py b/padam_django/apps/geography/models.py index 58c80a1a..2f9ea04d 100644 --- a/padam_django/apps/geography/models.py +++ b/padam_django/apps/geography/models.py @@ -1,5 +1,5 @@ from django.db import models - +from django.utils import timezone class Place(models.Model): name = models.CharField("Name of the place", max_length=50) @@ -14,11 +14,20 @@ class Meta: def __str__(self): return f"Place: {self.name} (id: {self.pk})" -class BusStop(models.Model): +class BusStop(models.Model): + name = models.CharField(max_length=100) place = models.ForeignKey(Place,on_delete=models.CASCADE, related_name="bus_stops") + arrival_at = models.DateTimeField(default=timezone.now) - class Meta: - verbose_name_plural = "Buses stops" + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["name", "place", "arrival_at"], + name="unique_place_stop_time", + ) + ] + ordering = ["arrival_at"] + def __str__(self): - return f"BusStop: {self.place.name} (id: {self.pk})" \ No newline at end of file + return f"BusStop: {self.place.name} at {self.arrival_at}" \ No newline at end of file From 89f2036862ae9943963c33633339fc2e0878afbc Mon Sep 17 00:00:00 2001 From: aaarrouf Date: Wed, 26 Nov 2025 12:32:21 +0100 Subject: [PATCH 3/3] add tests --- padam_django/apps/fleet/factories.py | 12 ++- padam_django/apps/fleet/tests.py | 101 ++++++++++++++++++----- padam_django/apps/geography/factories.py | 9 +- 3 files changed, 92 insertions(+), 30 deletions(-) diff --git a/padam_django/apps/fleet/factories.py b/padam_django/apps/fleet/factories.py index b5634514..15223a38 100644 --- a/padam_django/apps/fleet/factories.py +++ b/padam_django/apps/fleet/factories.py @@ -30,17 +30,15 @@ class Meta: bus = factory.SubFactory(BusFactory) driver = factory.SubFactory(DriverFactory) - departure_time = factory.LazyFunction(lambda: timezone.now()) - arrival_time = factory.LazyAttribute( - lambda obj: obj.departure_time + timedelta(hours=2) - ) @factory.post_generation - def bus_stop(self,create,extracted,**kwargs): + def bus_stops(self,create,extracted,**kwargs): if not create: return if extracted: for stop in extracted: - self.bus_stop.add(stop) + self.bus_stops.add(stop) else: - self.bus_stop.add(BusStopFactory(),BusStopFactory()) \ No newline at end of file + bus_stop1 = BusStopFactory(arrival_at=timezone.now()) + bus_stop2 = BusStopFactory(arrival_at=timezone.now() + timedelta(minutes=30)) + self.bus_stops.add(bus_stop1,bus_stop2) \ No newline at end of file diff --git a/padam_django/apps/fleet/tests.py b/padam_django/apps/fleet/tests.py index e78e6e8d..1dbc086d 100644 --- a/padam_django/apps/fleet/tests.py +++ b/padam_django/apps/fleet/tests.py @@ -1,9 +1,10 @@ from datetime import timedelta from django.utils import timezone - from django.test import TestCase -from .factories import DriverFactory,BusFactory,BusShiftFactory +from ..geography.factories import BusStopFactory +from .factories import DriverFactory,BusFactory,BusShiftFactory +from .forms import BusShiftAdminForm class BusShiftTestCase(TestCase): """ @@ -22,42 +23,100 @@ def setUp(self): self.driver1 = DriverFactory() self.driver2 = DriverFactory() - self.departure = timezone.now() - self.arrival = self.departure + timedelta(hours=2) + self.base_time = timezone.now() def test_bus_shift_creation(self): - # Test that a BusShift can be created successfully - # Also check that arrival_time is after departure_time - # and that two bus stops are assigned by default + """ + Test that a BusShift can be created successfully, + with at least two bus stops and correct departure/arrival. + """ bus_shift = BusShiftFactory() self.assertIsNotNone(bus_shift.pk) - self.assertGreater(bus_shift.arrival_time, bus_shift.departure_time) - self.assertEqual(bus_shift.bus_stop.count(),2) + self.assertGreater(bus_shift.arrival_at, bus_shift.departure_at) + self.assertEqual(bus_shift.bus_stops.count(),2) def test_bus_shift_no_overlap_different_bus(self): - # Test that shifts with the same driver but different buses - # and non-overlapping times are allowed + """ + Test non-overlapping shifts with same driver but different buses. + Also test that overlapping shifts raise ValidationError. + """ + + # Shift 1 + stop1 = BusStopFactory(arrival_at=self.base_time) + stop2 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=2)) + shift1 = BusShiftFactory( + bus=self.bus1, driver=self.driver1, bus_stops=[stop1, stop2] + ) - shift1 = BusShiftFactory(bus=self.bus1,driver=self.driver1,departure_time=self.departure,arrival_time=self.arrival) - # Shift2 is on a different bus, starting after shift1 ends - shift2 = BusShiftFactory(bus=self.bus2,driver=self.driver1,departure_time=self.arrival + timedelta(hours=1),arrival_time=self.arrival + timedelta(hours=4)) + # Shift 2 non-overlapping + stop3 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=3)) + stop4 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=6)) + shift2 = BusShiftFactory( + bus=self.bus2, driver=self.driver1, bus_stops=[stop3, stop4] + ) self.assertIsNotNone(shift1.pk) self.assertIsNotNone(shift2.pk) + # Shift 3 overlapping with shift 1 + stop5 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=1)) + stop6 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=3)) + + form_data = { + 'bus': self.bus2, + 'driver': self.driver1, + 'bus_stops': [stop5, stop6], + } + + form = BusShiftAdminForm(data=form_data) + self.assertFalse(form.is_valid()) + def test_bus_shift_no_overlap_different_driver(self): - # Test that shifts with the same bus but different drivers - # and non-overlapping times are allowed + """ + Shifts with the same bus but different drivers + and non-overlapping times are allowed. + """ + # Shift 1 + stop1 = BusStopFactory(arrival_at=self.base_time) + stop2 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=2)) + shift1 = BusShiftFactory( + bus=self.bus1, driver=self.driver1, bus_stops=[stop1, stop2] + ) - shift1 = BusShiftFactory(bus=self.bus1,driver=self.driver1,departure_time=self.departure,arrival_time=self.arrival) - # Shift2 uses the same bus but a different driver, Starting after shift1 ends - shift2 = BusShiftFactory(bus=self.bus1,driver=self.driver2,departure_time=self.arrival + timedelta(hours=1),arrival_time=self.arrival + timedelta(hours=6)) + # Shift 2 non-overlapping + stop3 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=3)) + stop4 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=6)) + shift2 = BusShiftFactory( + bus=self.bus1, driver=self.driver2, bus_stops=[stop3, stop4] + ) self.assertIsNotNone(shift1.pk) self.assertIsNotNone(shift2.pk) + + # Shift 3 overlapping with shift 1 + stop5 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=1)) + stop6 = BusStopFactory(arrival_at=self.base_time + timedelta(hours=3)) + + form_data = { + 'bus': self.bus1, + 'driver': self.driver2, + 'bus_stops': [stop5, stop6], + } + + form = BusShiftAdminForm(data=form_data) + self.assertFalse(form.is_valid()) def test_bus_shift_has_at_least_two_stops(self): - # BusShift must have at least two BusStop + """ + BusShift must have at least two stops + """ busshift = BusShiftFactory() - self.assertEqual(busshift.bus_stop.count(),2) \ No newline at end of file + self.assertEqual(busshift.bus_stops.count(),2) + + def test_bus_shift_departure_before_arrival(self): + """ + Departure must be before arrival + """ + busshift = BusShiftFactory() + self.assertLess(busshift.departure_at, busshift.arrival_at) \ No newline at end of file diff --git a/padam_django/apps/geography/factories.py b/padam_django/apps/geography/factories.py index 9d84cba8..9eda0624 100644 --- a/padam_django/apps/geography/factories.py +++ b/padam_django/apps/geography/factories.py @@ -1,6 +1,7 @@ +from django.utils import timezone import factory from faker import Faker - +from datetime import timedelta from . import models @@ -17,7 +18,11 @@ class Meta: model = models.Place class BusStopFactory(factory.django.DjangoModelFactory): + name = factory.LazyFunction(fake.street_name) + place = factory.SubFactory(PlaceFactory) + arrival_at= factory.LazyFunction(lambda: timezone.now() + timedelta(minutes=fake.random_int(min=1,max=300))) + class Meta: model = models.BusStop - place = factory.SubFactory(PlaceFactory) \ No newline at end of file + \ No newline at end of file