Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.9.19
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@ run: ## Run the test server.

install: ## Install the python requirements.
pip install -r requirements.txt

migrate: ## Migrate Django models
python manage.py migrate

user: ## Create a super-user to access Django-admin
python manage.py createsuperuser

populate: ## Populate data
python manage.py create_places -n 10
python manage.py create_buses -n 5
python manage.py create_drivers -n 5
python manage.py create_bus_shifts -n 10
python manage.py create_bus_stops -n 20

clear: ## Clear database
python manage.py flush --noinput
42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
# DB Diagram

https://dbdiagram.io/d/django-test-69505ef039fa3db27ba5a9b9

# To do list

- OK 1 BusShift === 1 Bus
- OK 1 BusShift === 1 Driver
- OK 1 BusShift === N ordered BusStop (N>=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.
Expand All @@ -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.

Expand All @@ -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`
Expand Down Expand Up @@ -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.

Expand All @@ -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...



Expand All @@ -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.
Expand All @@ -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.

Expand All @@ -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`
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down
18 changes: 18 additions & 0 deletions padam_django/apps/fleet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
24 changes: 24 additions & 0 deletions padam_django/apps/fleet/factories.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import factory
from datetime import timedelta
from django.utils import timezone
from faker import Faker

from . import models
Expand All @@ -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
13 changes: 13 additions & 0 deletions padam_django/apps/fleet/management/commands/create_bus_shifts.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions padam_django/apps/fleet/management/commands/create_bus_stops.py
Original file line number Diff line number Diff line change
@@ -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)
38 changes: 38 additions & 0 deletions padam_django/apps/fleet/migrations/0003_busshift_busstop.py
Original file line number Diff line number Diff line change
@@ -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'],
},
),
]
64 changes: 64 additions & 0 deletions padam_django/apps/fleet/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.db import models


Expand All @@ -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})"
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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