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
5 changes: 4 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/padam-django-tech-test.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
run: ## Run the test server.
python manage.py runserver_plus

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

migrate: ## Migrate django models
python manage.py makemigrations
python manage.py migrate

run: ## Run the test server.
python manage.py runserver_plus
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ Pour réaliser le test, pensez à fork ce repository. Idéalement, ouvrir une PR
## 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
- Le projet a é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 @@ -33,15 +33,16 @@ make run
```

Des scripts sont à votre disposition pour vous permettre de rapidement créer de la donnée et de prendre le projet en
main:
main :

- `create_data`
- `create_buses`
- `create_drivers`
- `create_places`
- `create_users`
- `create_bus_stops`

Par exemple:
Par exemple :

```
python manage.py create_drivers -n 5
Expand All @@ -51,7 +52,7 @@ python manage.py create_drivers -n 5

### Description

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

- Un bus: (`Bus`).
- Un chauffeur: (`Driver`).
Expand All @@ -60,7 +61,7 @@ Un trajet en bus (`BusShift`) est composé des éléments suivants:
- 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:
La structure de projet qui vous est proposée comprend déjà les models suivants :
- `Bus`
- `Driver`
- `Place`
Expand All @@ -71,7 +72,7 @@ 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
doivent être respectées:
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.
Expand All @@ -82,12 +83,12 @@ doivent être respectées:
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 ...
**Note** : Il existe plusieurs solutions pour concevoir cette fonctionnalité. Certaines seront peut-être plus couteuse
en temps que d'autres

### Conseils

- 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 ;-))
- 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é.
- Soyez prêt à présenter le sujet, à justifier vos choix et à parler de comment vous auriez fait les parties que vous avez laissées de côté.
4 changes: 2 additions & 2 deletions padam_django/apps/common/management/commands/create_data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from django.core.management.base import BaseCommand

from django.core import management
from django.core.management.base import BaseCommand


class Command(BaseCommand):
Expand All @@ -12,3 +11,4 @@ def handle(self, *args, **options):
management.call_command('create_drivers', number=5)
management.call_command('create_buses', number=10)
management.call_command('create_places', number=30)
management.call_command('create_bus_stops', number=100)
5 changes: 5 additions & 0 deletions padam_django/apps/geography/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@
@admin.register(models.Place)
class PlaceAdmin(admin.ModelAdmin):
pass


@admin.register(models.BusStop)
class BusStopAdmin(admin.ModelAdmin):
pass
12 changes: 11 additions & 1 deletion padam_django/apps/geography/factories.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from datetime import time

import factory
from faker import Faker

from . import models


fake = Faker(['fr'])


Expand All @@ -15,3 +16,12 @@ class PlaceFactory(factory.django.DjangoModelFactory):

class Meta:
model = models.Place


class BusStopFactory(factory.django.DjangoModelFactory):
place = factory.SubFactory(PlaceFactory)
expected_arrival = factory.LazyFunction(
lambda: time(hour=fake.random_int(min=0, max=23), minute=fake.random_int(min=0, max=59)))

class Meta:
model = models.BusStop
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from padam_django.apps.common.management.base import CreateDataBaseCommand

from padam_django.apps.geography.factories import BusStopFactory


class Command(CreateDataBaseCommand):
help = 'Create few bus stops'

def handle(self, *args, **options):
super().handle(*args, **options)
self.stdout.write(f'Creating {self.number} bus stops ...')
BusStopFactory.create_batch(size=self.number)
25 changes: 25 additions & 0 deletions padam_django/apps/geography/migrations/0002_busstop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2024-11-10 14:24

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('geography', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='BusStop',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('expected_arrival', models.TimeField(verbose_name='Expected arrival time')),
('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geography.place')),
],
options={
'unique_together': {('place', 'expected_arrival')},
},
),
]
16 changes: 16 additions & 0 deletions padam_django/apps/geography/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,19 @@ class Meta:

def __str__(self):
return f"Place: {self.name} (id: {self.pk})"


class BusStop(models.Model):
"""
A bus stop is a Place where a bus is expected to stop at a given time.
"""
place = models.ForeignKey(Place, on_delete=models.CASCADE)
expected_arrival = models.TimeField("Expected arrival time")

class Meta:
# A bus stop cannot be duplicated.
# its either same time different place or same place different time
unique_together = (("place", "expected_arrival"),)

