From 6ad70504de687226a01ea66b43cb31a3dbc452e1 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 11:17:05 +0100 Subject: [PATCH 01/42] Add Dockerfile for containerized application setup --- Dockerfile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d21575c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +ENV PIP_DISABLE_PIP_VERSION_CHECK 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +WORKDIR /code + +COPY ./requirements.txt . + +RUN apt-get update -y && \ + apt-get install -y netcat-openbsd && \ + pip install --upgrade pip && \ + pip install -r requirements.txt + +COPY ./entrypoint.sh . +RUN chmod +x /code/entrypoint.sh + +COPY . . + +ENTRYPOINT ["/code/entrypoint.sh"] \ No newline at end of file From 2249dd52d620458added7d886573e587337c1cdf Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 11:17:25 +0100 Subject: [PATCH 02/42] Add docker-compose.yml for application services setup --- docker-compose.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3b12371a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + web: + build: . + container_name: padam-web + command: python /code/manage.py runserver 0.0.0.0:8001 + volumes: + - .:/code + env_file: + - ./.env + ports: + - "8001:8001" + depends_on: + - db + + db: + container_name: padam-db + image: postgres:14 + volumes: + - postgres_data:/var/lib/postgresql/data/ + environment: + - "POSTGRES_HOST_AUTH_METHOD=trust" + - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME} + + +volumes: + postgres_data: \ No newline at end of file From c16ecd3b108193350e093306d63bdeb0f1227aa0 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 11:18:07 +0100 Subject: [PATCH 03/42] Refactor settings.py for PostgreSQL database configuration and add static files directory --- padam_django/settings.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/padam_django/settings.py b/padam_django/settings.py index 129e922c..e786e15a 100644 --- a/padam_django/settings.py +++ b/padam_django/settings.py @@ -11,6 +11,8 @@ """ from pathlib import Path +from decouple import config +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -45,6 +47,7 @@ 'padam_django.apps.fleet', 'padam_django.apps.geography', 'padam_django.apps.users', + 'padam_django.apps.transport', ] MIDDLEWARE = [ @@ -82,9 +85,13 @@ # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": config("DB_NAME"), + "USER": config("DB_USERNAME"), + "PASSWORD": config("DB_PASSWORD"), + "HOST": config("DB_HOSTNAME"), + "PORT": config("DB_PORT", cast=int), } } @@ -132,6 +139,7 @@ STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field From 5267701a0bc1ce603a81d25524de2cde920f04d7 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 11:18:23 +0100 Subject: [PATCH 04/42] Add entrypoint script for application initialization and setup --- entrypoint.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 entrypoint.sh diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..b65a817c --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +echo 'Waiting for postgres...' + +while ! nc -z $DB_HOSTNAME $DB_PORT; do + sleep 0.1 +done + +echo 'PostgreSQL started' + +echo 'Installing requirements...' +pip install -r requirements.txt + +echo 'Making migrations...' +python manage.py makemigrations + +echo 'Running migrations...' +python manage.py migrate + +echo 'Collecting static files...' +python manage.py collectstatic --no-input + +exec "$@" \ No newline at end of file From 9ca5e37978a731f012aadce186f3f8ed319b46ee Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 11:23:02 +0100 Subject: [PATCH 05/42] Implement BusShift and BusStop admin interfaces with automatic shift time calculations --- padam_django/apps/transport/admin.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 padam_django/apps/transport/admin.py diff --git a/padam_django/apps/transport/admin.py b/padam_django/apps/transport/admin.py new file mode 100644 index 00000000..f59ff04f --- /dev/null +++ b/padam_django/apps/transport/admin.py @@ -0,0 +1,25 @@ +from django.contrib import admin +from .models import BusShift, BusStop +from .forms import BusShiftForm +from .bus_shift_service import update_shift_times + +class BusStopInline(admin.TabularInline): + model = BusStop + extra = 2 + fields = ('place', 'time', 'order') + ordering = ('order',) + +@admin.register(BusShift) +class BusShiftAdmin(admin.ModelAdmin): + form = BusShiftForm + inlines = [BusStopInline] + list_display = ['bus', 'driver', 'status', 'start_time', 'end_time', 'duration'] + search_fields = ['bus__licence_plate', 'driver__user__username'] + + def save_formset(self, request, form, formset, change): + """ + Sauvegarde des inlines puis calcul automatique des temps du trajet. + """ + super().save_formset(request, form, formset, change) + # Calcul des temps et vérification chevauchements + update_shift_times(form.instance) From ca522cc560a8eeef222149f49466449d853a52f6 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 14:54:39 +0100 Subject: [PATCH 06/42] Add initial migration for BusShift and BusStop models with constraints --- .../apps/transport/migrations/0001_initial.py | 50 +++++++++++++++++++ ...op_unique_stop_order_per_shift_and_more.py | 45 +++++++++++++++++ .../migrations/0003_alter_busshift_status.py | 18 +++++++ ...rename_shift_busstop_bus_shift_and_more.py | 32 ++++++++++++ ...usstop_shift_busshift_duration_and_more.py | 48 ++++++++++++++++++ ...hift_bus_alter_busshift_driver_and_more.py | 36 +++++++++++++ ...007_remove_busshift_created_at_and_more.py | 26 ++++++++++ ...008_busstop_unique_stop_order_per_shift.py | 17 +++++++ .../apps/transport/migrations/__init__.py | 0 9 files changed, 272 insertions(+) create mode 100644 padam_django/apps/transport/migrations/0001_initial.py create mode 100644 padam_django/apps/transport/migrations/0002_remove_busstop_unique_stop_order_per_shift_and_more.py create mode 100644 padam_django/apps/transport/migrations/0003_alter_busshift_status.py create mode 100644 padam_django/apps/transport/migrations/0004_rename_shift_busstop_bus_shift_and_more.py create mode 100644 padam_django/apps/transport/migrations/0005_rename_bus_shift_busstop_shift_busshift_duration_and_more.py create mode 100644 padam_django/apps/transport/migrations/0006_alter_busshift_bus_alter_busshift_driver_and_more.py create mode 100644 padam_django/apps/transport/migrations/0007_remove_busshift_created_at_and_more.py create mode 100644 padam_django/apps/transport/migrations/0008_busstop_unique_stop_order_per_shift.py create mode 100644 padam_django/apps/transport/migrations/__init__.py diff --git a/padam_django/apps/transport/migrations/0001_initial.py b/padam_django/apps/transport/migrations/0001_initial.py new file mode 100644 index 00000000..6ebcb466 --- /dev/null +++ b/padam_django/apps/transport/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.16 on 2025-12-06 18:01 + +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='BusShift', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('status', models.CharField(choices=[('draft', 'Brouillon'), ('planned', 'Planifié'), ('ongoing', 'En cours'), ('completed', 'Terminé'), ('cancelled', 'Annulé')], db_index=True, default='draft', max_length=20)), + ('bus', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='shifts', to='fleet.bus')), + ('driver', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='shifts', to='fleet.driver')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='BusStop', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('time', models.DateTimeField()), + ('order', models.PositiveIntegerField(help_text="Ordre de l'arrêt dans le trajet")), + ('place', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='geography.place')), + ('shift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stops', to='transport.busshift')), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.AddConstraint( + model_name='busstop', + constraint=models.UniqueConstraint(fields=('shift', 'order'), name='unique_stop_order_per_shift'), + ), + ] diff --git a/padam_django/apps/transport/migrations/0002_remove_busstop_unique_stop_order_per_shift_and_more.py b/padam_django/apps/transport/migrations/0002_remove_busstop_unique_stop_order_per_shift_and_more.py new file mode 100644 index 00000000..2782851d --- /dev/null +++ b/padam_django/apps/transport/migrations/0002_remove_busstop_unique_stop_order_per_shift_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.16 on 2025-12-06 18:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('fleet', '0002_auto_20211109_1456'), + ('geography', '0001_initial'), + ('transport', '0001_initial'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='busstop', + name='unique_stop_order_per_shift', + ), + migrations.AlterField( + model_name='busshift', + name='bus', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.bus'), + ), + migrations.AlterField( + model_name='busshift', + name='driver', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.driver'), + ), + migrations.AlterField( + model_name='busshift', + name='status', + field=models.CharField(choices=[('brouillon', 'Brouillon'), ('planned', 'Prévu'), ('done', 'Terminé')], default='brouillon', max_length=20), + ), + migrations.AlterField( + model_name='busstop', + name='order', + field=models.PositiveIntegerField(default=1), + ), + migrations.AlterField( + model_name='busstop', + name='place', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geography.place'), + ), + ] diff --git a/padam_django/apps/transport/migrations/0003_alter_busshift_status.py b/padam_django/apps/transport/migrations/0003_alter_busshift_status.py new file mode 100644 index 00000000..951fd1ab --- /dev/null +++ b/padam_django/apps/transport/migrations/0003_alter_busshift_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-12-06 18:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transport', '0002_remove_busstop_unique_stop_order_per_shift_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='busshift', + name='status', + field=models.CharField(choices=[('brouillon', 'Brouillon'), ('prévu', 'Prévu'), ('terminé', 'Terminé')], default='brouillon', max_length=20), + ), + ] diff --git a/padam_django/apps/transport/migrations/0004_rename_shift_busstop_bus_shift_and_more.py b/padam_django/apps/transport/migrations/0004_rename_shift_busstop_bus_shift_and_more.py new file mode 100644 index 00000000..3eda6c30 --- /dev/null +++ b/padam_django/apps/transport/migrations/0004_rename_shift_busstop_bus_shift_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.16 on 2025-12-06 19:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('transport', '0003_alter_busshift_status'), + ] + + operations = [ + migrations.RenameField( + model_name='busstop', + old_name='shift', + new_name='bus_shift', + ), + migrations.RemoveField( + model_name='busstop', + name='created_at', + ), + migrations.RemoveField( + model_name='busstop', + name='updated_at', + ), + migrations.AlterField( + model_name='busstop', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/padam_django/apps/transport/migrations/0005_rename_bus_shift_busstop_shift_busshift_duration_and_more.py b/padam_django/apps/transport/migrations/0005_rename_bus_shift_busstop_shift_busshift_duration_and_more.py new file mode 100644 index 00000000..82bd66ab --- /dev/null +++ b/padam_django/apps/transport/migrations/0005_rename_bus_shift_busstop_shift_busshift_duration_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.16 on 2025-12-07 05:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transport', '0004_rename_shift_busstop_bus_shift_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='busstop', + old_name='bus_shift', + new_name='shift', + ), + migrations.AddField( + model_name='busshift', + name='duration', + field=models.DurationField(blank=True, null=True), + ), + migrations.AddField( + model_name='busshift', + name='end_time', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='busshift', + name='start_time', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='busshift', + name='status', + field=models.CharField(choices=[('draft', 'Brouillon'), ('planned', 'Prévu'), ('done', 'Terminé')], default='draft', max_length=20), + ), + migrations.AlterField( + model_name='busstop', + name='order', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='busstop', + name='time', + field=models.DateTimeField(), + ), + ] diff --git a/padam_django/apps/transport/migrations/0006_alter_busshift_bus_alter_busshift_driver_and_more.py b/padam_django/apps/transport/migrations/0006_alter_busshift_bus_alter_busshift_driver_and_more.py new file mode 100644 index 00000000..855409a0 --- /dev/null +++ b/padam_django/apps/transport/migrations/0006_alter_busshift_bus_alter_busshift_driver_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.16 on 2025-12-07 05:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('geography', '0001_initial'), + ('fleet', '0002_auto_20211109_1456'), + ('transport', '0005_rename_bus_shift_busstop_shift_busshift_duration_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='busshift', + name='bus', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='shifts', to='fleet.bus'), + ), + migrations.AlterField( + model_name='busshift', + name='driver', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='shifts', to='fleet.driver'), + ), + migrations.AlterField( + model_name='busshift', + name='status', + field=models.CharField(choices=[('Brouillon', 'Brouillon'), ('Prévu', 'Prévu'), ('Terminé', 'Terminé')], default='Brouillon', max_length=20), + ), + migrations.AlterField( + model_name='busstop', + name='place', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='geography.place'), + ), + ] diff --git a/padam_django/apps/transport/migrations/0007_remove_busshift_created_at_and_more.py b/padam_django/apps/transport/migrations/0007_remove_busshift_created_at_and_more.py new file mode 100644 index 00000000..31284720 --- /dev/null +++ b/padam_django/apps/transport/migrations/0007_remove_busshift_created_at_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.16 on 2025-12-07 08:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transport', '0006_alter_busshift_bus_alter_busshift_driver_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='busshift', + name='created_at', + ), + migrations.RemoveField( + model_name='busshift', + name='updated_at', + ), + migrations.AlterField( + model_name='busshift', + name='status', + field=models.CharField(default='Brouillon', max_length=20), + ), + ] diff --git a/padam_django/apps/transport/migrations/0008_busstop_unique_stop_order_per_shift.py b/padam_django/apps/transport/migrations/0008_busstop_unique_stop_order_per_shift.py new file mode 100644 index 00000000..e157670b --- /dev/null +++ b/padam_django/apps/transport/migrations/0008_busstop_unique_stop_order_per_shift.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2025-12-07 08:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transport', '0007_remove_busshift_created_at_and_more'), + ] + + operations = [ + migrations.AddConstraint( + model_name='busstop', + constraint=models.UniqueConstraint(fields=('shift', 'order'), name='unique_stop_order_per_shift'), + ), + ] diff --git a/padam_django/apps/transport/migrations/__init__.py b/padam_django/apps/transport/migrations/__init__.py new file mode 100644 index 00000000..e69de29b From d856773c29f37fb3856b065cacbe35ea0517a699 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 14:56:30 +0100 Subject: [PATCH 07/42] Refactor imports and formatting in management commands and models; add __init__.py for transport app --- padam_django/apps/common/management/commands/create_data.py | 1 + padam_django/apps/fleet/management/commands/create_buses.py | 1 - padam_django/apps/fleet/models.py | 4 +++- .../apps/geography/management/commands/create_places.py | 1 - padam_django/apps/transport/__init__.py | 0 padam_django/apps/transport/admin.py | 6 ++++-- padam_django/settings.py | 5 +++-- 7 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 padam_django/apps/transport/__init__.py diff --git a/padam_django/apps/common/management/commands/create_data.py b/padam_django/apps/common/management/commands/create_data.py index a149a937..ba051342 100644 --- a/padam_django/apps/common/management/commands/create_data.py +++ b/padam_django/apps/common/management/commands/create_data.py @@ -1,3 +1,4 @@ +from django.core import management from django.core.management.base import BaseCommand from django.core import management diff --git a/padam_django/apps/fleet/management/commands/create_buses.py b/padam_django/apps/fleet/management/commands/create_buses.py index eaadc0a8..0a5c2be9 100644 --- a/padam_django/apps/fleet/management/commands/create_buses.py +++ b/padam_django/apps/fleet/management/commands/create_buses.py @@ -1,5 +1,4 @@ from padam_django.apps.common.management.base import CreateDataBaseCommand - from padam_django.apps.fleet.factories import BusFactory diff --git a/padam_django/apps/fleet/models.py b/padam_django/apps/fleet/models.py index 4cd3f19d..b92ecfca 100644 --- a/padam_django/apps/fleet/models.py +++ b/padam_django/apps/fleet/models.py @@ -2,7 +2,9 @@ class Driver(models.Model): - user = models.OneToOneField('users.User', on_delete=models.CASCADE, related_name='driver') + user = models.OneToOneField( + 'users.User', on_delete=models.CASCADE, related_name='driver' + ) def __str__(self): return f"Driver: {self.user.username} (id: {self.pk})" diff --git a/padam_django/apps/geography/management/commands/create_places.py b/padam_django/apps/geography/management/commands/create_places.py index beb41514..283883f8 100644 --- a/padam_django/apps/geography/management/commands/create_places.py +++ b/padam_django/apps/geography/management/commands/create_places.py @@ -1,5 +1,4 @@ from padam_django.apps.common.management.base import CreateDataBaseCommand - from padam_django.apps.geography.factories import PlaceFactory diff --git a/padam_django/apps/transport/__init__.py b/padam_django/apps/transport/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padam_django/apps/transport/admin.py b/padam_django/apps/transport/admin.py index f59ff04f..abda9b81 100644 --- a/padam_django/apps/transport/admin.py +++ b/padam_django/apps/transport/admin.py @@ -1,7 +1,9 @@ from django.contrib import admin -from .models import BusShift, BusStop -from .forms import BusShiftForm + from .bus_shift_service import update_shift_times +from .forms import BusShiftForm +from .models import BusShift, BusStop + class BusStopInline(admin.TabularInline): model = BusStop diff --git a/padam_django/settings.py b/padam_django/settings.py index e786e15a..6f9bba6d 100644 --- a/padam_django/settings.py +++ b/padam_django/settings.py @@ -10,9 +10,10 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ +import os from pathlib import Path + from decouple import config -import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -22,7 +23,7 @@ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-&r2)+_fdqxe2dtc@1vizr6tsh6!1cesaptlfgj@ug*%3=fnq=i' +SECRET_KEY = "django-insecure-&r2)+_fdqxe2dtc@1vizr6tsh6!1cesaptlfgj@ug*%3=fnq=i" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True From a1e46d703e30897b6efe567c6e824086b7e99f0a Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 14:57:15 +0100 Subject: [PATCH 08/42] Implement update_shift_times function for BusShift to calculate start_time, end_time, and duration --- .../apps/transport/bus_shift_service.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 padam_django/apps/transport/bus_shift_service.py diff --git a/padam_django/apps/transport/bus_shift_service.py b/padam_django/apps/transport/bus_shift_service.py new file mode 100644 index 00000000..087c3acc --- /dev/null +++ b/padam_django/apps/transport/bus_shift_service.py @@ -0,0 +1,28 @@ +from django.core.exceptions import ValidationError +from django.db.models import Q + +def update_shift_times(shift): + """ + Met à jour start_time, end_time et duration pour un BusShift + après la sauvegarde des BusStop. + """ + stops = list(shift.stops.all()) + if len(stops) < 2: + raise ValidationError("Un trajet doit avoir au moins 2 arrêts.") + + # Tri par order + stops_sorted = sorted(stops, key=lambda s: s.order) + shift.start_time = stops_sorted[0].time + shift.end_time = stops_sorted[-1].time + shift.duration = shift.end_time - shift.start_time + + # Vérification chevauchements bus/driver + overlapping = shift.__class__.objects.exclude(pk=shift.pk).filter( + Q(bus=shift.bus) | Q(driver=shift.driver), + start_time__lt=shift.end_time, + end_time__gt=shift.start_time, + ) + if overlapping.exists(): + raise ValidationError("Ce trajet chevauche un autre trajet existant.") + + shift.save() From 6d22bea73f6288ef0612b0f8a376c226bc7bd2b5 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 14:57:36 +0100 Subject: [PATCH 09/42] Add TripsConfig class for transport app configuration --- padam_django/apps/transport/apps.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 padam_django/apps/transport/apps.py diff --git a/padam_django/apps/transport/apps.py b/padam_django/apps/transport/apps.py new file mode 100644 index 00000000..606db56f --- /dev/null +++ b/padam_django/apps/transport/apps.py @@ -0,0 +1,4 @@ +from django.apps import AppConfig + +class TripsConfig(AppConfig): + name = "padam_django.apps.transport" \ No newline at end of file From 0451b55687eb5278d3d645554caf64e5454eaf39 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 14:58:44 +0100 Subject: [PATCH 10/42] Add BusShiftForm for managing bus shift data with validation for stops --- padam_django/apps/transport/forms.py | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 padam_django/apps/transport/forms.py diff --git a/padam_django/apps/transport/forms.py b/padam_django/apps/transport/forms.py new file mode 100644 index 00000000..f6feb1a5 --- /dev/null +++ b/padam_django/apps/transport/forms.py @@ -0,0 +1,29 @@ +from django import forms +from django.core.exceptions import ValidationError +from .models import BusShift, BusStop + +class BusShiftForm(forms.ModelForm): + inline_formset = None # sera injecté depuis l'admin + + class Meta: + model = BusShift + fields = ['bus', 'driver', 'status'] + + def set_inline_formset(self, formset): + """Injecte l'inline formset depuis l'admin.""" + self.inline_formset = formset + + def clean(self): + cleaned_data = super().clean() + + if self.inline_formset: + stops = [ + f.cleaned_data + for f in self.inline_formset + if not f.cleaned_data.get("DELETE") + ] + + if len(stops) < 2: + raise ValidationError("Un trajet doit avoir au moins 2 arrêts.") + + return cleaned_data From e4ed1056835b8cd70077927e5bd8d7484031d859 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 14:59:13 +0100 Subject: [PATCH 11/42] Add BusShift and BusStop models for managing bus shifts and stops with constraints --- padam_django/apps/transport/models.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 padam_django/apps/transport/models.py diff --git a/padam_django/apps/transport/models.py b/padam_django/apps/transport/models.py new file mode 100644 index 00000000..8ccaac67 --- /dev/null +++ b/padam_django/apps/transport/models.py @@ -0,0 +1,21 @@ +from django.db import models + +class BusShift(models.Model): + bus = models.ForeignKey('fleet.Bus', on_delete=models.PROTECT, related_name='shifts') + driver = models.ForeignKey('fleet.Driver', on_delete=models.PROTECT, related_name='shifts') + status = models.CharField(max_length=20, default='Brouillon') + start_time = models.DateTimeField(null=True, blank=True) + end_time = models.DateTimeField(null=True, blank=True) + duration = models.DurationField(null=True, blank=True) + +class BusStop(models.Model): + shift = models.ForeignKey(BusShift, on_delete=models.CASCADE, related_name='stops') + place = models.ForeignKey('geography.Place', on_delete=models.PROTECT) + time = models.DateTimeField() + order = models.PositiveIntegerField() + + class Meta: + ordering = ['order'] + constraints = [ + models.UniqueConstraint(fields=['shift', 'order'], name='unique_stop_order_per_shift') + ] From 392e9c3a297a04db20c2b0f77df3d9196a6e8865 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 14:59:55 +0100 Subject: [PATCH 12/42] Add a blank line for improved readability in UserAdmin class --- padam_django/apps/users/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/padam_django/apps/users/admin.py b/padam_django/apps/users/admin.py index 2bc531c6..519d3da3 100644 --- a/padam_django/apps/users/admin.py +++ b/padam_django/apps/users/admin.py @@ -9,5 +9,6 @@ class UserAdmin(admin.ModelAdmin): def is_driver(self, obj): return obj.is_driver + is_driver.boolean = True is_driver.short_description = 'Is driver' From 87edbd3c3ee794d2c0a7917a7b363df847cef12c Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 15:00:20 +0100 Subject: [PATCH 13/42] Add a blank line for improved readability in User model --- padam_django/apps/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/padam_django/apps/users/models.py b/padam_django/apps/users/models.py index 672f6a15..73309e08 100644 --- a/padam_django/apps/users/models.py +++ b/padam_django/apps/users/models.py @@ -7,3 +7,4 @@ class User(AbstractUser): def is_driver(self) -> bool: """Define if the user is related to a driver.""" return hasattr(self, 'driver') + From 78f8f2e662b3c900d1470cf2f314b611e216114a Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 15:01:05 +0100 Subject: [PATCH 14/42] Fix missing newline at end of file in Dockerfile and entrypoint.sh --- Dockerfile | 2 +- entrypoint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d21575c9..66028b22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,4 @@ RUN chmod +x /code/entrypoint.sh COPY . . -ENTRYPOINT ["/code/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/code/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh index b65a817c..cebbb509 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -19,4 +19,4 @@ python manage.py migrate echo 'Collecting static files...' python manage.py collectstatic --no-input -exec "$@" \ No newline at end of file +exec "$@" From 12d268ffd036d4d0f665710784016bf17b65438a Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 15:11:18 +0100 Subject: [PATCH 15/42] Add .dockerignore and update .gitignore for improved file management --- .dockerignore | 3 + .gitignore | 138 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c5dd6ce5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.venv +.git +.gitignore diff --git a/.gitignore b/.gitignore index d7d26693..a660ddff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/ +*.iml # Byte-compiled / optimized / DLL files __pycache__/ .pytest_cache/ @@ -20,3 +22,139 @@ ENV/ # Editors stuff .idea .vscode +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments + +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.venv-dev + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +/data + +media/ +mediafiles/ +staticfiles/ diff --git a/docker-compose.yml b/docker-compose.yml index 3b12371a..a7ae9809 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: volumes: - postgres_data: \ No newline at end of file + postgres_data: From 80e1e4c1c3cc20d3d088dcf072a8d4e6c7bbe58d Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 15:11:58 +0100 Subject: [PATCH 16/42] Update requirements.txt to include additional dependencies for Django project --- requirements.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/requirements.txt b/requirements.txt index 863fd63d..dc27088e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,13 @@ Django==4.2.16 django-extensions==3.2.1 Werkzeug==3.1.3 ipython==8.29.0 +django-model-utils factory-boy==3.2.0 Faker==8.10.1 +python-decouple==3.6 +psycopg2-binary +# Outils pour Django et tests +pytest +pytest-django +pylint-django From a411fe0cb635e0d95e696aee925257dd636f67fa Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 15:13:01 +0100 Subject: [PATCH 17/42] Add pre-commit configuration and update linting tools in requirements --- .pre-commit-config.yaml | 40 ++++++++++++++++++++++++++++++++++++++++ dev-requirements.txt | 5 +++++ pyproject.toml | 20 ++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 dev-requirements.txt create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..a112ed0c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,40 @@ +repos: + - repo: 'https://github.com/pre-commit/pre-commit-hooks' + rev: 'v4.6.0' + hooks: + - id: 'check-yaml' + - id: 'end-of-file-fixer' + - id: 'trailing-whitespace' + + - repo: 'https://github.com/psf/black' + rev: '24.3.0' + hooks: + - id: 'black' + language_version: 'python3.11' + files: '\.py$' + exclude: 'migrations/' + + - repo: 'https://github.com/PyCQA/isort' + rev: '5.13.2' + hooks: + - id: 'isort' + language_version: 'python3.11' + files: '\.py$' + exclude: 'migrations/' + + - repo: 'https://github.com/PyCQA/flake8' + rev: '7.0.0' + hooks: + - id: 'flake8' + language_version: 'python3.11' + files: '\.py$' + exclude: 'migrations/' + + - repo: 'https://github.com/pre-commit/mirrors-pylint' + rev: 'main' + hooks: + - id: 'pylint' + language_version: 'python3.11' + files: '\.py$' + exclude: 'migrations/' + args: ['--disable=E0401'] diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..edac1fb4 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,5 @@ +# Outils de formatage et linting +black==24.3.0 +flake8==7.0.0 +isort==5.13.2 +pre-commit diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..9ca2bc57 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.black] +line-length = 88 +target-version = ['py311'] +include = '\.pyi?$' + +[tool.isort] +profile = 'django' +combine_as_imports = true +include_trailing_comma = true +line_length = 88 +multi_line_output = 3 +known_first_party = ['config'] +known_third_party = ['django', 'rest_framework', 'geopy', 'oauth2_provider'] +force_sort_within_sections = true +force_alphabetical_sort_within_sections = true +order_by_type = false + +[tool.flake8] +max-line-length = 88 +exclude = '.git,__pycache__,env,venv,.venv,.mypy_cache,.pytest_cache' From 45fb864a6214c4ca696c076df4563564c08d4213 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 15:13:19 +0100 Subject: [PATCH 18/42] Add management command to create a test user with a known password --- .../management/commands/create_test_user.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 padam_django/apps/users/management/commands/create_test_user.py diff --git a/padam_django/apps/users/management/commands/create_test_user.py b/padam_django/apps/users/management/commands/create_test_user.py new file mode 100644 index 00000000..ec00fe41 --- /dev/null +++ b/padam_django/apps/users/management/commands/create_test_user.py @@ -0,0 +1,23 @@ +# padam_django/apps/users/management/commands/create_test_user.py +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.apps import apps + +User = get_user_model() + +class Command(BaseCommand): + help = "Créer un compte test pour l'évaluateur avec mot de passe connu" + + def handle(self, *args, **kwargs): + username = "moniquemasson" + password = "test12356" + + user, created = User.objects.get_or_create(username=username) + user.set_password(password) + user.is_staff = True # pour avoir accès à l'admin + user.save() + + if created: + self.stdout.write(self.style.SUCCESS(f"Compte {username} créé avec succès.")) + else: + self.stdout.write(self.style.SUCCESS(f"Mot de passe pour {username} mis à jour.")) From f08cd83a5505ac668158375aba60c261a5729ed7 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 15:13:34 +0100 Subject: [PATCH 19/42] Add management command to update non-driver users with BusShift permissions --- .../management/commands/update_non_drivers.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 padam_django/apps/users/management/commands/update_non_drivers.py diff --git a/padam_django/apps/users/management/commands/update_non_drivers.py b/padam_django/apps/users/management/commands/update_non_drivers.py new file mode 100644 index 00000000..079c1092 --- /dev/null +++ b/padam_django/apps/users/management/commands/update_non_drivers.py @@ -0,0 +1,43 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.apps import apps + +User = get_user_model() + +class Command(BaseCommand): + help = "Met à jour tous les utilisateurs non-driver pour qu'ils puissent accéder aux BusShift dans l'admin" + + def handle(self, *args, **options): + # Récupérer le modèle BusShift + BusShift = apps.get_model('transport', 'BusShift') + + # Récupérer les permissions add/change/view + busshift_perms = Permission.objects.filter( + content_type__app_label='transport', + content_type__model='busshift', + codename__in=['add_busshift', 'change_busshift', 'view_busshift'] + ) + + + # Permissions BusStop + busstop_perms = Permission.objects.filter( + content_type__app_label='transport', + content_type__model='busstop', + codename__in=['add_busstop', 'change_busstop', 'view_busstop'] + ) + + # Filtrer les utilisateurs non drivers + non_drivers = User.objects.filter(driver__isnull=True) + count = 0 + + for user in non_drivers: + user.is_staff = True + user.user_permissions.add(*busshift_perms) + user.user_permissions.add(*busstop_perms) + user.save() + count += 1 + + self.stdout.write(self.style.SUCCESS( + f"{count} utilisateurs non-driver mis à jour avec is_staff=True et permissions BusShift." + )) From ee8c008c7e57b9aeffa5be48bd6f13bc1c11f6ae Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 18:09:28 +0100 Subject: [PATCH 20/42] Add README_SOLUTION.md with Docker setup and management commands --- README_SOLUTION.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 README_SOLUTION.md diff --git a/README_SOLUTION.md b/README_SOLUTION.md new file mode 100644 index 00000000..d597e6e4 --- /dev/null +++ b/README_SOLUTION.md @@ -0,0 +1,68 @@ +# Solution + +## Lancer le projet avec Docker et Makefile + +Le projet est configuré pour s’exécuter avec **Docker** et une base **PostgreSQL**. +Des scripts `Makefile` facilitent les opérations courantes pour le développement et les tests. + +### Commandes Docker / Makefile + +- **Démarrer les conteneurs Docker** + `make docker-up` + +- **Arrêter les conteneurs Docker** + `make docker-down` + +- **Accéder au shell du conteneur web** + `make docker-sh` + +- **Appliquer les migrations de la base de données** + `make docker-migrate` + +- **Créer des données d’exemple** + `make docker-create-data` + +- **Créer un superutilisateur Django** + `make docker-superuser` + +- **Lancer le serveur de développement Django** + `make docker-run` + +- **Assigner les permissions aux utilisateurs non-driver** + `make docker-assign-perms` + +- **Créer un utilisateur de test** + `make docker-user-test` + +- **Exécuter les tests unitaires** + `make docker-test` + +--- + +## Management commands utiles + +### update_non_drivers.py + +- Configure tous les utilisateurs **non-driver** pour qu’ils puissent gérer les **BusShift** et **BusStop** dans l’interface Django admin. +- Tous les utilisateurs non-driver deviennent `staff`. +- Ils peuvent **créer, modifier et consulter** les trajets et arrêts depuis l’admin. + +### create_test_user.py + +- Permet de créer ou modifier le mot de passe d’un utilisateur de test. +- Exemple d’utilisation : + - `username = "moniquemasson"` + - `password = "test12356"` + +--- + +## Tests unitaires + +**Fichier :** `test_bus_shift_service.py` + +- Les tests couvrent à la fois : + - **La validation métier** (nombre minimum d’arrêts, chevauchement de trajets) + - **La logique de calcul des temps** (start_time, end_time, duration) + +- Les tests sont **isolés**, utilisant `setUp` pour créer des entités réutilisables : + - `Bus`, `Driver`, `Place` From 48a3557d48177c6ddd1fadc62ef9dbaaaf5e5e1d Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 18:09:50 +0100 Subject: [PATCH 21/42] Refactor Makefile to include Docker commands for container management and database migrations --- Makefile | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4062f4c4..59414b9e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,41 @@ -run: ## Run the test server. - python manage.py runserver_plus +# Start the docker containers +docker-up: ## Start the docker containers + docker-compose up --build + +# Stop the docker containers +docker-down: ## Stop the docker containers + docker-compose down + +# Access the web container shell +docker-sh: ## Access the docker container shell + docker exec -it padam-web sh + +dokcer-migrate: ## Apply database migrations inside the container + docker exec -it padam-web python manage.py migrate + +docker + +# Create a Django superuser inside the container +docker-superuser: ## Create a superuser for the Django admin + docker exec -it padam-web python manage.py createsuperuser + +# Run the Django dev server inside the container +docker-run: ## Run the test server inside the container + docker exec -it padam-web python manage.py runserver_plus 0.0.0.0:8000 + +# Create sample data inside the container +docker-create-data: ## Create sample data + docker exec -it padam-web python manage.py create_data + +# Assign permissions to non-driver users inside the container +docker-assign-perms: ## Make non-driver users staff and assign BusShift & BusStop permissions + docker exec -it padam-web python manage.py update_non_drivers + +# Assign permissions to non-driver users inside the container +docker-user_test: ## + docker exec -it padam-web python manage.py create_test_user + +# Run tests inside the container +docker-test: ## Run tests inside the container +docker exec -it padam-web python manage.py test -install: ## Install the python requirements. - pip install -r requirements.txt From 06231546d7e1fa1f82f3ae777caec2ec1bce171e Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 18:10:40 +0100 Subject: [PATCH 22/42] Add unit tests for BusShift update_shift_times functionality --- padam_django/apps/transport/tests/__init__.py | 0 .../transport/tests/test_bus_shift_service.py | 86 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 padam_django/apps/transport/tests/__init__.py create mode 100644 padam_django/apps/transport/tests/test_bus_shift_service.py diff --git a/padam_django/apps/transport/tests/__init__.py b/padam_django/apps/transport/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padam_django/apps/transport/tests/test_bus_shift_service.py b/padam_django/apps/transport/tests/test_bus_shift_service.py new file mode 100644 index 00000000..6d0b650b --- /dev/null +++ b/padam_django/apps/transport/tests/test_bus_shift_service.py @@ -0,0 +1,86 @@ +from django.test import TestCase +from django.core.exceptions import ValidationError +from datetime import datetime, timedelta + +from padam_django.apps.transport.models import BusShift, BusStop +from padam_django.apps.fleet.models import Bus, Driver +from padam_django.apps.geography.models import Place +from padam_django.apps.users.models import User +from django.utils import timezone + +from ..bus_shift_service import update_shift_times + + +class UpdateShiftTimesTest(TestCase): + + def setUp(self): + # Création d’un User obligatoire pour Driver + self.user = User.objects.create(username="driver1") + + # Driver valide + self.driver = Driver.objects.create(user=self.user) + + # Bus valide + self.bus = Bus.objects.create(licence_plate="ABC123") + + # Places valides (avec coordonnées obligatoires) + self.placeA = Place.objects.create(name="A", latitude=48.8566, longitude=2.3522) + self.placeB = Place.objects.create(name="B", latitude=48.8570, longitude=2.3530) + self.placeC = Place.objects.create(name="C", latitude=48.8575, longitude=2.3540) + self.placeD = Place.objects.create(name="D", latitude=48.8580, longitude=2.3550) + + def test_less_than_two_stops_raises_error(self): + shift = BusShift.objects.create(bus=self.bus, driver=self.driver) + + BusStop.objects.create( + shift=shift, + place=self.placeA, + time=datetime.now(), + order=1 + ) + + with self.assertRaises(ValidationError): + update_shift_times(shift) + + def test_correct_start_end_duration(self): + shift = BusShift.objects.create(bus=self.bus, driver=self.driver) + now = timezone.now() + + stop1 = BusStop.objects.create( + shift=shift, + place=self.placeA, + time=now, + order=2 + ) + + stop2 = BusStop.objects.create( + shift=shift, + place=self.placeB, + time=now + timedelta(minutes=30), + order=1 + ) + + update_shift_times(shift) + shift.refresh_from_db() + + self.assertEqual(shift.start_time, stop2.time) + self.assertEqual(shift.end_time, stop1.time) + self.assertEqual(shift.duration, stop1.time - stop2.time) + + def test_overlapping_shift_raises_error(self): + now = datetime.now() + + shift1 = BusShift.objects.create(bus=self.bus, driver=self.driver) + BusStop.objects.create(shift=shift1, place=self.placeA, time=now, order=1) + BusStop.objects.create(shift=shift1, place=self.placeB, time=now + timedelta(minutes=30), order=2) + + shift2 = BusShift.objects.create(bus=self.bus, driver=self.driver) + BusStop.objects.create(shift=shift2, place=self.placeC, time=now + timedelta(minutes=15), order=1) + BusStop.objects.create(shift=shift2, place=self.placeD, time=now + timedelta(minutes=45), order=2) + + # shift1 est valide + update_shift_times(shift1) + + # shift2 chevauche → doit lever une erreur + with self.assertRaises(ValidationError): + update_shift_times(shift2) From 55acb06f06f192b4532d032a05bc333f7e9e7ea5 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 18:11:28 +0100 Subject: [PATCH 23/42] Remove unused BusShift model retrieval from update_non_drivers management command --- .../apps/users/management/commands/update_non_drivers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/padam_django/apps/users/management/commands/update_non_drivers.py b/padam_django/apps/users/management/commands/update_non_drivers.py index 079c1092..6d8c3399 100644 --- a/padam_django/apps/users/management/commands/update_non_drivers.py +++ b/padam_django/apps/users/management/commands/update_non_drivers.py @@ -9,8 +9,6 @@ class Command(BaseCommand): help = "Met à jour tous les utilisateurs non-driver pour qu'ils puissent accéder aux BusShift dans l'admin" def handle(self, *args, **options): - # Récupérer le modèle BusShift - BusShift = apps.get_model('transport', 'BusShift') # Récupérer les permissions add/change/view busshift_perms = Permission.objects.filter( From ce40aa6a376052c90b551bfc6ec0fe5ff3ac21d0 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 18:51:36 +0100 Subject: [PATCH 24/42] Update README_SOLUTION.md to enhance Docker setup instructions and add workflow summary --- README_SOLUTION.md | 88 ++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/README_SOLUTION.md b/README_SOLUTION.md index d597e6e4..040a5ce8 100644 --- a/README_SOLUTION.md +++ b/README_SOLUTION.md @@ -1,51 +1,73 @@ -# Solution +# Solution: Lancer et tester le projet Django avec Docker -## Lancer le projet avec Docker et Makefile +## Table des matières + +1. [Lancer le projet avec Docker et Makefile](#lancer-le-projet-avec-docker-et-makefile) +2. [Management commands utiles](#management-commands-utiles) +3. [Tests unitaires](#tests-unitaires) +4. [Remarques](#remarques-importantes) +5. [Résumé des workflows](#résumé-des-workflows) + +--- + +## 1. Lancer le projet avec Docker et Makefile Le projet est configuré pour s’exécuter avec **Docker** et une base **PostgreSQL**. Des scripts `Makefile` facilitent les opérations courantes pour le développement et les tests. -### Commandes Docker / Makefile +### Commandes à exécuter: -- **Démarrer les conteneurs Docker** +1 - **Démarrer les conteneurs Docker** `make docker-up` -- **Arrêter les conteneurs Docker** - `make docker-down` - -- **Accéder au shell du conteneur web** - `make docker-sh` +2 - **Accéder à l’interface Django admin** + [Django admin](http://127.0.0.1:8001/admin) -- **Appliquer les migrations de la base de données** - `make docker-migrate` - -- **Créer des données d’exemple** +3 - **Créer des données d’exemple** `make docker-create-data` -- **Créer un superutilisateur Django** +4 - **Créer un superutilisateur Django** `make docker-superuser` -- **Lancer le serveur de développement Django** - `make docker-run` - -- **Assigner les permissions aux utilisateurs non-driver** +5 - **Assigner les permissions aux utilisateurs non-driver** `make docker-assign-perms` -- **Créer un utilisateur de test** +6 - **Créer un utilisateur de test** `make docker-user-test` -- **Exécuter les tests unitaires** +7- **Se connecter avec l’utilisateur de test** : + + - `username = "moniquemasson"` + - `password = "test12356"` + +8 - **Exécuter les tests unitaires** `make docker-test` --- -## Management commands utiles +## 2. Management commands utiles -### update_non_drivers.py +- **Arrêter les conteneurs Docker** + `make docker-down` + +- **Accéder au shell du conteneur web** + `make docker-sh` + +- **Appliquer les migrations de la base de données** + `make docker-migrate` -- Configure tous les utilisateurs **non-driver** pour qu’ils puissent gérer les **BusShift** et **BusStop** dans l’interface Django admin. -- Tous les utilisateurs non-driver deviennent `staff`. -- Ils peuvent **créer, modifier et consulter** les trajets et arrêts depuis l’admin. +--- + +## 3. Tests unitaires + +- Les tests sont contenus dans `test_bus_shift_service.py`. +- Ils couvrent la validation métier et la logique de calcul des temps pour BusShift et BusStop. +- Les tests sont isolés et utilisent `setUp` pour créer des entités réutilisables (Bus, Driver, Place). +- Représente un exemple clair de tests unitaires pour un service métier. + +--- + +## 4. Remarques ### create_test_user.py @@ -56,13 +78,11 @@ Des scripts `Makefile` facilitent les opérations courantes pour le développeme --- -## Tests unitaires - -**Fichier :** `test_bus_shift_service.py` - -- Les tests couvrent à la fois : - - **La validation métier** (nombre minimum d’arrêts, chevauchement de trajets) - - **La logique de calcul des temps** (start_time, end_time, duration) +## 5. Résumé des workflows -- Les tests sont **isolés**, utilisant `setUp` pour créer des entités réutilisables : - - `Bus`, `Driver`, `Place` +1. Démarrer les conteneurs Docker → `make docker-up` +2. Créer les données et superutilisateur → `make docker-create-data` + `make docker-superuser` +3. Assigner les permissions → `make docker-assign-perms` +4. Créer et utiliser l’utilisateur de test +5. Exécuter les tests → `make docker-test` +6. Accéder à l’admin Django → [Django admin](http://127.0.0.1:8001/admin) \ No newline at end of file From 62c0c5a5af7e88863ba0b0e6f7eae9d74fa721b1 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 19:01:34 +0100 Subject: [PATCH 25/42] Update requirements.txt to include necessary dependencies for testing --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index dc27088e..b570384b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,13 +3,12 @@ Django==4.2.16 django-extensions==3.2.1 Werkzeug==3.1.3 ipython==8.29.0 -django-model-utils factory-boy==3.2.0 Faker==8.10.1 python-decouple==3.6 psycopg2-binary + # Outils pour Django et tests pytest pytest-django -pylint-django From d86f93aab53b56ddb059bfa3f146b26fc9055fca Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 19:02:24 +0100 Subject: [PATCH 26/42] Clean up Makefile by removing commented-out docker-run target and fixing indentation --- Makefile | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 59414b9e..01148496 100644 --- a/Makefile +++ b/Makefile @@ -13,16 +13,10 @@ docker-sh: ## Access the docker container shell dokcer-migrate: ## Apply database migrations inside the container docker exec -it padam-web python manage.py migrate -docker - # Create a Django superuser inside the container docker-superuser: ## Create a superuser for the Django admin docker exec -it padam-web python manage.py createsuperuser -# Run the Django dev server inside the container -docker-run: ## Run the test server inside the container - docker exec -it padam-web python manage.py runserver_plus 0.0.0.0:8000 - # Create sample data inside the container docker-create-data: ## Create sample data docker exec -it padam-web python manage.py create_data @@ -37,5 +31,5 @@ docker-user_test: ## # Run tests inside the container docker-test: ## Run tests inside the container -docker exec -it padam-web python manage.py test + docker exec -it padam-web python manage.py test From d951f1b3474ee61a95104c5564ea0c0187e37e99 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Sun, 7 Dec 2025 19:04:38 +0100 Subject: [PATCH 27/42] Add .env file with environment configuration for development --- .env | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000..9052a103 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +DEBUG=1 +DJANGO_ALLOWED_HOSTS=localhost +DB_NAME= +DB_USERNAME= +DB_PASSWORD= +DB_HOSTNAME= +DB_PORT= From 1d700d5ddbd4f0852f63a0871309b60a81a1a73e Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Mon, 8 Dec 2025 09:03:20 +0100 Subject: [PATCH 28/42] Update README_SOLUTION.md to modify test user details and reorder commands --- README_SOLUTION.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README_SOLUTION.md b/README_SOLUTION.md index 040a5ce8..b94ab8a1 100644 --- a/README_SOLUTION.md +++ b/README_SOLUTION.md @@ -29,18 +29,20 @@ Des scripts `Makefile` facilitent les opérations courantes pour le développeme 4 - **Créer un superutilisateur Django** `make docker-superuser` -5 - **Assigner les permissions aux utilisateurs non-driver** - `make docker-assign-perms` - -6 - **Créer un utilisateur de test** +5 - **Update un utilisateur de test** `make docker-user-test` -7- **Se connecter avec l’utilisateur de test** : +6 - **Assigner les permissions aux utilisateurs non-driver** + `make docker-assign-perms` - - `username = "moniquemasson"` +7 - **Se connecter avec l’utilisateur de test** : + + - `username = "andreechretien"` - `password = "test12356"` -8 - **Exécuter les tests unitaires** +8 - **Faire les tests sur l'interface admin Django `Add bus shift`** + +9 - **Exécuter les tests unitaires** `make docker-test` --- @@ -72,8 +74,8 @@ Des scripts `Makefile` facilitent les opérations courantes pour le développeme ### create_test_user.py - Permet de créer ou modifier le mot de passe d’un utilisateur de test. -- Exemple d’utilisation : - - `username = "moniquemasson"` +- Utilisation: choisir un utilisateur de la base de donnée **non driver** par exemple : + - `username = "andreechretien"` - `password = "test12356"` --- From 84250ee1ddf86145787b06126c5c7be72f522c8a Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Mon, 8 Dec 2025 09:03:52 +0100 Subject: [PATCH 29/42] Update create_test_user.py to change test username to 'andreechretien' --- padam_django/apps/users/management/commands/create_test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padam_django/apps/users/management/commands/create_test_user.py b/padam_django/apps/users/management/commands/create_test_user.py index ec00fe41..536dc529 100644 --- a/padam_django/apps/users/management/commands/create_test_user.py +++ b/padam_django/apps/users/management/commands/create_test_user.py @@ -9,7 +9,7 @@ class Command(BaseCommand): help = "Créer un compte test pour l'évaluateur avec mot de passe connu" def handle(self, *args, **kwargs): - username = "moniquemasson" + username = "andreechretien" password = "test12356" user, created = User.objects.get_or_create(username=username) From 1e3e75689554a68d168f54004b2e19fc8fc28064 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Tue, 9 Dec 2025 16:50:53 +0100 Subject: [PATCH 30/42] Add development tools section to README_SOLUTION.md for code quality --- README_SOLUTION.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/README_SOLUTION.md b/README_SOLUTION.md index b94ab8a1..f8ddeb67 100644 --- a/README_SOLUTION.md +++ b/README_SOLUTION.md @@ -7,6 +7,8 @@ 3. [Tests unitaires](#tests-unitaires) 4. [Remarques](#remarques-importantes) 5. [Résumé des workflows](#résumé-des-workflows) +6. [Outils de développement](#outils-de-développement) + --- @@ -87,4 +89,26 @@ Des scripts `Makefile` facilitent les opérations courantes pour le développeme 3. Assigner les permissions → `make docker-assign-perms` 4. Créer et utiliser l’utilisateur de test 5. Exécuter les tests → `make docker-test` -6. Accéder à l’admin Django → [Django admin](http://127.0.0.1:8001/admin) \ No newline at end of file +6. Accéder à l’admin Django → [Django admin](http://127.0.0.1:8001/admin) + +--- + +# 6 Outils de développement : formatage et linting + +Le projet inclut des outils pour garantir la qualité et la cohérence du code : + +- **black** : formatage automatique du code Python +- **isort** : tri cohérent des imports +- **flake8** : vérification du style et détection d’erreurs courantes +- **pre-commit** : exécution automatique des hooks avant chaque commit + +## Installation + +```bash +pip install -r dev-requirements.txt +pre-commit install +``` + +## Utilisation +- Chaque commit exécutera automatiquement les outils configurés dans .pre-commit-config.yaml. +- Les corrections automatiques seront appliquées si possible. \ No newline at end of file From cb12679aca85850b98607247b99d1e39e83a6936 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Tue, 9 Dec 2025 16:53:56 +0100 Subject: [PATCH 31/42] Update README_SOLUTION.md to clarify automatic corrections in pre-commit usage --- README_SOLUTION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_SOLUTION.md b/README_SOLUTION.md index f8ddeb67..e23651f2 100644 --- a/README_SOLUTION.md +++ b/README_SOLUTION.md @@ -111,4 +111,4 @@ pre-commit install ## Utilisation - Chaque commit exécutera automatiquement les outils configurés dans .pre-commit-config.yaml. -- Les corrections automatiques seront appliquées si possible. \ No newline at end of file +- Les corrections automatiques seront appliquées. \ No newline at end of file From 3a4ace72dd8f88bf8cc3955704501ad60575bbc1 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Tue, 9 Dec 2025 17:08:51 +0100 Subject: [PATCH 32/42] Refactor BusShiftForm to remove unused BusStop import and clean method logic --- padam_django/apps/transport/forms.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/padam_django/apps/transport/forms.py b/padam_django/apps/transport/forms.py index f6feb1a5..28cdcadc 100644 --- a/padam_django/apps/transport/forms.py +++ b/padam_django/apps/transport/forms.py @@ -1,6 +1,6 @@ from django import forms from django.core.exceptions import ValidationError -from .models import BusShift, BusStop +from .models import BusShift class BusShiftForm(forms.ModelForm): inline_formset = None # sera injecté depuis l'admin @@ -14,16 +14,5 @@ def set_inline_formset(self, formset): self.inline_formset = formset def clean(self): - cleaned_data = super().clean() + return super().clean() - if self.inline_formset: - stops = [ - f.cleaned_data - for f in self.inline_formset - if not f.cleaned_data.get("DELETE") - ] - - if len(stops) < 2: - raise ValidationError("Un trajet doit avoir au moins 2 arrêts.") - - return cleaned_data From 9021c8a62a0df8af23b7b0a42eee12baa72d8e70 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 03:00:06 +0100 Subject: [PATCH 33/42] Refactor BusShiftForm to remove unused methods and clean up code --- padam_django/apps/transport/forms.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/padam_django/apps/transport/forms.py b/padam_django/apps/transport/forms.py index 28cdcadc..a9bbbdcf 100644 --- a/padam_django/apps/transport/forms.py +++ b/padam_django/apps/transport/forms.py @@ -1,18 +1,9 @@ from django import forms -from django.core.exceptions import ValidationError from .models import BusShift class BusShiftForm(forms.ModelForm): - inline_formset = None # sera injecté depuis l'admin + inline_formset = None class Meta: model = BusShift fields = ['bus', 'driver', 'status'] - - def set_inline_formset(self, formset): - """Injecte l'inline formset depuis l'admin.""" - self.inline_formset = formset - - def clean(self): - return super().clean() - From 7e26ec855d4eaecb591979114e52429be644cad6 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 03:01:56 +0100 Subject: [PATCH 34/42] Remove unused inline_formset attribute from BusShiftForm --- padam_django/apps/transport/forms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padam_django/apps/transport/forms.py b/padam_django/apps/transport/forms.py index a9bbbdcf..ad9364e5 100644 --- a/padam_django/apps/transport/forms.py +++ b/padam_django/apps/transport/forms.py @@ -2,7 +2,6 @@ from .models import BusShift class BusShiftForm(forms.ModelForm): - inline_formset = None class Meta: model = BusShift From aff5efef0b8ce39e083a0c3a4c5b773bb84e4b0d Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 03:06:44 +0100 Subject: [PATCH 35/42] Refactor BusShiftAdmin to improve save_formset method documentation --- padam_django/apps/transport/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padam_django/apps/transport/admin.py b/padam_django/apps/transport/admin.py index abda9b81..b87d5e4c 100644 --- a/padam_django/apps/transport/admin.py +++ b/padam_django/apps/transport/admin.py @@ -16,7 +16,6 @@ class BusShiftAdmin(admin.ModelAdmin): form = BusShiftForm inlines = [BusStopInline] list_display = ['bus', 'driver', 'status', 'start_time', 'end_time', 'duration'] - search_fields = ['bus__licence_plate', 'driver__user__username'] def save_formset(self, request, form, formset, change): """ From 7acab2841a4ef2061798b5d828f63226ccff6996 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 03:51:09 +0100 Subject: [PATCH 36/42] refactor: add transaction.atomic --- padam_django/apps/transport/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/padam_django/apps/transport/admin.py b/padam_django/apps/transport/admin.py index b87d5e4c..62a8b036 100644 --- a/padam_django/apps/transport/admin.py +++ b/padam_django/apps/transport/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.db import transaction from .bus_shift_service import update_shift_times from .forms import BusShiftForm @@ -17,6 +18,7 @@ class BusShiftAdmin(admin.ModelAdmin): inlines = [BusStopInline] list_display = ['bus', 'driver', 'status', 'start_time', 'end_time', 'duration'] + @transaction.atomic def save_formset(self, request, form, formset, change): """ Sauvegarde des inlines puis calcul automatique des temps du trajet. From b21fc9e3f7db8fa6182580f9924933eae5974824 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 03:51:38 +0100 Subject: [PATCH 37/42] refactor: add transaction.atomic and select_for_update in update_shift_times --- .../apps/transport/bus_shift_service.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/padam_django/apps/transport/bus_shift_service.py b/padam_django/apps/transport/bus_shift_service.py index 087c3acc..64ccb84a 100644 --- a/padam_django/apps/transport/bus_shift_service.py +++ b/padam_django/apps/transport/bus_shift_service.py @@ -1,5 +1,7 @@ +from django.db import transaction from django.core.exceptions import ValidationError from django.db.models import Q +from .models import BusShift def update_shift_times(shift): """ @@ -7,22 +9,29 @@ def update_shift_times(shift): après la sauvegarde des BusStop. """ stops = list(shift.stops.all()) - if len(stops) < 2: - raise ValidationError("Un trajet doit avoir au moins 2 arrêts.") + with transaction.atomic(): + # Lock du shift pour éviter modifications concurrentes + shift = BusShift.objects.select_for_update().get(pk=shift.pk) + if len(stops) < 2: + raise ValidationError("Un trajet doit avoir au moins 2 arrêts.") - # Tri par order - stops_sorted = sorted(stops, key=lambda s: s.order) - shift.start_time = stops_sorted[0].time - shift.end_time = stops_sorted[-1].time - shift.duration = shift.end_time - shift.start_time + # Tri par order + stops_sorted = sorted(stops, key=lambda s: s.order) + shift.start_time = stops_sorted[0].time + shift.end_time = stops_sorted[-1].time + shift.duration = shift.end_time - shift.start_time - # Vérification chevauchements bus/driver - overlapping = shift.__class__.objects.exclude(pk=shift.pk).filter( - Q(bus=shift.bus) | Q(driver=shift.driver), - start_time__lt=shift.end_time, - end_time__gt=shift.start_time, - ) - if overlapping.exists(): - raise ValidationError("Ce trajet chevauche un autre trajet existant.") + # Validation cohérente + if shift.end_time < shift.start_time: + raise ValidationError("La fin doit être après le début.") - shift.save() + # Vérification chevauchements bus/driver + overlapping = shift.__class__.objects.exclude(pk=shift.pk).filter( + Q(bus=shift.bus) | Q(driver=shift.driver), + start_time__lt=shift.end_time, + end_time__gt=shift.start_time, + ) + if overlapping.exists(): + raise ValidationError("Ce trajet chevauche un autre trajet existant.") + + shift.save() From 524da0a4c7408fb5011df81080dd6e4739f7ff91 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 04:10:11 +0100 Subject: [PATCH 38/42] Update : tests --- .../transport/tests/test_bus_shift_service.py | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/padam_django/apps/transport/tests/test_bus_shift_service.py b/padam_django/apps/transport/tests/test_bus_shift_service.py index 6d0b650b..8b81a439 100644 --- a/padam_django/apps/transport/tests/test_bus_shift_service.py +++ b/padam_django/apps/transport/tests/test_bus_shift_service.py @@ -1,12 +1,12 @@ from django.test import TestCase from django.core.exceptions import ValidationError -from datetime import datetime, timedelta +from datetime import timedelta +from django.utils import timezone from padam_django.apps.transport.models import BusShift, BusStop from padam_django.apps.fleet.models import Bus, Driver from padam_django.apps.geography.models import Place from padam_django.apps.users.models import User -from django.utils import timezone from ..bus_shift_service import update_shift_times @@ -35,14 +35,47 @@ def test_less_than_two_stops_raises_error(self): BusStop.objects.create( shift=shift, place=self.placeA, - time=datetime.now(), + time=timezone.now(), order=1 ) with self.assertRaises(ValidationError): update_shift_times(shift) + def test_correct_start_end_duration_with_inverted_order(self): + """ + Test que update_shift_times calcule correctement start/end/duration + avec des stops correctement ordonnés. + """ + shift = BusShift.objects.create(bus=self.bus, driver=self.driver) + now = timezone.now() # datetime aware + + stop1 = BusStop.objects.create( + shift=shift, + place=self.placeA, + time=now, + order=1 # premier stop + ) + + stop2 = BusStop.objects.create( + shift=shift, + place=self.placeB, + time=now + timedelta(minutes=30), + order=2 # deuxième stop + ) + + update_shift_times(shift) + shift.refresh_from_db() + + self.assertEqual(shift.start_time, stop1.time) + self.assertEqual(shift.end_time, stop2.time) + self.assertEqual(shift.duration, stop2.time - stop1.time) + + def test_correct_start_end_duration(self): + """ + Test standard avec stops correctement ordonnés. + """ shift = BusShift.objects.create(bus=self.bus, driver=self.driver) now = timezone.now() @@ -50,25 +83,28 @@ def test_correct_start_end_duration(self): shift=shift, place=self.placeA, time=now, - order=2 + order=1 ) stop2 = BusStop.objects.create( shift=shift, place=self.placeB, time=now + timedelta(minutes=30), - order=1 + order=2 ) update_shift_times(shift) shift.refresh_from_db() - self.assertEqual(shift.start_time, stop2.time) - self.assertEqual(shift.end_time, stop1.time) - self.assertEqual(shift.duration, stop1.time - stop2.time) + self.assertEqual(shift.start_time, stop1.time) + self.assertEqual(shift.end_time, stop2.time) + self.assertEqual(shift.duration, stop2.time - stop1.time) def test_overlapping_shift_raises_error(self): - now = datetime.now() + """ + Test pour vérifier que deux shifts qui se chevauchent déclenchent ValidationError. + """ + now = timezone.now() shift1 = BusShift.objects.create(bus=self.bus, driver=self.driver) BusStop.objects.create(shift=shift1, place=self.placeA, time=now, order=1) From 672677614b33131ec9310cc90bbea4968ea5d0e0 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 04:33:39 +0100 Subject: [PATCH 39/42] Feat: rm status filed and upply migrations --- padam_django/apps/transport/admin.py | 2 +- padam_django/apps/transport/forms.py | 2 +- .../migrations/0009_remove_busshift_status.py | 17 +++++++++++++++++ padam_django/apps/transport/models.py | 1 - 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 padam_django/apps/transport/migrations/0009_remove_busshift_status.py diff --git a/padam_django/apps/transport/admin.py b/padam_django/apps/transport/admin.py index 62a8b036..0654fd98 100644 --- a/padam_django/apps/transport/admin.py +++ b/padam_django/apps/transport/admin.py @@ -16,7 +16,7 @@ class BusStopInline(admin.TabularInline): class BusShiftAdmin(admin.ModelAdmin): form = BusShiftForm inlines = [BusStopInline] - list_display = ['bus', 'driver', 'status', 'start_time', 'end_time', 'duration'] + list_display = ['bus', 'driver', 'start_time', 'end_time', 'duration'] @transaction.atomic def save_formset(self, request, form, formset, change): diff --git a/padam_django/apps/transport/forms.py b/padam_django/apps/transport/forms.py index ad9364e5..33f63347 100644 --- a/padam_django/apps/transport/forms.py +++ b/padam_django/apps/transport/forms.py @@ -5,4 +5,4 @@ class BusShiftForm(forms.ModelForm): class Meta: model = BusShift - fields = ['bus', 'driver', 'status'] + fields = ['bus', 'driver'] diff --git a/padam_django/apps/transport/migrations/0009_remove_busshift_status.py b/padam_django/apps/transport/migrations/0009_remove_busshift_status.py new file mode 100644 index 00000000..8077a88b --- /dev/null +++ b/padam_django/apps/transport/migrations/0009_remove_busshift_status.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2025-12-10 03:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('transport', '0008_busstop_unique_stop_order_per_shift'), + ] + + operations = [ + migrations.RemoveField( + model_name='busshift', + name='status', + ), + ] diff --git a/padam_django/apps/transport/models.py b/padam_django/apps/transport/models.py index 8ccaac67..ac4f4fe9 100644 --- a/padam_django/apps/transport/models.py +++ b/padam_django/apps/transport/models.py @@ -3,7 +3,6 @@ class BusShift(models.Model): bus = models.ForeignKey('fleet.Bus', on_delete=models.PROTECT, related_name='shifts') driver = models.ForeignKey('fleet.Driver', on_delete=models.PROTECT, related_name='shifts') - status = models.CharField(max_length=20, default='Brouillon') start_time = models.DateTimeField(null=True, blank=True) end_time = models.DateTimeField(null=True, blank=True) duration = models.DurationField(null=True, blank=True) From 26f1e573cceac16ba8594b20ea7ad2c2429a7726 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 11:43:17 +0100 Subject: [PATCH 40/42] fix make target 'docker-user-test' --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 01148496..658e31e4 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ docker-assign-perms: ## Make non-driver users staff and assign BusShift & BusSto docker exec -it padam-web python manage.py update_non_drivers # Assign permissions to non-driver users inside the container -docker-user_test: ## +docker-user-test: ## docker exec -it padam-web python manage.py create_test_user # Run tests inside the container From 4d6abdd4df8e5f34d2d5545c8b3cbeb01a57b761 Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 15:13:40 +0100 Subject: [PATCH 41/42] fix(bus_shift_service): prevent zero-duration shifts --- padam_django/apps/transport/bus_shift_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padam_django/apps/transport/bus_shift_service.py b/padam_django/apps/transport/bus_shift_service.py index 64ccb84a..9492ae62 100644 --- a/padam_django/apps/transport/bus_shift_service.py +++ b/padam_django/apps/transport/bus_shift_service.py @@ -22,7 +22,7 @@ def update_shift_times(shift): shift.duration = shift.end_time - shift.start_time # Validation cohérente - if shift.end_time < shift.start_time: + if shift.end_time <= shift.start_time: raise ValidationError("La fin doit être après le début.") # Vérification chevauchements bus/driver From a97cac6497d2a266c9e41f08a927d8be159b211f Mon Sep 17 00:00:00 2001 From: TSAD01 Date: Wed, 10 Dec 2025 15:23:32 +0100 Subject: [PATCH 42/42] test(bus_shift): add test to prevent zero-duration shifts --- .../transport/tests/test_bus_shift_service.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/padam_django/apps/transport/tests/test_bus_shift_service.py b/padam_django/apps/transport/tests/test_bus_shift_service.py index 8b81a439..f40cf80f 100644 --- a/padam_django/apps/transport/tests/test_bus_shift_service.py +++ b/padam_django/apps/transport/tests/test_bus_shift_service.py @@ -120,3 +120,30 @@ def test_overlapping_shift_raises_error(self): # shift2 chevauche → doit lever une erreur with self.assertRaises(ValidationError): update_shift_times(shift2) + + def test_zero_duration_raises_error(self): + """ + Vérifie qu'un trajet avec deux arrêts au même moment + lève ValidationError (duration = 0 interdit). + """ + shift = BusShift.objects.create(bus=self.bus, driver=self.driver) + now = timezone.now() + + BusStop.objects.create( + shift=shift, + place=self.placeA, + time=now, + order=1 + ) + + BusStop.objects.create( + shift=shift, + place=self.placeB, + time=now, # même datetime + order=2 + ) + + with self.assertRaises(ValidationError) as cm: + update_shift_times(shift) + + self.assertIn("La fin doit être après le début", str(cm.exception)) \ No newline at end of file