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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ __pycache__/

# virtualenv
venv/
.venv*/
ENV/
# pipenv: https://github.com/kennethreitz/pipenv
/Pipfile
Expand All @@ -20,3 +21,6 @@ ENV/
# Editors stuff
.idea
.vscode

# Secrets
secrets.json
4 changes: 2 additions & 2 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'padam_django.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "padam_django.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
Expand All @@ -18,5 +18,5 @@ def main():
execute_from_command_line(sys.argv)


if __name__ == '__main__':
if __name__ == "__main__":
main()
Empty file.
97 changes: 97 additions & 0 deletions padam_django/apps/shifts/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from django.contrib import admin
from django import forms
from django.core.exceptions import ValidationError

from . import models


@admin.register(models.BusStop)
class BusStopAdmin(admin.ModelAdmin):
pass


class BusShiftAdminForm(forms.ModelForm):
class Meta:
model = models.BusShift
fields = "__all__"

#TODO: Form filtering to avoid seeing BusStops from all BusShifts

def clean(self):
"""Validate that a single Bus or Driver cannot be assigned at
the same time to several BusShifts.

Raises:
ValidationError: Field/description dict containing
the violation error(s).
"""
validation_errors = {}

# Check that BusShift has at least 2 stops
stops: list[models.BusStop] = self.cleaned_data.get("stops", [])
stops_count = len(stops)
if stops_count < 2:
validation_errors["stops"] = (
"BusShift must have at least two BusStops "
f"({stops_count} found)."
)

# If no bus and no driver is set, no possible conflict
if (
self.cleaned_data.get("bus") is None
and self.cleaned_data.get("driver") is None
):
return
# Check that BusShift is not overlapping with another shift
for shift in models.BusShift.objects.all():
if shift.pk == self.instance.pk or (
shift.bus != self.cleaned_data.get("bus")
and shift.driver != self.cleaned_data.get("driver")
):
# It's the same shift, or both the bus and the driver differ
continue
if (
shift.last_stop.date_time
>= self.cleaned_data.get("stops").order_by("date_time").first().date_time
and shift.first_stop.date_time
<= self.cleaned_data.get("stops").order_by("date_time").last().date_time
):
if shift.bus == self.cleaned_data.get("bus"):
validation_errors["bus"] = (
"Bus is not available at this time (conflicts with "
f"'{shift}')."
)
if shift.driver == self.cleaned_data.get("driver"):
validation_errors["driver"] = (
f"{shift.driver} is not available at this time "
f"(conflicts with '{shift}')."
)

if validation_errors:
raise ValidationError(validation_errors)

return self.cleaned_data


@admin.register(models.BusShift)
class BusShiftAdmin(admin.ModelAdmin):
# TODO: ordering (by first stop date_time)

list_display = (
"pk",
"first_stop_date_time",
"bus",
"driver",
"stops_count",
"first_stop",
"last_stop",
# "is_valid", # Not implemented yet
)

# def is_valid(self, obj):
# return obj.is_valid

# is_valid.boolean = True
# is_valid.short_description = "Is valid"

form = BusShiftAdminForm
5 changes: 5 additions & 0 deletions padam_django/apps/shifts/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class ShiftsConfig(AppConfig):
name = 'padam_django.apps.shifts'
34 changes: 34 additions & 0 deletions padam_django/apps/shifts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.5 on 2024-11-16 09:50

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


class Migration(migrations.Migration):

initial = True

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

operations = [
migrations.CreateModel(
name='BusStop',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()),
('place', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='geography.place')),
],
),
migrations.CreateModel(
name='BusShift',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bus', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fleet.bus')),
('driver', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fleet.driver')),
('stops', models.ManyToManyField(to='shifts.BusStop')),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2024-11-16 09:56

from django.db import migrations


class Migration(migrations.Migration):

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

operations = [
migrations.RenameField(
model_name='busstop',
old_name='time',
new_name='date_time',
),
]
32 changes: 32 additions & 0 deletions padam_django/apps/shifts/migrations/0003_auto_20241116_1157.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 3.2.5 on 2024-11-16 10:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('fleet', '0002_auto_20211109_1456'),
('shifts', '0002_rename_time_busstop_date_time'),
]

