diff --git a/simulator/admin.py b/simulator/admin.py index 7453591..bc91361 100644 --- a/simulator/admin.py +++ b/simulator/admin.py @@ -6,4 +6,20 @@ admin.site.register(Carrier) admin.site.register(AircraftBase) admin.site.register(Equipment) admin.site.register(Aerodrome) -admin.site.register(Flight) + + +@admin.register(Flight) +class FlightAdmin(admin.ModelAdmin): # type: ignore[type-arg] + list_display = ( + "carrier", + "flight_number_display", + "origin", + "destination", + "departure_time", + "arrival_time", + "status", + ) + + @admin.display(description="Flight") + def flight_number_display(self, obj): + return f"{obj.carrier.icao}{obj.flight_number}" diff --git a/simulator/migrations/0001_initial.py b/simulator/migrations/0001_initial.py index d73972a..1088ee6 100644 --- a/simulator/migrations/0001_initial.py +++ b/simulator/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 5.2.6 on 2025-09-30 02:02 +# Generated by Django 5.2.6 on 2025-09-30 03:22 +import django.core.validators import django.db.models.deletion from django.db import migrations, models @@ -20,7 +21,9 @@ class Migration(migrations.Migration): ('iata', models.CharField(blank=True, max_length=3, null=True, unique=True)), ('name', models.CharField(max_length=100)), ('city', models.CharField(max_length=100)), - ('country', models.CharField(max_length=100)), + ('country', models.CharField(max_length=3)), + ('latitude', models.FloatField()), + ('longitude', models.FloatField()), ], ), migrations.CreateModel( @@ -41,7 +44,7 @@ class Migration(migrations.Migration): ('icao', models.CharField(max_length=3, unique=True)), ('iata', models.CharField(max_length=2, unique=True)), ('name', models.CharField(max_length=100, unique=True)), - ('country', models.CharField(max_length=30)), + ('country', models.CharField(max_length=3)), ], ), migrations.CreateModel( @@ -53,4 +56,19 @@ class Migration(migrations.Migration): ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fleet', to='simulator.carrier')), ], ), + migrations.CreateModel( + name='Flight', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('flight_number', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)])), + ('departure_time', models.DateTimeField()), + ('carrier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flights', to='simulator.carrier')), + ('destination', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arrivals', to='simulator.aerodrome')), + ('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flights', to='simulator.equipment')), + ('origin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='departures', to='simulator.aerodrome')), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('carrier', 'flight_number'), name='unique_flight_per_carrier')], + }, + ), ] diff --git a/simulator/migrations/0002_alter_aerodrome_country.py b/simulator/migrations/0002_alter_aerodrome_country.py deleted file mode 100644 index b57b10f..0000000 --- a/simulator/migrations/0002_alter_aerodrome_country.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-30 02:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('simulator', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='aerodrome', - name='country', - field=models.CharField(max_length=2), - ), - ] diff --git a/simulator/migrations/0003_alter_aerodrome_country.py b/simulator/migrations/0003_alter_aerodrome_country.py deleted file mode 100644 index 29bdcd0..0000000 --- a/simulator/migrations/0003_alter_aerodrome_country.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-30 02:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('simulator', '0002_alter_aerodrome_country'), - ] - - operations = [ - migrations.AlterField( - model_name='aerodrome', - name='country', - field=models.CharField(max_length=3), - ), - ] diff --git a/simulator/migrations/0004_alter_carrier_country_flight.py b/simulator/migrations/0004_alter_carrier_country_flight.py deleted file mode 100644 index f8cb5df..0000000 --- a/simulator/migrations/0004_alter_carrier_country_flight.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-30 03:00 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('simulator', '0003_alter_aerodrome_country'), - ] - - operations = [ - migrations.AlterField( - model_name='carrier', - name='country', - field=models.CharField(max_length=3), - ), - migrations.CreateModel( - name='Flight', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('flight_number', models.PositiveIntegerField(max_length=4)), - ('departure_time', models.DateTimeField()), - ('arrival_time', models.DateTimeField()), - ('carrier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flights', to='simulator.carrier')), - ('destination', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arrivals', to='simulator.aerodrome')), - ('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flights', to='simulator.equipment')), - ('origin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='departures', to='simulator.aerodrome')), - ], - options={ - 'constraints': [models.UniqueConstraint(fields=('carrier', 'flight_number'), name='unique_flight_per_carrier')], - }, - ), - ] diff --git a/simulator/migrations/0005_alter_flight_flight_number.py b/simulator/migrations/0005_alter_flight_flight_number.py deleted file mode 100644 index 50f2073..0000000 --- a/simulator/migrations/0005_alter_flight_flight_number.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-30 03:03 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('simulator', '0004_alter_carrier_country_flight'), - ] - - operations = [ - migrations.AlterField( - model_name='flight', - name='flight_number', - field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)]), - ), - ] diff --git a/simulator/models/aerodrome.py b/simulator/models/aerodrome.py index 9042aed..3cde111 100644 --- a/simulator/models/aerodrome.py +++ b/simulator/models/aerodrome.py @@ -7,6 +7,8 @@ class Aerodrome(models.Model): name = models.CharField(max_length=100) city = models.CharField(max_length=100) country = models.CharField(max_length=3) + latitude = models.FloatField() + longitude = models.FloatField() def __str__(self): return f"{self.icao} - {self.city}" diff --git a/simulator/models/flight.py b/simulator/models/flight.py index feea239..c3e6d5c 100644 --- a/simulator/models/flight.py +++ b/simulator/models/flight.py @@ -3,6 +3,10 @@ from .carrier import Carrier from .aircraft import Equipment from .aerodrome import Aerodrome from django.core.validators import MinValueValidator, MaxValueValidator +from simulator.utils import haversine_nm +from datetime import timedelta +from django.core.exceptions import ValidationError +from django.utils import timezone class Flight(models.Model): @@ -23,7 +27,6 @@ class Flight(models.Model): validators=[MinValueValidator(1), MaxValueValidator(9999)] ) departure_time = models.DateTimeField() - arrival_time = models.DateTimeField() class Meta: constraints = [ @@ -34,3 +37,44 @@ class Flight(models.Model): def __str__(self): return f"{self.carrier.icao}{self.flight_number} {self.origin.icao} > {self.destination.icao}" + + @property + def distance_nm(self) -> float: + return haversine_nm( + self.origin.latitude, + self.origin.longitude, + self.destination.latitude, + self.destination.longitude, + ) + + @property + def duration(self) -> timedelta: + speed = self.equipment.model.cruise_speed_kt # knots = nm/hr + hours = self.distance_nm / speed + return timedelta(hours=hours) + + @property + def arrival_time(self): + return self.departure_time + self.duration + + def clean(self): + overlapping = Flight.objects.filter( + equipment=self.equipment, + departure_time__lt=self.arrival_time, + arrival_time__gt=self.departure_time, + ).exclude(pk=self.pk) + + if overlapping.exists(): + raise ValidationError( + f"{self.equipment} is already assigned to another flight in this timeframe." + ) + + @property + def status(self): + now = timezone.now() + if now < self.departure_time: + return "Scheduled" + elif self.departure_time <= now < self.arrival_time: + return "In-Flight" + else: + return "Arrived" diff --git a/simulator/utils.py b/simulator/utils.py new file mode 100644 index 0000000..3d6d3ac --- /dev/null +++ b/simulator/utils.py @@ -0,0 +1,17 @@ +import math + + +def haversine_nm(lat1, lon1, lat2, lon2): + # Earth radius in nautical miles + R = 3440.065 + phi1, phi2 = math.radians(lat1), math.radians(lat2) + dphi = math.radians(lat2 - lat1) + dlambda = math.radians(lon2 - lon1) + + a = ( + math.sin(dphi / 2) ** 2 + + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2 + ) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + return R * c