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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
run: ## Run the test server.
python manage.py runserver_plus

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

install: ## Install the python requirements.
pip install -r requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,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_busshifts', number=5)
Empty file.
37 changes: 37 additions & 0 deletions padam_django/apps/route/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.contrib import admin

from . import forms
from .models import BusShift, BusStop


@admin.register(BusStop)
class BusStopAdmin(admin.ModelAdmin):
"""
Django Admin configuration for the `BusStop` model.

The admin page displays a list of all the available bus stops, including their place name
and transit time.
"""
list_display = ('place', 'transit_time')


@admin.register(BusShift)
class BusShiftAdmin(admin.ModelAdmin):
"""
Django Admin configuration for the `BusShift` model.

The admin page allows users to create, view, update, and delete instances of the `BusShift` model.
The form used to create or update instances of the `BusShift` model is defined by the `BusShiftForm`
class in the `forms` module.

The list of `BusShift` instances includes the bus and driver assigned to each shift, as well as the
departure time, arrival time, and duration of the shift. The `BusShift` form includes fields to
specify the bus and driver, as well as a many-to-many relationship to the `BusStop` model to specify
the stops on the route.

The `departure_time`, `arrival_time`, and `shift_duration` fields are automatically calculated based
on the stops included in the shift.
"""
form = forms.BusShiftForm
list_display = ('bus', 'driver', 'departure_time', 'arrival_time', 'shift_duration')
fields = ('bus', 'driver', 'stops')
6 changes: 6 additions & 0 deletions padam_django/apps/route/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class RouteConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'padam_django.apps.route'
41 changes: 41 additions & 0 deletions padam_django/apps/route/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import random
from datetime import timedelta

import factory
from django.utils import timezone

from ..fleet.factories import BusFactory, DriverFactory
from ..geography.factories import PlaceFactory
from .models import BusShift, BusStop


class BusStopFactory(factory.django.DjangoModelFactory):
"""
A factory for creating BusStop instances.

Attributes:
place (factory): A subfactory that creates Place instances.
transit_time (factory): A Faker that generates a random date_time value.
"""
place = factory.SubFactory(PlaceFactory)
transit_time = factory.LazyFunction(
lambda: timezone.now() + timedelta(days=random.randint(-30, 30), hours=random.randint(0, 23))
)

class Meta:
model = BusStop


class BusShiftFactory(factory.django.DjangoModelFactory):
"""
A factory for creating BusShift instances.

Attributes:
bus (factory): A subfactory that creates Bus instances.
driver (factory): A subfactory that creates Driver instances.
"""
class Meta:
model = BusShift

bus = factory.SubFactory(BusFactory)
driver = factory.SubFactory(DriverFactory)
105 changes: 105 additions & 0 deletions padam_django/apps/route/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from datetime import datetime

from django import forms
from django.core.exceptions import ValidationError
from django.db.models.query import QuerySet

from ..fleet.models import Bus, Driver
from .models import BusShift


class BusShiftForm(forms.ModelForm):

class Meta:
"""Meta class to define the model and fields used in the form."""
model = BusShift
fields = ('bus', 'driver', 'stops')

def clean(self) -> None:
"""
Clean method to validate the form data.

Validates the following fields:
- bus: Check if the bus is available during the selected time frame.
- driver: Check if the driver is available during the selected time frame.
- stops: Check if there are at least two bus stops.

Raises a ValidationError if any of the validations fail.
"""
cleaned_data = super().clean()
bus = cleaned_data.get('bus')
stops = cleaned_data.get('stops')
driver = cleaned_data.get('driver')
if stops:
departure_time = stops.order_by('transit_time').first().transit_time
arrival_time = stops.order_by('transit_time').last().transit_time

self._check_two_stops_at_least(stops)
self._check_bus_availability(departure_time, arrival_time, bus)
self._check_driver_availability(departure_time, arrival_time, driver)

def _check_bus_availability(self, departure_time: datetime, arrival_time: datetime, bus: Bus) -> None:
"""
Check if the given bus is available during the given time range.

Args:
departure_time (datetime): The departure time of the bus shift.
arrival_time (datetime): The arrival time of the bus shift.
bus (Bus): The bus object to check availability for.

Raises:
ValidationError: If the given bus is already in use during the given time range.
"""
shifts = BusShift.objects.filter(bus__pk=bus.pk)
if not self._is_available(shifts, departure_time, arrival_time):
raise ValidationError('This bus is already booked for a shift')

