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
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@ run: ## Run the test server.

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

migrate: ## Generate and apply migrations.
python manage.py makemigrations
python manage.py migrate

superuser: ## Create a superuser.
python manage.py createsuperuser

test: ## Run the tests.
python manage.py test
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_bus_stops', number=50)
6 changes: 6 additions & 0 deletions padam_django/apps/fleet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ class BusAdmin(admin.ModelAdmin):
@admin.register(models.Driver)
class DriverAdmin(admin.ModelAdmin):
pass


@admin.register(models.BusShift)
class BusShiftAdmin(admin.ModelAdmin):
form = models.BusShiftForm
pass
11 changes: 11 additions & 0 deletions padam_django/apps/fleet/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ class BusFactory(factory.django.DjangoModelFactory):

class Meta:
model = models.Bus


class BusShiftFactory(factory.django.DjangoModelFactory):
bus = factory.SubFactory(BusFactory)
driver = factory.SubFactory(DriverFactory)
start = factory.LazyFunction(fake.date_time_this_month)
end = factory.LazyFunction(fake.date_time_this_month)
bus_stop_ids = factory.LazyFunction(lambda: [fake.random_int(min=1, max=100) for _ in range(2)])

class Meta:
model = models.BusShift
45 changes: 45 additions & 0 deletions padam_django/apps/fleet/migrations/0003_auto_20241112_0427.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 3.2.5 on 2024-11-12 04:27

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


class Migration(migrations.Migration):

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

operations = [
migrations.CreateModel(
name='BusShift',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start', models.DateTimeField(verbose_name='Start of the shift')),
('end', models.DateTimeField(verbose_name='End of the shift')),
('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')),
],
options={
'ordering': ['start'],
},
),
migrations.CreateModel(
name='BusShiftStop',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField(verbose_name='Order of the stop')),
('bus_shift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.busshift')),
('bus_stop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geography.busstop')),
],
options={
'ordering': ['order'],
},
),
migrations.AddField(
model_name='busshift',
name='stops',
field=models.ManyToManyField(related_name='shifts', through='fleet.BusShiftStop', to='geography.BusStop'),
),
]
25 changes: 25 additions & 0 deletions padam_django/apps/fleet/migrations/0004_auto_20241112_0612.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2024-11-12 06:12

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('fleet', '0003_auto_20241112_0427'),
]

operations = [
migrations.RemoveField(
model_name='busshift',
name='stops',
),
migrations.AddField(
model_name='busshift',
name='bus_stop_ids',
field=models.TextField(blank=True, default='[]'),
),
migrations.DeleteModel(
name='BusShiftStop',
),
]
71 changes: 71 additions & 0 deletions padam_django/apps/fleet/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from django.db import models
from django.core.exceptions import ValidationError
from django import forms
from padam_django.apps.geography.models import BusStop


class Driver(models.Model):
Expand All @@ -16,3 +19,71 @@ class Meta:

def __str__(self):
return f"Bus: {self.licence_plate} (id: {self.pk})"


class BusShift(models.Model):
bus = models.ForeignKey(Bus, on_delete=models.CASCADE, related_name='shifts')
driver = models.ForeignKey(Driver, on_delete=models.CASCADE, related_name='shifts')
start = models.DateTimeField("Start of the shift")
end = models.DateTimeField("End of the shift")
bus_stop_ids = models.TextField(blank=True, default='[]')

class Meta:
ordering = ['start']

def __str__(self):
return f"BusShift: {self.bus.licence_plate} (Driver: {self.driver.user.username}, Start: {self.start}, End: {self.end})"


class BusShiftForm(forms.ModelForm):
bus_stop_ids = forms.CharField(widget=forms.Textarea, help_text="Enter bus stop IDs separated by commas")

class Meta:
model = BusShift
fields = ['bus', 'driver', 'start', 'end', 'bus_stop_ids']

def clean_bus_stop_ids(self):
bus_stop_ids = self.cleaned_data['bus_stop_ids']
bus_stop_ids = [int(id.strip()) for id in bus_stop_ids.split(',') if id.strip().isdigit()]

if len(bus_stop_ids) < 2:
raise ValidationError("There must be at least two stops.")

# Ensure all bus stop IDs are valid
if not BusStop.objects.filter(id__in=bus_stop_ids).count() == len(bus_stop_ids):
raise ValidationError("One or more bus stop IDs are invalid.")

return bus_stop_ids

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

# Ensure the end time is after the start time
if start and end and start >= end:
raise ValidationError("The end time must be after the start time.")

# Ensure no overlapping shifts for the same bus
if bus and start and end:
overlapping_bus_shifts = BusShift.objects.filter(
bus=bus,
start__lt=end,
end__gt=start
).exclude(pk=self.instance.pk)
if overlapping_bus_shifts.exists():
raise ValidationError("The bus has overlapping shifts.")

