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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ ENV/
# Editors stuff
.idea
.vscode

myenv
Empty file.
13 changes: 13 additions & 0 deletions padam_django/apps/trips/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.contrib import admin
from .models import BusStop, BusShift
from .forms import BusShiftForm


class BusShiftAdmin(admin.ModelAdmin):
form = BusShiftForm
list_display = ["bus", "driver", "start_time", "end_time", "duration"]
search_fields = ["bus__licence_plate", "driver__user__username"]


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


class TripsConfig(AppConfig):
name = "padam_django.apps.trips"
91 changes: 91 additions & 0 deletions padam_django/apps/trips/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q
from .models import BusShift


class BusShiftForm(forms.ModelForm):
"""
A Django form for creating or updating `BusShift` instances, which represent
individual bus shifts that must contain a unique sequence of at least two bus stops.

This form performs validation on the set of bus stops provided to ensure:
- No duplicate stops are included within the shift.
- At least two unique bus stops are specified for a valid shift.

Attributes:
model: The model associated with this form, set to `BusShift`.
fields: Specifies that all fields in `BusShift` should be included in the form.
"""

class Meta:
model = BusShift
fields = "__all__"

def clean(self):
"""
Override the clean method to perform custom validation on bus stops.

This method checks:
- No duplicate bus stops are included in the shift.
- At least two unique bus stops are specified.

Raises:
ValidationError: If duplicate stops are found or fewer than two unique stops are provided.

Returns:
cleaned_data: The validated form data.
"""
cleaned_data = super().clean()
stops = self.cleaned_data.get("stops")

if stops:
unique_stops = set()
for stop in stops:
unique_stops.add(stop)
Comment on lines +43 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unique_stops = set(stops.values())


if len(unique_stops) < 2:
raise ValidationError("At least two bus stops are required.")

self._calculate_shift_times(unique_stops)
self._validate_unique_shift()
return cleaned_data

def _calculate_shift_times(self, unique_stops):
"""
Calculate start_time, end_time, and duration based on unique bus stops.
Assigns None values if no stops are assigned, and duration as zero.
"""
if unique_stops:
sorted_stops = sorted(unique_stops, key=lambda x: x.stop_time)
first_stop = sorted_stops[0]
last_stop = sorted_stops[-1]

self.instance.start_time = first_stop.stop_time
self.instance.end_time = last_stop.stop_time
else:
self.instance.start_time = self.instance.end_time = None

def _validate_unique_shift(self):
"""
Ensure there are no overlapping shifts for the same bus or driver.

Raises:
ValidationError: If any overlap exists with another shift's start or end times.
"""

if not self.instance.start_time or not self.instance.end_time:
return

bus = self.cleaned_data.get("bus")
driver = self.cleaned_data.get("driver")
if bus and driver and self.instance.start_time and self.instance.end_time:
overlapping_shifts = BusShift.objects.exclude(pk=self.instance.pk).filter(
Q(bus=bus) | Q(driver=driver),
Q(start_time__lt=self.instance.end_time),
Q(end_time__gt=self.instance.start_time),
)
if overlapping_shifts.exists():
raise ValidationError(
"This shift overlaps with an existing shift for the same bus or driver."
)
45 changes: 45 additions & 0 deletions padam_django/apps/trips/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 3.2.5 on 2024-11-04 11: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', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='BusStop',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('stop_time', models.DateTimeField(verbose_name='Stop Time')),
('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bus_stops', to='geography.place')),
],
options={
'ordering': ['stop_time'],
},
),
migrations.CreateModel(
name='BusShift',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('duration', models.DurationField(blank=True, editable=False, null=True, verbose_name='Duration')),
('start_time', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Start Time')),
('end_time', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='End Time')),
('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')),
('stops', models.ManyToManyField(to='trips.BusStop')),
],
),
migrations.AddConstraint(
model_name='busstop',
constraint=models.UniqueConstraint(fields=('name', 'place', 'stop_time'), name='unique_place_stop_time'),
),
]
Empty file.
74 changes: 74 additions & 0 deletions padam_django/apps/trips/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from django.db import models


class BusStop(models.Model):
"""
Represents a bus stop with a specific place, stop time, and unique identifier (name).

Attributes:
name (str): The name of the bus stop.
place (ForeignKey): The geographical location of the stop.
stop_time (DateTimeField): The scheduled stop time at this place.

Meta:
constraints: Ensures that a unique combination of name, place, and stop_time exists.
ordering: Orders bus stops by their stop time by default.
"""

name = models.CharField(max_length=100)
place = models.ForeignKey(
"geography.Place", on_delete=models.CASCADE, related_name="bus_stops"
)
stop_time = models.DateTimeField("Stop Time")

class Meta:
constraints = [
models.UniqueConstraint(
fields=["name", "place", "stop_time"],
name="unique_place_stop_time",
)
]
ordering = ["stop_time"]

def __str__(self):
return f"{self.place} at {self.stop_time}"


class BusShift(models.Model):
"""
Represents a shift for a specific bus and driver with assigned stops, start and end times, and a duration.

Attributes:
bus (ForeignKey): The bus assigned to this shift.
driver (ForeignKey): The driver assigned to this shift.
stops (ManyToManyField): The sequence of bus stops for the shift.
start_time (DateTimeField): Calculated start time of the shift.
end_time (DateTimeField): Calculated end time of the shift.
"""

bus = models.ForeignKey(
"fleet.Bus", on_delete=models.CASCADE, related_name="shifts"
)
driver = models.ForeignKey(
"fleet.Driver", on_delete=models.CASCADE, related_name="shifts"
)
stops = models.ManyToManyField("BusStop")
start_time = models.DateTimeField(
"Start Time", null=True, blank=True, editable=False
)
end_time = models.DateTimeField("End Time", null=True, blank=True, editable=False)

@property
def duration(self):
"""
Calculate the duration of the shift based on the start and end times.

Returns:
timedelta: The duration of the shift if both start_time and end_time are defined.
None: If either start_time or end_time is None, duration cannot be calculated.
"""
if self.end_time and self.start_time:
return self.end_time - self.start_time

def __str__(self):
return f"Bus shift with Bus {self.bus} and driver {self.driver}"
Empty file.
Loading