def _check_driver_availability(self, departure_time: datetime, arrival_time: datetime, driver: Driver) -> None:
"""
Check if the selected driver is available during the specified shift time.

Args:
departure_time (datetime): The departure time of the bus shift.
arrival_time (datetime): The arrival time of the bus shift.
driver (Driver): The Driver object representing the selected driver for the bus shift.

Raises:
ValidationError: If the driver is already assigned to another shift during the specified time period.
"""
shifts = BusShift.objects.filter(driver__pk=driver.pk)
if not self._is_available(shifts, departure_time, arrival_time):
raise ValidationError('This driver is already on a shift')

@staticmethod
def _is_available(shifts: QuerySet, departure_time: datetime, arrival_time: datetime) -> bool:
"""
Check if there is no overlap between the given shifts and the time range defined by the
departure and arrival times.

Args:
shifts (QuerySet): A queryset of BusShift instances to compare against.
departure_time (datetime): The departure time of the new shift to be created.
arrival_time (datetime): The arrival time of the new shift to be created.

Returns:
bool: True if there is no overlap between the shifts and the given time range, False otherwise.
"""
return not any(
(shift.departure_time <= departure_time <= shift.arrival_time) or
(shift.departure_time <= arrival_time <= shift.arrival_time) or
(shift.departure_time >= departure_time and shift.arrival_time <= arrival_time)
for shift in shifts
)

@staticmethod
def _check_two_stops_at_least(stops: QuerySet) -> None:
"""Validate that at least two bus stops are provided.

Args:
stops (QuerySet): The set of bus stops included in the shift.

Raises:
ValidationError: Raised if less than two bus stops are included in the shift.
"""
if stops.count() < 2:
raise ValidationError('At least two stops are required')
29 changes: 29 additions & 0 deletions padam_django/apps/route/management/commands/create_busshifts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from padam_django.apps.common.management.base import CreateDataBaseCommand
from padam_django.apps.route.factories import BusShiftFactory, BusStopFactory


class Command(CreateDataBaseCommand):
"""
Create bus shifts with BusStops using the CreateDataBaseCommand.

This command creates a specified number of bus shifts with two BusStops each.
The number of bus shifts to be created can be specified using the '--number' option.
"""

help = 'Create bus shifts with BusStops'

def handle(self, *args, **options):
"""
Handle the execution of the command.

This method creates the specified number of bus shifts with two BusStops each,
using the CreateDataBaseCommand's handle method. It also prints the progress
and completion messages to the console.
"""
super().handle(*args, **options)
for _ in range(self.number):
stops = BusStopFactory.create_batch(2)
shift = BusShiftFactory()
shift.stops.set(stops)
shift.save()
self.stdout.write(f'Created BusShift {shift.pk} with {len(stops)} stops')
34 changes: 34 additions & 0 deletions padam_django/apps/route/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 2023-04-03 12:18

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')),
('transit_time', models.DateTimeField()),
('place', models.ForeignKey(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.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.bus')),
('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.driver')),
('stops', models.ManyToManyField(blank=True, related_name='bus_shifts', to='route.BusStop')),
],
),
]
Empty file.
50 changes: 50 additions & 0 deletions padam_django/apps/route/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from datetime import datetime
from typing import Optional

from django.db import models


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

def __str__(self) -> str:
"""Returns a string representation of a BusStop instance."""
transit_time_str = self.transit_time.strftime('%m/%d/%Y %H:%M')
return f"Bus Stop: {self.place.name} - {transit_time_str} (id: {self.pk})"


class BusShift(models.Model):
"""A model representing a bus shift."""
bus = models.ForeignKey('fleet.Bus', on_delete=models.CASCADE, null=False, blank=False)
driver = models.ForeignKey('fleet.Driver', on_delete=models.CASCADE, null=False, blank=False,)
stops = models.ManyToManyField('BusStop', related_name='bus_shifts', blank=True)

@property
def departure_time(self) -> Optional[datetime]:
"""Returns the departure time of the bus shift."""
if self.stops.exists():
return self.stops.all().order_by('transit_time').first().transit_time
else:
return None

@property
def arrival_time(self) -> Optional[datetime]:
"""Returns the arrival time of the bus shift."""
if self.stops.exists():
return self.stops.all().order_by('transit_time').last().transit_time
else:
return None

@property
def shift_duration(self) -> Optional[datetime]:
"""Returns the duration of the bus shift."""
if self.stops.exists():
return self.arrival_time - self.departure_time
else:
return None

def __str__(self) -> str:
"""Returns a string representation of a BusShift instance."""
return f'Bus Shift: Bus:{self.bus.licence_plate}, Driver: {self.driver.user.username} (id: {self.pk})'
Loading