From d233d522e7865a92b531b8f5018f25b199de3489 Mon Sep 17 00:00:00 2001 From: Aymen Tilfani Date: Tue, 12 Nov 2024 07:39:02 +0100 Subject: [PATCH] My try for django-test --- Makefile | 10 ++ .../common/management/commands/create_data.py | 1 + padam_django/apps/fleet/admin.py | 6 + padam_django/apps/fleet/factories.py | 11 ++ .../migrations/0003_auto_20241112_0427.py | 45 +++++++ .../migrations/0004_auto_20241112_0612.py | 25 ++++ padam_django/apps/fleet/models.py | 71 +++++++++++ padam_django/apps/fleet/test_bus_shifts.py | 110 ++++++++++++++++++ padam_django/apps/geography/admin.py | 4 + padam_django/apps/geography/factories.py | 6 + .../management/commands/create_bus_stops.py | 11 ++ .../apps/geography/migrations/0002_busstop.py | 21 ++++ padam_django/apps/geography/models.py | 6 + 13 files changed, 327 insertions(+) create mode 100644 padam_django/apps/fleet/migrations/0003_auto_20241112_0427.py create mode 100644 padam_django/apps/fleet/migrations/0004_auto_20241112_0612.py create mode 100644 padam_django/apps/fleet/test_bus_shifts.py create mode 100644 padam_django/apps/geography/management/commands/create_bus_stops.py create mode 100644 padam_django/apps/geography/migrations/0002_busstop.py diff --git a/Makefile b/Makefile index 4062f4c4..d5f64883 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,13 @@ run: ## Run the test server. install: ## Install the python requirements. pip install -r requirements.txt + +migrate: ## Generate and apply migrations. + python manage.py makemigrations + python manage.py migrate + +superuser: ## Create a superuser. + python manage.py createsuperuser + +test: ## Run the tests. + python manage.py test \ 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..94b70dfa 100644 --- a/padam_django/apps/common/management/commands/create_data.py +++ b/padam_django/apps/common/management/commands/create_data.py @@ -12,3 +12,4 @@ def handle(self, *args, **options): 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=50) diff --git a/padam_django/apps/fleet/admin.py b/padam_django/apps/fleet/admin.py index 3fba5023..dcc660cf 100644 --- a/padam_django/apps/fleet/admin.py +++ b/padam_django/apps/fleet/admin.py @@ -11,3 +11,9 @@ class BusAdmin(admin.ModelAdmin): @admin.register(models.Driver) class DriverAdmin(admin.ModelAdmin): pass + + +@admin.register(models.BusShift) +class BusShiftAdmin(admin.ModelAdmin): + form = models.BusShiftForm + pass diff --git a/padam_django/apps/fleet/factories.py b/padam_django/apps/fleet/factories.py index c78c832e..a070c660 100644 --- a/padam_django/apps/fleet/factories.py +++ b/padam_django/apps/fleet/factories.py @@ -19,3 +19,14 @@ class BusFactory(factory.django.DjangoModelFactory): class Meta: model = models.Bus + + +class BusShiftFactory(factory.django.DjangoModelFactory): + bus = factory.SubFactory(BusFactory) + driver = factory.SubFactory(DriverFactory) + start = factory.LazyFunction(fake.date_time_this_month) + end = factory.LazyFunction(fake.date_time_this_month) + bus_stop_ids = factory.LazyFunction(lambda: [fake.random_int(min=1, max=100) for _ in range(2)]) + + class Meta: + model = models.BusShift \ No newline at end of file diff --git a/padam_django/apps/fleet/migrations/0003_auto_20241112_0427.py b/padam_django/apps/fleet/migrations/0003_auto_20241112_0427.py new file mode 100644 index 00000000..538dda18 --- /dev/null +++ b/padam_django/apps/fleet/migrations/0003_auto_20241112_0427.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.5 on 2024-11-12 04:27 + +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')), + ('start', models.DateTimeField(verbose_name='Start of the shift')), + ('end', models.DateTimeField(verbose_name='End of the shift')), + ('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')), + ], + options={ + 'ordering': ['start'], + }, + ), + migrations.CreateModel( + name='BusShiftStop', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(verbose_name='Order of the stop')), + ('bus_shift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.busshift')), + ('bus_stop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geography.busstop')), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.AddField( + model_name='busshift', + name='stops', + field=models.ManyToManyField(related_name='shifts', through='fleet.BusShiftStop', to='geography.BusStop'), + ), + ] diff --git a/padam_django/apps/fleet/migrations/0004_auto_20241112_0612.py b/padam_django/apps/fleet/migrations/0004_auto_20241112_0612.py new file mode 100644 index 00000000..fe8a3e6d --- /dev/null +++ b/padam_django/apps/fleet/migrations/0004_auto_20241112_0612.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.5 on 2024-11-12 06:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fleet', '0003_auto_20241112_0427'), + ] + + operations = [ + migrations.RemoveField( + model_name='busshift', + name='stops', + ), + migrations.AddField( + model_name='busshift', + name='bus_stop_ids', + field=models.TextField(blank=True, default='[]'), + ), + migrations.DeleteModel( + name='BusShiftStop', + ), + ] diff --git a/padam_django/apps/fleet/models.py b/padam_django/apps/fleet/models.py index 4cd3f19d..ed1e7eea 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 django.core.exceptions import ValidationError +from django import forms +from padam_django.apps.geography.models import BusStop class Driver(models.Model): @@ -16,3 +19,71 @@ 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') + start = models.DateTimeField("Start of the shift") + end = models.DateTimeField("End of the shift") + bus_stop_ids = models.TextField(blank=True, default='[]') + + class Meta: + ordering = ['start'] + + def __str__(self): + return f"BusShift: {self.bus.licence_plate} (Driver: {self.driver.user.username}, Start: {self.start}, End: {self.end})" + + +class BusShiftForm(forms.ModelForm): + bus_stop_ids = forms.CharField(widget=forms.Textarea, help_text="Enter bus stop IDs separated by commas") + + class Meta: + model = BusShift + fields = ['bus', 'driver', 'start', 'end', 'bus_stop_ids'] + + def clean_bus_stop_ids(self): + bus_stop_ids = self.cleaned_data['bus_stop_ids'] + bus_stop_ids = [int(id.strip()) for id in bus_stop_ids.split(',') if id.strip().isdigit()] + + if len(bus_stop_ids) < 2: + raise ValidationError("There must be at least two stops.") + + # Ensure all bus stop IDs are valid + if not BusStop.objects.filter(id__in=bus_stop_ids).count() == len(bus_stop_ids): + raise ValidationError("One or more bus stop IDs are invalid.") + + return bus_stop_ids + + def clean(self): + cleaned_data = super().clean() + bus = cleaned_data.get('bus') + driver = cleaned_data.get('driver') + start = cleaned_data.get('start') + end = cleaned_data.get('end') + + # Ensure the end time is after the start time + if start and end and start >= end: + raise ValidationError("The end time must be after the start time.") + + # Ensure no overlapping shifts for the same bus + if bus and start and end: + overlapping_bus_shifts = BusShift.objects.filter( + bus=bus, + start__lt=end, + end__gt=start + ).exclude(pk=self.instance.pk) + if overlapping_bus_shifts.exists(): + raise ValidationError("The bus has overlapping shifts.") + + # Ensure no overlapping shifts for the same driver + if driver and start and end: + overlapping_driver_shifts = BusShift.objects.filter( + driver=driver, + start__lt=end, + end__gt=start + ).exclude(pk=self.instance.pk) + if overlapping_driver_shifts.exists(): + raise ValidationError("The driver has overlapping shifts.") + + return cleaned_data \ No newline at end of file diff --git a/padam_django/apps/fleet/test_bus_shifts.py b/padam_django/apps/fleet/test_bus_shifts.py new file mode 100644 index 00000000..b3859fc8 --- /dev/null +++ b/padam_django/apps/fleet/test_bus_shifts.py @@ -0,0 +1,110 @@ +from django.test import TestCase +from padam_django.apps.fleet.models import BusShiftForm +from padam_django.apps.fleet.factories import BusFactory, DriverFactory +from padam_django.apps.geography.factories import BusStopFactory + +class BusShiftFormTest(TestCase): + + def setUp(self): + self.bus = BusFactory() + self.second_bus = BusFactory() + self.driver = DriverFactory() + self.stop1 = BusStopFactory() + self.stop2 = BusStopFactory() + + def test_bus_shift_form_with_valid_data(self): + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T08:00:00Z', + 'end': '2023-10-01T10:00:00Z', + 'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}' + } + form = BusShiftForm(data=form_data) + self.assertTrue(form.is_valid()) + + def test_bus_shift_form_with_less_than_two_stops(self): + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T08:00:00Z', + 'end': '2023-10-01T10:00:00Z', + 'bus_stop_ids': f'{self.stop1.id}' + } + form = BusShiftForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('There must be at least two stops.', form.errors['bus_stop_ids']) + + def test_bus_shift_form_with_invalid_stop_ids(self): + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T08:00:00Z', + 'end': '2023-10-01T10:00:00Z', + 'bus_stop_ids': '999,1000' + } + form = BusShiftForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('One or more bus stop IDs are invalid.', form.errors['bus_stop_ids']) + + def test_bus_shift_form_with_end_time_before_start_time(self): + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T10:00:00Z', + 'end': '2023-10-01T08:00:00Z', + 'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}' + } + form = BusShiftForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('The end time must be after the start time.', form.errors['__all__']) + + def test_bus_shift_form_with_overlapping_shifts_for_bus(self): + # Create an initial BusShift to overlap with + initial_form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T08:00:00Z', + 'end': '2023-10-01T10:00:00Z', + 'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}' + } + initial_form = BusShiftForm(data=initial_form_data) + if initial_form.is_valid(): + initial_form.save() + + # Create a new BusShift that overlaps with the initial one + form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T09:00:00Z', + 'end': '2023-10-01T11:00:00Z', + 'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}' + } + form = BusShiftForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('The bus has overlapping shifts.', form.errors['__all__']) + + def test_bus_shift_form_with_overlapping_shifts_for_driver(self): + # Create an initial BusShift to overlap with + initial_form_data = { + 'bus': self.bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T08:00:00Z', + 'end': '2023-10-01T10:00:00Z', + 'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}' + } + initial_form = BusShiftForm(data=initial_form_data) + if initial_form.is_valid(): + initial_form.save() + + # Create a new BusShift that overlaps with the initial one + form_data = { + 'bus': self.second_bus.id, + 'driver': self.driver.id, + 'start': '2023-10-01T09:00:00Z', + 'end': '2023-10-01T11:00:00Z', + 'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}' + } + form = BusShiftForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertIn('The driver has overlapping shifts.', form.errors['__all__']) \ 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..3dd82f54 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): + place = factory.SubFactory(PlaceFactory) + + class Meta: + model = models.BusStop diff --git a/padam_django/apps/geography/management/commands/create_bus_stops.py b/padam_django/apps/geography/management/commands/create_bus_stops.py new file mode 100644 index 00000000..867a1767 --- /dev/null +++ b/padam_django/apps/geography/management/commands/create_bus_stops.py @@ -0,0 +1,11 @@ +from padam_django.apps.common.management.base import CreateDataBaseCommand +from padam_django.apps.geography.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/geography/migrations/0002_busstop.py b/padam_django/apps/geography/migrations/0002_busstop.py new file mode 100644 index 00000000..a24a25af --- /dev/null +++ b/padam_django/apps/geography/migrations/0002_busstop.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.5 on 2024-11-12 04:27 + +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')), + ], + ), + ] diff --git a/padam_django/apps/geography/models.py b/padam_django/apps/geography/models.py index e566ee2b..c425d201 100644 --- a/padam_django/apps/geography/models.py +++ b/padam_django/apps/geography/models.py @@ -13,3 +13,9 @@ 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') + + def __str__(self): + return f"BusStop: {self.place.name}"