operations = [
migrations.RemoveField(
model_name='busshift',
name='bus',
),
migrations.AddField(
model_name='busshift',
name='bus',
field=models.ManyToManyField(to='fleet.Bus'),
),
migrations.RemoveField(
model_name='busshift',
name='driver',
),
migrations.AddField(
model_name='busshift',
name='driver',
field=models.ManyToManyField(to='fleet.Driver'),
),
]
33 changes: 33 additions & 0 deletions padam_django/apps/shifts/migrations/0004_auto_20241116_1200.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 3.2.5 on 2024-11-16 11:00

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


class Migration(migrations.Migration):

dependencies = [
('fleet', '0002_auto_20211109_1456'),
('shifts', '0003_auto_20241116_1157'),
]

operations = [
migrations.RemoveField(
model_name='busshift',
name='bus',
),
migrations.AddField(
model_name='busshift',
name='bus',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='fleet.bus'),
),
migrations.RemoveField(
model_name='busshift',
name='driver',
),
migrations.AddField(
model_name='busshift',
name='driver',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='fleet.driver'),
),
]
25 changes: 25 additions & 0 deletions padam_django/apps/shifts/migrations/0005_auto_20241116_1211.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2024-11-16 11:11

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


class Migration(migrations.Migration):

dependencies = [
('fleet', '0002_auto_20211109_1456'),
('shifts', '0004_auto_20241116_1200'),
]

operations = [
migrations.AlterField(
model_name='busshift',
name='bus',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fleet.bus'),
),
migrations.AlterField(
model_name='busshift',
name='driver',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fleet.driver'),
),
]
Empty file.
98 changes: 98 additions & 0 deletions padam_django/apps/shifts/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from typing import Iterable, Union
from django.db import models
from django.core.exceptions import ValidationError
from datetime import datetime


def fmt_date_time(date_time: datetime):
return date_time.strftime("%d/%m/%Y à %H:%M")


class BusStop(models.Model):
place = models.OneToOneField("geography.Place", on_delete=models.CASCADE)
date_time = models.DateTimeField()

def __str__(self):
# TODO: Format date time to be human-readable
return (
f"BusStop: '{self.place.name}' [{fmt_date_time(self.date_time)}] "
f"(id: {self.pk})"
)


class BusShift(models.Model):
bus = models.ForeignKey(
"fleet.Bus", null=True, blank=True, on_delete=models.CASCADE
)
driver = models.ForeignKey(
"fleet.Driver", null=True, blank=True, on_delete=models.CASCADE
)
# TODO: Order by date_time to avoid doing so in other places
stops = models.ManyToManyField(BusStop)

@property
def first_stop(self):
return self.stops.all().order_by("date_time").first()

@property
def first_stop_date_time(self):
return self.first_stop.date_time if self.first_stop else None

@property
def last_stop(self):
return self.stops.all().order_by("date_time").last()

@property
def stops_count(self):
return len(self.stops.all())

def validate(self):
# TODO: Mutualize validation from BusShiftAdminForm + add check that both Bus and Driver are set
# Skip validation if the object is not saved yet (no primary key)
if not self.pk:
return

@property
def is_valid(self) -> bool:
"""Check that the BusShift is fully ready (does not violate
constraints and has both a Driver and a Bus assigned to.

Returns:
bool: Validity status
"""
try:
self.validate()
except ValidationError:
return False
return True

def clean(self) -> None:
try:
self.validate()
except ValidationError as e:
raise (e)

def save(
self,
force_insert: bool = False,
force_update: bool = False,
using: Union[str, None] = None,
update_fields: Union[Iterable[str], None] = None,
) -> None:
# Calling clean to validate data before saving
self.full_clean()
return super().save(force_insert, force_update, using, update_fields)

def __str__(self):
bus_license_plate = self.bus.licence_plate if self.bus else "<NO BUS>"
driver_first_name = (
self.driver.user.first_name if self.driver else "<NO DRIVER>"
)
driver_last_name = self.driver.user.last_name if self.driver else ""
return (
f"BusShift: {bus_license_plate} "
f"[{fmt_date_time(self.first_stop.date_time)} - "
f"{fmt_date_time(self.last_stop.date_time)}] "
f"conducted by {driver_first_name} {driver_last_name} "
f"(id: {self.pk})"
)
4 changes: 4 additions & 0 deletions padam_django/secrets_example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"SECRET_KEY": "",
"DB_PASSWORD": ""
}
Loading