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
90 changes: 8 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,19 @@
# 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
concevoir une interface simple de gestion de trajets de bus, en utilisant l'admin de Django.
### Tester l'interface de gestion des trajets de bus

Pour réaliser le test, pensez à fork ce repository. Idéalement, ouvrir une PR à la fin.

## Critères d'évaluation

- Documentation et clarté du code
- Modélisation de la base de donnée
- Maîtrise du framework Django

## Stack Technique

| Nom | Version |
| ------ | ------- |
| Python | 3.7 |
| Django | 3.2.5 |

- 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.

### Démarrer le projet

*Depuis votre virtualenv Python 3.7*:
Une fois les migrations faites, il suffit de créer un super utilisateur pour pouvoir se connecter à l'admin Django:

```
make install
make migrate
make run
python manage.py createsuperuser
```

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`
- `create_buses`
- `create_drivers`
- `create_places`
- `create_users`

Par exemple:
Puis, il faut créer les données de base :

```
python manage.py create_drivers -n 5
python manage.py create_data
```

## Sujet

### Description

Un trajet en bus (`BusShift`) est composé des éléments suivants:

- Un bus: (`Bus`).
- Un chauffeur: (`Driver`).
- Entre 2 et une infinité d'arrêts (`BusStop`).
- L'heure de départ est déterminée par l'heure de passage au premier arrêt.
- L'heure d'arrivée est déterminée par l'heure de passage au dernier arrêt.
- Il est possible de déduire le temps total nécessaire pour effectuer le trajet depuis l'heure de départ et l'heure d'arrivée.

La structure de projet qui vous est proposée comprends déjà les models suivants:
- `Bus`
- `Driver`
- `Place`
- `User` (étends le model [AbstractUser de Django](https://docs.djangoproject.com/en/3.2/topics/auth/customizing/#substituting-a-custom-user-model))

### Objectifs

#### 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
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
chevaucheraient.
- Il en va de même pour les chauffeurs.

#### Fournir une interface de gestion des trajets de bus

Il doit être possible, pour un utilisateur, de créer ou de modifier des trajets de bus (`BusShift`) en utilisant l'admin
de django.

**Note**: Il existe plusieurs solutions pour concevoir cette fonctionnalité. Certaines seront peut être plus couteuse
en temps que d'autres ...

### Conseils
- Et enfin, il faut lancer le serveur.
- Si vous voulez, vous pouvez créer des bus stop directement dans l'interface de création des bus shifts.

- Ne passez pas plus de 4 heures sur un sujet (le but est d'évaluer vos compétences, pas de réduire votre temps libre à néant ;-))
- Privilégier la qualité et les bonnes pratiques.
- Vous pouvez réduire le périmètre du projet si vous manquez de temps. Une ébauche de réponse est déjà une bonne chose.
- Soyez prêt à présenter le sujet, à justifier vos choix et à parler de comment vous auriez fait les parties que vous avez laisser de côté.
**Note** : J'ai laissé de côté les tests unitaires.
Empty file.
19 changes: 19 additions & 0 deletions padam_django/apps/shifts/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.contrib import admin
from .models import BusShift, BusStop
from .modelform import BusShiftForm


class BusStopAdmin(admin.ModelAdmin):
""" Class to manage the BusStop model through Django admin """
list_display = ('place', 'planned_time')



class BusShiftAdmin(admin.ModelAdmin):
""" Class to manage the BusShift model through Django admin """
form = BusShiftForm
list_display = ('bus', 'driver', 'start_time', 'end_time', 'total_duration_seconds')


admin.site.register(BusShift, BusShiftAdmin)
admin.site.register(BusStop, BusStopAdmin)
7 changes: 7 additions & 0 deletions padam_django/apps/shifts/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class ShiftsConfig(AppConfig):
""" Configuration class for the shifts app """
default_auto_field = 'django.db.models.BigAutoField'
name = 'padam_django.apps.shifts'
40 changes: 40 additions & 0 deletions padam_django/apps/shifts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 3.2.5 on 2024-10-31 00:12

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='BusStop',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('planned_time', models.DateTimeField()),
('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bus_stops', to='geography.place')),
],
options={
'ordering': ['planned_time'],
'unique_together': {('place', 'planned_time')},
},
),
migrations.CreateModel(
name='BusShift',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.DateTimeField(blank=True, null=True)),
('end_time', models.DateTimeField(blank=True, null=True)),
('bus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shifts', to='fleet.bus')),
('bus_stops', models.ManyToManyField(related_name='bus_shifts', to='shifts.BusStop')),
('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bus_shifts', to='fleet.driver')),
],
),
]
Empty file.
59 changes: 59 additions & 0 deletions padam_django/apps/shifts/modelform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django import forms
from padam_django.apps.shifts.models import BusShift

class BusShiftForm(forms.ModelForm):
""" Class to manage the form for the Shift model through Django admin """
class Meta:
model = BusShift
fields = ['bus', 'driver', 'bus_stops']


def clean(self):
""" Method to validate the form data based on all criteria """
cleaned_data = super().clean()
bus_stops = cleaned_data.get('bus_stops')

if bus_stops and bus_stops.count() < 2:
raise forms.ValidationError("A shift must have at least 2 bus stops.")

if bus_stops:
start_time = bus_stops.order_by('planned_time').first().planned_time
end_time = bus_stops.order_by('planned_time').last().planned_time
else:
return cleaned_data

cleaned_data['start_time'] = start_time
cleaned_data['end_time'] = end_time

if start_time and end_time:
overlapping_bus = BusShift.objects.filter(
bus=self.cleaned_data.get('bus'),
start_time__lt=cleaned_data['end_time'],
end_time__gt=cleaned_data['start_time']
).exclude(id=self.instance.pk)

if overlapping_bus.exists():
raise forms.ValidationError("This shift overlaps with another shift for the same bus.")

overlapping_driver = BusShift.objects.filter(
driver=self.cleaned_data.get('driver'),
start_time__lt=cleaned_data['end_time'],
end_time__gt=cleaned_data['start_time']
).exclude(id=self.instance.pk)

if overlapping_driver.exists():
raise forms.ValidationError("This shift overlaps with another shift for the same driver.")

return cleaned_data

def save(self, commit=True):
""" Save the bus shift instance """
bus_shift = super().save(commit=False)
bus_shift.start_time = self.cleaned_data['start_time']
bus_shift.end_time = self.cleaned_data['end_time']

if commit:
bus_shift.save()
bus_shift.bus_stops.set(self.cleaned_data['bus_stops'])

return bus_shift
68 changes: 68 additions & 0 deletions padam_django/apps/shifts/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from django.db import models



class BusStop(models.Model):
""" Model representing a bus stop. """
place = models.ForeignKey('geography.Place', on_delete=models.CASCADE, related_name='bus_stops')
planned_time = models.DateTimeField(auto_now=False, auto_now_add=False)

class Meta:
ordering = ['planned_time']
# Two Bus stops can't have the same planned time and place
unique_together = ['place', 'planned_time']

def __str__(self):
""" String for representing the BusStop instance. """
planned_time = self.planned_time.strftime("%B %d, %Y, %I:%M %p")
return f"{self.place.name.upper()} at {planned_time}"


class BusShift(models.Model):
""" Model representing a bus shift. """
bus = models.ForeignKey('fleet.Bus', on_delete=models.CASCADE,null=False, blank=False, related_name='shifts')
driver = models.ForeignKey('fleet.Driver', on_delete=models.CASCADE, null=False,blank=False,related_name='bus_shifts')
bus_stops = models.ManyToManyField(BusStop, blank=False, related_name='bus_shifts')
start_time = models.DateTimeField(auto_now=False, auto_now_add=False, null=True, blank=True)
end_time = models.DateTimeField(auto_now=False, auto_now_add=False, null=True, blank=True)

def __str__(self):
""" String representation of the BusShift instance. """
return f"Bus Shift: {self.bus} with driver {self.driver} from {self.start_time} to {self.end_time}"

def calculate_start_time(self):
"""Returns the start time of the shift, which is the time of the first bus stop."""
if not self.pk:
return

ordered_stops = self.bus_stops.order_by('planned_time')
if ordered_stops.exists():
self.start_time = ordered_stops.first().planned_time

def calculate_end_time(self):
"""" Returns the end time of the shift which is the time of the last bus stop. """
if not self.pk:
return
ordered_stops = self.bus_stops.order_by('planned_time')
if ordered_stops.exists():
self.end_time = ordered_stops.last().planned_time


@property
def total_duration_seconds(self):
"""Calculates the total duration (in seconds) based on start and end times of the shift."""
if self.start_time and self.end_time:
time_difference = self.end_time - self.start_time
return time_difference.total_seconds()
return

def save(self, *args, **kwargs):
""" Save the BusShift instance """
super().save(*args, **kwargs)
if self.bus_stops.exists():
self.calculate_start_time()
self.calculate_end_time()
super().save(update_fields=['start_time', 'end_time']) # Sauvegarder uniquement les champs modifiés



1 change: 1 addition & 0 deletions padam_django/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'padam_django.apps.fleet',
'padam_django.apps.geography',
'padam_django.apps.users',
'padam_django.apps.shifts',
]

MIDDLEWARE = [
Expand Down