From 9ed948c9a38a752acf2a29be1be1c5165747df75 Mon Sep 17 00:00:00 2001 From: Florent Belotti Date: Mon, 29 Dec 2025 17:23:55 +0100 Subject: [PATCH 1/3] Add Makefile migration target --- .python-version | 1 + Makefile | 3 +++ requirements.txt | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..8e34c813 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.19 diff --git a/Makefile b/Makefile index 4062f4c4..8a396c29 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,6 @@ run: ## Run the test server. install: ## Install the python requirements. pip install -r requirements.txt + +migrate: ## Migrate Django models + python manage.py migrate diff --git a/requirements.txt b/requirements.txt index 863fd63d..b86b8864 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.29.0 factory-boy==3.2.0 Faker==8.10.1 From eb72547a890589e9c30375fd132619ca023e3dfa Mon Sep 17 00:00:00 2001 From: Florent Belotti Date: Mon, 29 Dec 2025 17:38:35 +0100 Subject: [PATCH 2/3] Add Makefile superuser creation target --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 8a396c29..fa2a6955 100644 --- a/Makefile +++ b/Makefile @@ -6,3 +6,6 @@ install: ## Install the python requirements. migrate: ## Migrate Django models python manage.py migrate + +user: ## Create a super-user to access Django-admin + python manage.py createsuperuser From 518ff2d8927b526ad082f7ff031952411849dd5d Mon Sep 17 00:00:00 2001 From: Florent Belotti Date: Tue, 6 Jan 2026 15:42:08 +0100 Subject: [PATCH 3/3] concept answer ok --- .DS_Store | Bin 0 -> 6148 bytes Makefile | 10 +++ README.md | 42 ++++++++---- padam_django/apps/fleet/admin.py | 18 +++++ padam_django/apps/fleet/factories.py | 24 +++++++ .../management/commands/create_bus_shifts.py | 13 ++++ .../management/commands/create_bus_stops.py | 13 ++++ .../fleet/migrations/0003_busshift_busstop.py | 38 +++++++++++ padam_django/apps/fleet/models.py | 64 ++++++++++++++++++ 9 files changed, 210 insertions(+), 12 deletions(-) create mode 100644 .DS_Store create mode 100644 padam_django/apps/fleet/management/commands/create_bus_shifts.py create mode 100644 padam_django/apps/fleet/management/commands/create_bus_stops.py create mode 100644 padam_django/apps/fleet/migrations/0003_busshift_busstop.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..cf14c411e42214463edf5c7cd25c494a474c44e5 GIT binary patch literal 6148 zcmeHKy-veG4EE6=MZi!oF=m1Zq_VMvDlDvh0NN5%ikhZFsl*l*1{M}p-hh>X0oHE3 z01F!u5)vaL_Suv>^+!wyRkq~&E@wM-z9{D$0GR4&S^+2nK!{2hEa5Og$WA&Y3$jAN zhVId6RceQM)*JKG1!X`P_>T-V>-`@-gS;#EUgV6xwfhIb}RjjzgKraW#c;;AZJ||xOJ9xoHUTL%vI z2!P0qiEZ>tK{1hFELbq21;ubEB8M6|ViueIXQ0HFW$OQX)6f5YkX|VR%D`MPV1lHURPm9dw~jp=_1Xxvj!Hs)1)~kY jLE5pMP%B=2) +- OK 1 BusStop === 1 Place +- OK 1 BusStop === 1 passage_time +- OK First passage_time of BusShift === passage_time of first BusStop +- OK Last passage_time of BusShift === passage_time of last BusStop +- OK Calculate total duration of BusShift from first and last stop +- OK 1 Bus === 0 or 1 Busshift at the same time +- OK 1 Driver === 0 or 1 Busshift at the same time +- OK Creation / modification in django admin + # Django technical test / Backend (English version first, French version below) -The objective of the exercise below is to model a database based on business specifications and to +The objective of the exercise below is to model a database based on business specifications and to design a simple interface for managing bus routes, using Django admin. To carry out the test, remember to fork this repository. Ideally, open a PR at the end. @@ -18,7 +36,7 @@ To carry out the test, remember to fork this repository. Ideally, open a PR at t | Python | 3.9 | | Django | 4.2.16 | -- This project was created using Python 3.7. You are free to use another version, but this is the one we recommend. +- This project was created using Python 3.7. You are free to use another version, but this is the one we recommend. recommended. - The database is freely selectable. The project is configured to use `sqlite` by default. @@ -32,7 +50,7 @@ make migrate make run ``` -Scripts are available to help you quickly create data and take control of the project. +Scripts are available to help you quickly create data and take control of the project. in hand: - `create_data` @@ -70,10 +88,10 @@ The proposed project structure already includes the following models: #### Implement the `BusShift` and `BusStop` models in the existing code base -The implementation of these two models is up to you. The following business constraints +The implementation of these two models is up to you. The following business constraints must be respected: -- The same bus cannot be assigned to several routes at the same time, with overlapping start and end times. +- The same bus cannot be assigned to several routes at the same time, with overlapping start and end times. overlap. - The same applies to drivers. @@ -83,7 +101,7 @@ It must be possible for a user to create or modify bus routes (`BusShift`) using interface. **Note**: There are several ways of designing this functionality. Some may be more time-consuming -more time-consuming than others... +more time-consuming than others... @@ -102,7 +120,7 @@ more time-consuming than others... # Test technique Django / Backend -L'objectif de l'exercice ci-dessous est de modéliser une base de données à partir de spécifications métiers et de +L'objectif de l'exercice ci-dessous est de modéliser une base de données à partir de spécifications métiers et de concevoir une interface simple de gestion de trajets de bus, en utilisant l'admin de Django. Pour réaliser le test, pensez à fork ce repository. Idéalement, ouvrir une PR à la fin. @@ -120,7 +138,7 @@ Pour réaliser le test, pensez à fork ce repository. Idéalement, ouvrir une PR | Python | 3.9 | | Django | 4.2.16 | - - Le projet à été réalisé en utilisant Python 3.7. Vous êtes libre d'utiliser une autre version mais c'est celle que + - Le projet à été réalisé en utilisant Python 3.7. Vous êtes libre d'utiliser une autre version mais c'est celle que nous vous conseillons. - La base de donnée est au choix. Le projet est configuré pour utiliser `sqlite` par défaut. @@ -134,7 +152,7 @@ make migrate make run ``` -Des scripts sont à votre disposition pour vous permettre de rapidement créer de la donnée et de prendre le projet en +Des scripts sont à votre disposition pour vous permettre de rapidement créer de la donnée et de prendre le projet en main: - `create_data` @@ -172,10 +190,10 @@ La structure de projet qui vous est proposée comprends déjà les models suivan #### Implémenter les modèles `BusShift` and `BusStop` à la base de code existante -L'implémentation de ces deux modèles est libre et laissée à votre appréciation. Les contraintes métiers suivantes +L'implémentation de ces deux modèles est libre et laissée à votre appréciation. Les contraintes métiers suivantes doivent être respectées: - - Un même bus ne peut être assigné, en même temps, à plusieurs trajets dont les heures de début et fin se + - Un même bus ne peut être assigné, en même temps, à plusieurs trajets dont les heures de début et fin se chevaucheraient. - Il en va de même pour les chauffeurs. @@ -185,7 +203,7 @@ Il doit être possible, pour un utilisateur, de créer ou de modifier des trajet de django. **Note**: Il existe plusieurs solutions pour concevoir cette fonctionnalité. Certaines seront peut être plus couteuse -en temps que d'autres ... +en temps que d'autres ... ### Conseils diff --git a/padam_django/apps/fleet/admin.py b/padam_django/apps/fleet/admin.py index 3fba5023..59da263f 100644 --- a/padam_django/apps/fleet/admin.py +++ b/padam_django/apps/fleet/admin.py @@ -11,3 +11,21 @@ class BusAdmin(admin.ModelAdmin): @admin.register(models.Driver) class DriverAdmin(admin.ModelAdmin): pass + + +class BusStopInline(admin.TabularInline): + model = models.BusStop + + # One empty default form + extra = 1 + fields = ['order', 'place', 'stop_time'] + + +@admin.register(models.BusShift) +class BusShiftAdmin(admin.ModelAdmin): + + # Admin view + list_display = ['id', 'bus', 'driver', 'start_time', 'end_time'] + list_filter = ['bus', 'driver'] + + inlines = [BusStopInline] diff --git a/padam_django/apps/fleet/factories.py b/padam_django/apps/fleet/factories.py index c78c832e..dbb6d834 100644 --- a/padam_django/apps/fleet/factories.py +++ b/padam_django/apps/fleet/factories.py @@ -1,4 +1,6 @@ import factory +from datetime import timedelta +from django.utils import timezone from faker import Faker from . import models @@ -19,3 +21,25 @@ class BusFactory(factory.django.DjangoModelFactory): class Meta: model = models.Bus + + +class BusShiftFactory(factory.django.DjangoModelFactory): + bus = factory.SubFactory(BusFactory) + driver = factory.SubFactory(DriverFactory) + start_time = factory.LazyFunction(lambda: timezone.make_aware(fake.date_time_this_month())) + end_time = factory.LazyAttribute(lambda obj: obj.start_time + timedelta(hours=fake.random_int(min=2, max=8))) + + class Meta: + model = models.BusShift + + +class BusStopFactory(factory.django.DjangoModelFactory): + bus_shift = factory.SubFactory(BusShiftFactory) + place = factory.SubFactory('padam_django.apps.geography.factories.PlaceFactory') + order = factory.Sequence(lambda n: n) + stop_time = factory.LazyAttribute( + lambda obj: obj.bus_shift.start_time + timedelta(minutes=fake.random_int(min=10, max=60)) + ) + + class Meta: + model = models.BusStop diff --git a/padam_django/apps/fleet/management/commands/create_bus_shifts.py b/padam_django/apps/fleet/management/commands/create_bus_shifts.py new file mode 100644 index 00000000..c228fa1a --- /dev/null +++ b/padam_django/apps/fleet/management/commands/create_bus_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 bus shifts' + + def handle(self, *args, **options): + super().handle(*args, **options) + self.stdout.write(f'Creating {self.number} bus shifts ...') + BusShiftFactory.create_batch(size=self.number) 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..fdd689d8 --- /dev/null +++ b/padam_django/apps/fleet/management/commands/create_bus_stops.py @@ -0,0 +1,13 @@ +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_busshift_busstop.py b/padam_django/apps/fleet/migrations/0003_busshift_busstop.py new file mode 100644 index 00000000..ed38e940 --- /dev/null +++ b/padam_django/apps/fleet/migrations/0003_busshift_busstop.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.16 on 2026-01-05 21:52 + +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')), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ('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')), + ], + ), + migrations.CreateModel( + name='BusStop', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField()), + ('stop_time', models.DateTimeField()), + ('bus_shift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stops', to='fleet.busshift')), + ('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geography.place')), + ], + options={ + 'ordering': ['order'], + }, + ), + ] diff --git a/padam_django/apps/fleet/models.py b/padam_django/apps/fleet/models.py index 4cd3f19d..7bbfdd67 100644 --- a/padam_django/apps/fleet/models.py +++ b/padam_django/apps/fleet/models.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.db import models @@ -16,3 +17,66 @@ class Meta: def __str__(self): return f"Bus: {self.licence_plate} (id: {self.pk})" + + +class BusShift(models.Model): + + '''A shift during which a bus is driven by a driver. Joining the business side (driver, bus...) + and the technical/geographic (place, busStop...) aspect of the data.''' + + # Relations Many-to-one: 1 shifts === 1 bus, but 1 bus === N shifts + bus = models.ForeignKey(Bus, on_delete=models.CASCADE, related_name='shifts') + driver = models.ForeignKey(Driver, on_delete=models.CASCADE, related_name='shifts') + + start_time = models.DateTimeField() + end_time = models.DateTimeField() + + def __str__(self): + return f"BusShift: {self.bus} {self.driver} {self.start_time} {self.end_time} (id: {self.pk})" + + def clean(self): + + # Validation: time consistency + if self.start_time and self.end_time and self.start_time >= self.end_time: + raise ValidationError("End time must be after start time.") + + # No overlapping bus shifts for the same bus or the same time. + if self.bus_id and self.start_time and self.end_time: + overlapping_bus_shifts = BusShift.objects.filter( + bus=self.bus, + start_time__lt=self.end_time, + end_time__gt=self.start_time + ).exclude(pk=self.pk) + if overlapping_bus_shifts.exists(): + raise ValidationError(f"Bus [{self.bus}] is already assigned to a shift.") + + # No overlapping driver shifts for the same driver or the same time. + if self.driver_id and self.start_time and self.end_time: + overlapping_driver_shifts = BusShift.objects.filter( + driver=self.driver, + start_time__lt=self.end_time, + end_time__gt=self.start_time + ).exclude(pk=self.pk) + if overlapping_driver_shifts.exists(): + raise ValidationError(f"Driver [{self.driver}] is already assigned to a shift.") + + +class BusStop(models.Model): + + '''A stop made by a bus during a bus shift.''' + + # One-to-many relation: 1 busShift === N busStops, but 1 busStop === 1 busShift + bus_shift = models.ForeignKey(BusShift, on_delete=models.CASCADE, related_name='stops') + place = models.ForeignKey('geography.Place', on_delete=models.CASCADE) + + # Define order of stops within a bus shift + order = models.IntegerField() + + stop_time = models.DateTimeField() + + # Auto ordering by 'order' field + class Meta: + ordering = ['order'] + + def __str__(self): + return f"Stop {self.order}: {self.place.name} at {self.stop_time} (id: {self.pk})"