def __str__(self):
return f"BusStop: {self.place.name} at {self.expected_arrival}"
Empty file.
9 changes: 9 additions & 0 deletions padam_django/apps/schedules/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib import admin

from . import models

# Register your models here.
@admin.register(models.BusShift)
class PlaceAdmin(admin.ModelAdmin):
form = models.BusShiftForm
list_display = ['bus', 'driver', 'start_time', 'end_time', 'duration']
5 changes: 5 additions & 0 deletions padam_django/apps/schedules/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class SchedulesConfig(AppConfig):
name = 'padam_django.apps.schedules'
31 changes: 31 additions & 0 deletions padam_django/apps/schedules/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.2.5 on 2024-11-10 14:24

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
('fleet', '0002_auto_20211109_1456'),
('geography', '0002_busstop'),
]

operations = [
migrations.CreateModel(
name='BusShift',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.TimeField(verbose_name='Start time')),
('end_time', models.TimeField(verbose_name='End time')),
('bus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.bus')),
('bus_stops', models.ManyToManyField(to='geography.BusStop')),
('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.driver')),
],
options={
'unique_together': {('bus', 'driver', 'start_time')},
},
),
]
Empty file.
92 changes: 92 additions & 0 deletions padam_django/apps/schedules/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Model
from django.forms import ModelForm
from django import forms
from datetime import datetime

# Create your models here.
class BusShift(models.Model):
"""
A bus shift is a period of time during which a bus is driven by a driver and stops at least two bus stops.
Creating a bus shift is done by creating a BusShiftForm instance.
"""
bus = models.ForeignKey('fleet.Bus', on_delete=models.CASCADE)
driver = models.ForeignKey('fleet.Driver', on_delete=models.CASCADE)
bus_stops = models.ManyToManyField('geography.BusStop')
start_time = models.TimeField("Start time", blank=True, null=True)
end_time = models.TimeField("End time", blank=True, null=True)

@property
def duration(self):
start_time = datetime.combine(datetime.today(), self.start_time)
end_time = datetime.combine(datetime.today(), self.end_time)
return end_time - start_time

class Meta:
unique_together = (("bus", "driver", "start_time"), )

def __str__(self):
return (f"BusShift: {self.bus.licence_plate} driven by {self.driver.user.username} at {self.start_time}, duration: {self.duration}, with {self.bus_stops.count()} stops")

class BusShiftForm(ModelForm):
"""
A form for creating and updating a BusShift instance.
This has the responsibility of validating the form data and computing the start_time and end_time fields.
"""
class Meta:
model = BusShift
fields = '__all__'

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Hide start_time and end_time fields from the form but still compute them in the clean method.
self.fields['start_time'].widget = forms.HiddenInput()
self.fields['end_time'].widget = forms.HiddenInput()

def clean(self):
cleaned_data = super().clean()
driver = cleaned_data.get('driver')
bus = cleaned_data.get('bus')
bus_stops = cleaned_data.get('bus_stops')

self.check_bus_stops(bus_stops, cleaned_data)

self.check_overlapping_shifts(bus, cleaned_data, driver)

return cleaned_data

def check_overlapping_shifts(self, bus, cleaned_data, driver):
"""
Check if the driver or the bus is already scheduled for another shift at this time.
:param bus:
:param cleaned_data:
:param driver:
:return:
"""
overlapping_shifts = BusShift.objects.filter(
models.Q(driver=driver) | models.Q(bus=bus),
models.Q(start_time__lt=cleaned_data['end_time'], end_time__gt=cleaned_data['start_time'])
).exclude(pk=self.instance.pk)
if overlapping_shifts.exists():
raise ValidationError("The driver or the bus is already scheduled for another shift at this time.")

def check_bus_stops(self, bus_stops, cleaned_data):
"""
Check if the bus shift has at least two bus stops and sort them by expected arrival time.
This method alters the cleaned_data dictionary by sorting the bus stops and setting the start_time and end_time fields.
:param bus_stops:
:param cleaned_data:
:return:
"""
bus_stops_count = bus_stops.count()

if bus_stops_count < 2:
raise ValidationError("A bus shift must have at least two bus stops.")
# Sort and remove duplicates
cleaned_data['bus_stops'] = sorted(list(set(bus_stops)), key=lambda x: x.expected_arrival)
last_item = bus_stops_count - 1
cleaned_data['start_time'] = cleaned_data['bus_stops'][0].expected_arrival
cleaned_data['end_time'] = cleaned_data['bus_stops'][last_item].expected_arrival

Loading