# Ensure no overlapping shifts for the same driver
if driver and start and end:
overlapping_driver_shifts = BusShift.objects.filter(
driver=driver,
start__lt=end,
end__gt=start
).exclude(pk=self.instance.pk)
if overlapping_driver_shifts.exists():
raise ValidationError("The driver has overlapping shifts.")

return cleaned_data
110 changes: 110 additions & 0 deletions padam_django/apps/fleet/test_bus_shifts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from django.test import TestCase
from padam_django.apps.fleet.models import BusShiftForm
from padam_django.apps.fleet.factories import BusFactory, DriverFactory
from padam_django.apps.geography.factories import BusStopFactory

class BusShiftFormTest(TestCase):

def setUp(self):
self.bus = BusFactory()
self.second_bus = BusFactory()
self.driver = DriverFactory()
self.stop1 = BusStopFactory()
self.stop2 = BusStopFactory()

def test_bus_shift_form_with_valid_data(self):
form_data = {
'bus': self.bus.id,
'driver': self.driver.id,
'start': '2023-10-01T08:00:00Z',
'end': '2023-10-01T10:00:00Z',
'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}'
}
form = BusShiftForm(data=form_data)
self.assertTrue(form.is_valid())

def test_bus_shift_form_with_less_than_two_stops(self):
form_data = {
'bus': self.bus.id,
'driver': self.driver.id,
'start': '2023-10-01T08:00:00Z',
'end': '2023-10-01T10:00:00Z',
'bus_stop_ids': f'{self.stop1.id}'
}
form = BusShiftForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('There must be at least two stops.', form.errors['bus_stop_ids'])

def test_bus_shift_form_with_invalid_stop_ids(self):
form_data = {
'bus': self.bus.id,
'driver': self.driver.id,
'start': '2023-10-01T08:00:00Z',
'end': '2023-10-01T10:00:00Z',
'bus_stop_ids': '999,1000'
}
form = BusShiftForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('One or more bus stop IDs are invalid.', form.errors['bus_stop_ids'])

def test_bus_shift_form_with_end_time_before_start_time(self):
form_data = {
'bus': self.bus.id,
'driver': self.driver.id,
'start': '2023-10-01T10:00:00Z',
'end': '2023-10-01T08:00:00Z',
'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}'
}
form = BusShiftForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('The end time must be after the start time.', form.errors['__all__'])

def test_bus_shift_form_with_overlapping_shifts_for_bus(self):
# Create an initial BusShift to overlap with
initial_form_data = {
'bus': self.bus.id,
'driver': self.driver.id,
'start': '2023-10-01T08:00:00Z',
'end': '2023-10-01T10:00:00Z',
'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}'
}
initial_form = BusShiftForm(data=initial_form_data)
if initial_form.is_valid():
initial_form.save()

# Create a new BusShift that overlaps with the initial one
form_data = {
'bus': self.bus.id,
'driver': self.driver.id,
'start': '2023-10-01T09:00:00Z',
'end': '2023-10-01T11:00:00Z',
'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}'
}
form = BusShiftForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('The bus has overlapping shifts.', form.errors['__all__'])

def test_bus_shift_form_with_overlapping_shifts_for_driver(self):
# Create an initial BusShift to overlap with
initial_form_data = {
'bus': self.bus.id,
'driver': self.driver.id,
'start': '2023-10-01T08:00:00Z',
'end': '2023-10-01T10:00:00Z',
'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}'
}
initial_form = BusShiftForm(data=initial_form_data)
if initial_form.is_valid():
initial_form.save()

# Create a new BusShift that overlaps with the initial one
form_data = {
'bus': self.second_bus.id,
'driver': self.driver.id,
'start': '2023-10-01T09:00:00Z',
'end': '2023-10-01T11:00:00Z',
'bus_stop_ids': f'{self.stop1.id},{self.stop2.id}'
}
form = BusShiftForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('The driver has overlapping shifts.', form.errors['__all__'])
4 changes: 4 additions & 0 deletions padam_django/apps/geography/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
@admin.register(models.Place)
class PlaceAdmin(admin.ModelAdmin):
pass

@admin.register(models.BusStop)
class BusStopAdmin(admin.ModelAdmin):
pass
6 changes: 6 additions & 0 deletions padam_django/apps/geography/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ class PlaceFactory(factory.django.DjangoModelFactory):

class Meta:
model = models.Place

class BusStopFactory(factory.django.DjangoModelFactory):
place = factory.SubFactory(PlaceFactory)

class Meta:
model = models.BusStop
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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)
21 changes: 21 additions & 0 deletions padam_django/apps/geography/migrations/0002_busstop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.5 on 2024-11-12 04:27

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')),
('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bus_stops', to='geography.place')),
],
),
]
6 changes: 6 additions & 0 deletions padam_django/apps/geography/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ class Meta:

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

class BusStop(models.Model):
place = models.ForeignKey(Place, on_delete=models.CASCADE, related_name='bus_stops')

def __str__(self):
return f"BusStop: {self.place.name}"