From de6e23163955210612f91b82e2005a88452991ae Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Sat, 27 Jan 2024 10:43:59 +0100 Subject: [PATCH] Add import script --- .../management/commands/update_sncf_gtfs.py | 345 ++++++++++++++++++ sncfgtfs/migrations/0001_initial.py | 173 ++++++--- sncfgtfs/models.py | 178 ++++++--- 3 files changed, 597 insertions(+), 99 deletions(-) create mode 100644 sncfgtfs/management/commands/update_sncf_gtfs.py diff --git a/sncfgtfs/management/commands/update_sncf_gtfs.py b/sncfgtfs/management/commands/update_sncf_gtfs.py new file mode 100644 index 0000000..01acba7 --- /dev/null +++ b/sncfgtfs/management/commands/update_sncf_gtfs.py @@ -0,0 +1,345 @@ +import csv +from datetime import datetime +from io import BytesIO +from zipfile import ZipFile + +import requests +from django.core.management import BaseCommand + +from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, Route, Stop, StopTime, Transfer, Trip + + +class Command(BaseCommand): + help = "Update the SNCF GTFS database." + + GTFS_FEEDS = { + "TGV": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export_gtfs_voyages.zip", + "IC": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-intercites-gtfs-last.zip", + "TER": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip", + "TN": "https://eu.ftp.opendatasoft.com/sncf/gtfs/transilien-gtfs.zip", + } + + def add_arguments(self, parser): + parser.add_argument('--bulk_size', type=int, default=1000, help='Number of objects to create in bulk.') + + def handle(self, *args, **options): + bulk_size = options['bulk_size'] + + if not FeedInfo.objects.exists(): + last_update_date = "1970-01-01" + else: + last_update_date = FeedInfo.objects.get().feed_version + + for url in self.GTFS_FEEDS.values(): + last_modified = requests.head(url).headers["Last-Modified"] + last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") + if last_modified.date().isoformat() > last_update_date: + break + else: + self.stdout.write(self.style.WARNING("Database already up-to-date.")) + return + + self.stdout.write("Updating database...") + + for transport_type, feed_url in self.GTFS_FEEDS.items(): + self.stdout.write(f"Downloading {transport_type} GTFS feed...") + with ZipFile(BytesIO(requests.get(feed_url).content)) as zipfile: + agencies = [] + for agency_dict in csv.DictReader(zipfile.read("agency.txt").decode().splitlines()): + agency_dict: dict + agency = Agency( + id=agency_dict['agency_id'], + name=agency_dict['agency_name'], + url=agency_dict['agency_url'], + timezone=agency_dict['agency_timezone'], + lang=agency_dict['agency_lang'], + phone=agency_dict.get('agency_phone', ""), + email=agency_dict.get('agency_email', ""), + ) + agencies.append(agency) + if agencies: + Agency.objects.bulk_create(agencies, + update_conflicts=True, + update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email'], + unique_fields=['id']) + agencies.clear() + + stops = [] + for stop_dict in csv.DictReader(zipfile.read("stops.txt").decode().splitlines()): + stop_dict: dict + stop = Stop( + id=stop_dict["stop_id"], + name=stop_dict['stop_name'], + desc=stop_dict['stop_desc'], + lat=stop_dict['stop_lat'], + lon=stop_dict['stop_lon'], + zone_id=stop_dict['zone_id'], + url=stop_dict['stop_url'], + location_type=stop_dict['location_type'], + parent_station_id=stop_dict['parent_station'] or None + if last_update_date != "1970-01-01" or transport_type != "TN" else None, + timezone=stop_dict.get('stop_timezone', ""), + wheelchair_boarding=stop_dict.get('wheelchair_boarding', 0), + level_id=stop_dict.get('level_id', ""), + platform_code=stop_dict.get('platform_code', ""), + ) + stops.append(stop) + + if len(stops) >= bulk_size: + Stop.objects.bulk_create(stops, + update_conflicts=True, + update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url', + 'location_type', 'parent_station_id', 'timezone', + 'wheelchair_boarding', 'level_id', 'platform_code'], + unique_fields=['id']) + stops.clear() + if stops: + Stop.objects.bulk_create(stops, + update_conflicts=True, + update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url', + 'location_type', 'parent_station_id', 'timezone', + 'wheelchair_boarding', 'level_id', 'platform_code'], + unique_fields=['id']) + stops.clear() + + routes = [] + for route_dict in csv.DictReader(zipfile.read("routes.txt").decode().splitlines()): + route_dict: dict + route = Route( + id=route_dict['route_id'], + agency_id=route_dict['agency_id'], + short_name=route_dict['route_short_name'], + long_name=route_dict['route_long_name'], + desc=route_dict['route_desc'], + type=route_dict['route_type'], + url=route_dict['route_url'], + color=route_dict['route_color'], + text_color=route_dict['route_text_color'], + ) + routes.append(route) + + if len(routes) >= bulk_size: + Route.objects.bulk_create(routes, + update_conflicts=True, + update_fields=['agency_id', 'short_name', 'long_name', 'desc', + 'type', 'url', 'color', 'text_color'], + unique_fields=['id']) + routes.clear() + if routes: + Route.objects.bulk_create(routes, + update_conflicts=True, + update_fields=['agency_id', 'short_name', 'long_name', 'desc', + 'type', 'url', 'color', 'text_color'], + unique_fields=['id']) + routes.clear() + + calendar_ids = [] + if "calendar.txt" in zipfile.namelist(): + calendars = [] + for calendar_dict in csv.DictReader(zipfile.read("calendar.txt").decode().splitlines()): + calendar_dict: dict + calendar = Calendar( + id=f"{transport_type}-{calendar_dict['service_id']}", + monday=calendar_dict['monday'], + tuesday=calendar_dict['tuesday'], + wednesday=calendar_dict['wednesday'], + thursday=calendar_dict['thursday'], + friday=calendar_dict['friday'], + saturday=calendar_dict['saturday'], + sunday=calendar_dict['sunday'], + start_date=calendar_dict['start_date'], + end_date=calendar_dict['end_date'], + transport_type=transport_type, + ) + calendars.append(calendar) + calendar_ids.append(calendar.id) + + if len(calendars) >= bulk_size: + Calendar.objects.bulk_create(calendars, + update_conflicts=True, + update_fields=['monday', 'tuesday', 'wednesday', 'thursday', + 'friday', 'saturday', 'sunday', 'start_date', + 'end_date', 'transport_type'], + unique_fields=['id']) + calendars.clear() + if calendars: + Calendar.objects.bulk_create(calendars, update_conflicts=True, + update_fields=['monday', 'tuesday', 'wednesday', 'thursday', + 'friday', 'saturday', 'sunday', 'start_date', + 'end_date', 'transport_type'], + unique_fields=['id']) + calendars.clear() + + calendars = [] + calendar_dates = [] + for calendar_date_dict in csv.DictReader(zipfile.read("calendar_dates.txt").decode().splitlines()): + calendar_date_dict: dict + calendar_date = CalendarDate( + id=f"{transport_type}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}", + service_id=f"{transport_type}-{calendar_date_dict['service_id']}", + date=calendar_date_dict['date'], + exception_type=calendar_date_dict['exception_type'], + ) + calendar_dates.append(calendar_date) + + if calendar_date.service_id not in calendar_ids: + calendar = Calendar( + id=f"{transport_type}-{calendar_date_dict['service_id']}", + monday=False, + tuesday=False, + wednesday=False, + thursday=False, + friday=False, + saturday=False, + sunday=False, + start_date=calendar_date_dict['date'], + end_date=calendar_date_dict['date'], + transport_type=transport_type, + ) + calendars.append(calendar) + + if len(calendar_dates) >= bulk_size: + Calendar.objects.bulk_create(calendars, + update_conflicts=True, + update_fields=['end_date'], + unique_fields=['id']) + CalendarDate.objects.bulk_create(calendar_dates, + update_conflicts=True, + update_fields=['service_id', 'date', 'exception_type'], + unique_fields=['id']) + calendars.clear() + calendar_dates.clear() + + if calendar_dates: + Calendar.objects.bulk_create(calendars, + update_conflicts=True, + update_fields=['end_date'], + unique_fields=['id']) + CalendarDate.objects.bulk_create(calendar_dates, + update_conflicts=True, + update_fields=['service_id', 'date', 'exception_type'], + unique_fields=['id']) + calendars.clear() + calendar_dates.clear() + + trips = [] + for trip_dict in csv.DictReader(zipfile.read("trips.txt").decode().splitlines()): + trip_dict: dict + trip = Trip( + id=trip_dict['trip_id'], + route_id=trip_dict['route_id'], + service_id=f"{transport_type}-{trip_dict['service_id']}", + headsign=trip_dict['trip_headsign'], + short_name=trip_dict.get('trip_short_name', ""), + direction_id=trip_dict['direction_id'] or None, + block_id=trip_dict['block_id'], + shape_id=trip_dict['shape_id'], + wheelchair_accessible=trip_dict.get('wheelchair_accessible', None), + bikes_allowed=trip_dict.get('bikes_allowed', None), + ) + trips.append(trip) + + if len(trips) >= bulk_size: + Trip.objects.bulk_create(trips, + update_conflicts=True, + update_fields=['route_id', 'service_id', 'headsign', 'short_name', + 'direction_id', 'block_id', 'shape_id', + 'wheelchair_accessible', 'bikes_allowed'], + unique_fields=['id']) + trips.clear() + if trips: + Trip.objects.bulk_create(trips, + update_conflicts=True, + update_fields=['route_id', 'service_id', 'headsign', 'short_name', + 'direction_id', 'block_id', 'shape_id', + 'wheelchair_accessible', 'bikes_allowed'], + unique_fields=['id']) + trips.clear() + + stop_times = [] + for stop_time_dict in csv.DictReader(zipfile.read("stop_times.txt").decode().splitlines()): + stop_time_dict: dict + + arrival_next_day = False + arrival_time = stop_time_dict['arrival_time'] + if int(arrival_time.split(":")[0]) >= 24: + split = arrival_time.split(':') + arrival_time = f"{int(split[0]) - 24:02}:{split[1]}:{split[2]}" + arrival_next_day = True + departure_time = stop_time_dict['departure_time'] + if int(departure_time.split(":")[0]) >= 24: + split = departure_time.split(':') + departure_time = f"{int(split[0]) - 24:02}:{split[1]}:{split[2]}" + arrival_next_day = True + + st = StopTime( + id=f"{stop_time_dict['trip_id']}-{stop_time_dict['stop_sequence']}", + trip_id=stop_time_dict['trip_id'], + arrival_time=arrival_time, + departure_time=departure_time, + arrival_next_day=arrival_next_day, + stop_id=stop_time_dict['stop_id'], + stop_sequence=stop_time_dict['stop_sequence'], + stop_headsign=stop_time_dict['stop_headsign'], + pickup_type=stop_time_dict['pickup_type'], + drop_off_type=stop_time_dict['drop_off_type'], + timepoint=stop_time_dict.get('timepoint', None), + ) + stop_times.append(st) + + if len(stop_times) >= bulk_size: + StopTime.objects.bulk_create(stop_times, + update_conflicts=True, + update_fields=['stop_id', 'arrival_time', 'departure_time', + 'arrival_next_day', 'stop_headsign', 'pickup_type', + 'drop_off_type', 'timepoint'], + unique_fields=['id']) + stop_times.clear() + if stop_times: + StopTime.objects.bulk_create(stop_times, + update_conflicts=True, + update_fields=['stop_id', 'arrival_time', 'departure_time', + 'arrival_next_day', 'stop_headsign', 'pickup_type', + 'drop_off_type', 'timepoint'], + unique_fields=['id']) + stop_times.clear() + + transfers = [] + for transfer_dict in csv.DictReader(zipfile.read("transfers.txt").decode().splitlines()): + transfer_dict: dict + transfer = Transfer( + id=f"{transfer_dict['from_stop_id']}-{transfer_dict['to_stop_id']}", + from_stop_id=transfer_dict['from_stop_id'], + to_stop_id=transfer_dict['to_stop_id'], + transfer_type=transfer_dict['transfer_type'], + min_transfer_time=transfer_dict['min_transfer_time'], + ) + transfers.append(transfer) + + if len(transfers) >= bulk_size: + Transfer.objects.bulk_create(transfers, + update_conflicts=True, + update_fields=['transfer_type', 'min_transfer_time'], + unique_fields=['id']) + transfers.clear() + + if transfers: + Transfer.objects.bulk_create(transfers, + update_conflicts=True, + update_fields=['transfer_type', 'min_transfer_time'], + unique_fields=['id']) + transfers.clear() + + if "feed_info.txt" in zipfile.namelist(): + for feed_info_dict in csv.DictReader(zipfile.read("feed_info.txt").decode().splitlines()): + feed_info_dict: dict + FeedInfo.objects.update_or_create( + feed_publisher_name=feed_info_dict['feed_publisher_name'], + defaults={ + 'feed_publisher_url': feed_info_dict['feed_publisher_url'], + 'feed_lang': feed_info_dict['feed_lang'], + 'feed_start_date': feed_info_dict['feed_start_date'], + 'feed_end_date': feed_info_dict['feed_end_date'], + 'feed_version': feed_info_dict['feed_version'], + } + ) diff --git a/sncfgtfs/migrations/0001_initial.py b/sncfgtfs/migrations/0001_initial.py index 4b51463..e22a080 100644 --- a/sncfgtfs/migrations/0001_initial.py +++ b/sncfgtfs/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.1 on 2024-01-26 20:15 +# Generated by Django 5.0.1 on 2024-01-27 09:09 import django.db.models.deletion from django.db import migrations, models @@ -14,7 +14,7 @@ class Migration(migrations.Migration): name="Agency", fields=[ ( - "agency_id", + "id", models.CharField( max_length=255, primary_key=True, @@ -23,22 +23,34 @@ class Migration(migrations.Migration): ), ), ( - "agency_name", + "name", models.CharField( max_length=255, unique=True, verbose_name="Agency name" ), ), - ("agency_url", models.URLField(verbose_name="Agency URL")), + ("url", models.URLField(verbose_name="Agency URL")), ( - "agency_timezone", + "timezone", models.CharField(max_length=255, verbose_name="Agency timezone"), ), ( - "agency_lang", + "lang", models.CharField( blank=True, max_length=255, verbose_name="Agency language" ), ), + ( + "phone", + models.CharField( + blank=True, max_length=255, verbose_name="Agency phone" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="Agency email" + ), + ), ], options={ "verbose_name": "Agency", @@ -49,7 +61,7 @@ class Migration(migrations.Migration): name="Calendar", fields=[ ( - "service_id", + "id", models.CharField( max_length=255, primary_key=True, @@ -66,6 +78,19 @@ class Migration(migrations.Migration): ("sunday", models.BooleanField(verbose_name="Sunday")), ("start_date", models.DateField(verbose_name="Start date")), ("end_date", models.DateField(verbose_name="End date")), + ( + "transport_type", + models.CharField( + choices=[ + ("TGV", "TGV"), + ("TER", "TER"), + ("IC", "Intercités"), + ("TN", "Transilien"), + ], + max_length=255, + verbose_name="Transport type", + ), + ), ], options={ "verbose_name": "Calendar", @@ -76,9 +101,12 @@ class Migration(migrations.Migration): name="FeedInfo", fields=[ ( - "feed_id", - models.SmallIntegerField( - primary_key=True, serialize=False, verbose_name="Feed ID" + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", ), ), ( @@ -112,21 +140,41 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( - auto_created=True, + models.CharField( + max_length=255, primary_key=True, serialize=False, verbose_name="ID", ), ), ("date", models.DateField(verbose_name="Date")), - ("exception_type", models.IntegerField(verbose_name="Exception type")), ( - "service_id", + "exception_type", + models.IntegerField( + choices=[(1, "Added"), (2, "Removed")], + verbose_name="Exception type", + ), + ), + ( + "transport_type", + models.CharField( + choices=[ + ("TGV", "TGV"), + ("TER", "TER"), + ("IC", "Intercités"), + ("TN", "Transilien"), + ], + max_length=255, + verbose_name="Transport type", + ), + ), + ( + "service", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="dates", to="sncfgtfs.calendar", - verbose_name="Service ID", + verbose_name="Service", ), ), ], @@ -139,30 +187,30 @@ class Migration(migrations.Migration): name="Route", fields=[ ( - "route_id", + "id", models.CharField( max_length=255, primary_key=True, serialize=False, - verbose_name="Route ID", + verbose_name="ID", ), ), ( - "route_short_name", + "short_name", models.CharField(max_length=255, verbose_name="Route short name"), ), ( - "route_long_name", + "long_name", models.CharField(max_length=255, verbose_name="Route long name"), ), ( - "route_desc", + "desc", models.CharField( blank=True, max_length=255, verbose_name="Route description" ), ), ( - "route_type", + "type", models.IntegerField( choices=[ (0, "Tram"), @@ -177,15 +225,15 @@ class Migration(migrations.Migration): verbose_name="Route type", ), ), - ("route_url", models.URLField(blank=True, verbose_name="Route URL")), + ("url", models.URLField(blank=True, verbose_name="Route URL")), ( - "route_color", + "color", models.CharField( blank=True, max_length=255, verbose_name="Route color" ), ), ( - "route_text_color", + "text_color", models.CharField( blank=True, max_length=255, verbose_name="Route text color" ), @@ -194,6 +242,7 @@ class Migration(migrations.Migration): "agency", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="routes", to="sncfgtfs.agency", verbose_name="Agency ID", ), @@ -208,7 +257,7 @@ class Migration(migrations.Migration): name="Stop", fields=[ ( - "stop_id", + "id", models.CharField( max_length=255, primary_key=True, @@ -217,25 +266,22 @@ class Migration(migrations.Migration): ), ), ( - "stop_code", + "code", models.CharField( blank=True, max_length=255, verbose_name="Stop code" ), ), + ("name", models.CharField(max_length=255, verbose_name="Stop name")), ( - "stop_name", - models.CharField(max_length=255, verbose_name="Stop name"), - ), - ( - "stop_desc", + "desc", models.CharField( blank=True, max_length=255, verbose_name="Stop description" ), ), - ("stop_lon", models.FloatField(verbose_name="Stop longitude")), - ("stop_lat", models.FloatField(verbose_name="Stop latitude")), + ("lon", models.FloatField(verbose_name="Stop longitude")), + ("lat", models.FloatField(verbose_name="Stop latitude")), ("zone_id", models.CharField(max_length=255, verbose_name="Zone ID")), - ("stop_url", models.URLField(blank=True, verbose_name="Stop URL")), + ("url", models.URLField(blank=True, verbose_name="Stop URL")), ( "location_type", models.IntegerField( @@ -252,7 +298,7 @@ class Migration(migrations.Migration): ), ), ( - "stop_timezone", + "timezone", models.CharField( blank=True, max_length=255, verbose_name="Stop timezone" ), @@ -278,13 +324,17 @@ class Migration(migrations.Migration): ), ( "platform_code", - models.CharField(max_length=255, verbose_name="Platform code"), + models.CharField( + blank=True, max_length=255, verbose_name="Platform code" + ), ), ( "parent_station", models.ForeignKey( blank=True, + null=True, on_delete=django.db.models.deletion.PROTECT, + related_name="children", to="sncfgtfs.stop", verbose_name="Parent station", ), @@ -300,8 +350,8 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( - auto_created=True, + models.CharField( + max_length=255, primary_key=True, serialize=False, verbose_name="ID", @@ -354,7 +404,7 @@ class Migration(migrations.Migration): name="Trip", fields=[ ( - "trip_id", + "id", models.CharField( max_length=255, primary_key=True, @@ -363,17 +413,13 @@ class Migration(migrations.Migration): ), ), ( - "service_id", - models.CharField(max_length=255, verbose_name="Service ID"), - ), - ( - "trip_headsign", + "headsign", models.CharField( blank=True, max_length=255, verbose_name="Trip headsign" ), ), ( - "trip_short_name", + "short_name", models.CharField( blank=True, max_length=255, verbose_name="Trip short name" ), @@ -381,8 +427,8 @@ class Migration(migrations.Migration): ( "direction_id", models.IntegerField( - blank=True, choices=[(0, "Outbound"), (1, "Inbound")], + null=True, verbose_name="Direction", ), ), @@ -401,13 +447,13 @@ class Migration(migrations.Migration): ( "wheelchair_accessible", models.IntegerField( - blank=True, choices=[ (0, "No information"), (1, "Possible"), (2, "Not possible"), ], default=0, + null=True, verbose_name="Wheelchair accessible", ), ), @@ -420,6 +466,7 @@ class Migration(migrations.Migration): (2, "Not possible"), ], default=0, + null=True, verbose_name="Bikes allowed", ), ), @@ -427,8 +474,18 @@ class Migration(migrations.Migration): "route", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="trips", to="sncfgtfs.route", - verbose_name="Route ID", + verbose_name="Route", + ), + ), + ( + "service", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="trips", + to="sncfgtfs.calendar", + verbose_name="Service", ), ), ], @@ -442,8 +499,8 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( - auto_created=True, + models.CharField( + max_length=255, primary_key=True, serialize=False, verbose_name="ID", @@ -451,6 +508,10 @@ class Migration(migrations.Migration): ), ("arrival_time", models.TimeField(verbose_name="Arrival time")), ("departure_time", models.TimeField(verbose_name="Departure time")), + ( + "arrival_next_day", + models.BooleanField(default=False, verbose_name="Arrival next day"), + ), ("stop_sequence", models.IntegerField(verbose_name="Stop sequence")), ( "stop_headsign", @@ -461,7 +522,6 @@ class Migration(migrations.Migration): ( "pickup_type", models.IntegerField( - blank=True, choices=[ (0, "Regular"), (1, "None"), @@ -469,13 +529,13 @@ class Migration(migrations.Migration): (3, "Must coordinate with driver"), ], default=0, + null=True, verbose_name="Pickup type", ), ), ( "drop_off_type", models.IntegerField( - blank=True, choices=[ (0, "Regular"), (1, "None"), @@ -483,29 +543,32 @@ class Migration(migrations.Migration): (3, "Must coordinate with driver"), ], default=0, + null=True, verbose_name="Drop off type", ), ), ( "timepoint", models.BooleanField( - blank=True, default=True, verbose_name="Timepoint" + default=True, null=True, verbose_name="Timepoint" ), ), ( "stop", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="stop_times", to="sncfgtfs.stop", verbose_name="Stop ID", ), ), ( - "trip_id", + "trip", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="stop_times", to="sncfgtfs.trip", - verbose_name="Trip ID", + verbose_name="Trip", ), ), ], diff --git a/sncfgtfs/models.py b/sncfgtfs/models.py index f0f009b..a513de5 100644 --- a/sncfgtfs/models.py +++ b/sncfgtfs/models.py @@ -2,6 +2,13 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +class TransportType(models.TextChoices): + TGV = "TGV", _("TGV") + TER = "TER", _("TER") + INTERCITES = "IC", _("Intercités") + TRANSILIEN = "TN", _("Transilien") + + class LocationType(models.IntegerChoices): STOP_PLATFORM = 0, _("Stop/platform") STATION = 1, _("Station") @@ -46,68 +53,87 @@ class TransferType(models.IntegerChoices): NOT_POSSIBLE = 3, _("Not possible") +class ExceptionType(models.IntegerChoices): + ADDED = 1, _("Added") + REMOVED = 2, _("Removed") + + class Agency(models.Model): - agency_id = models.CharField( + id = models.CharField( max_length=255, primary_key=True, verbose_name=_("Agency ID"), ) - agency_name = models.CharField( + name = models.CharField( max_length=255, unique=True, verbose_name=_("Agency name"), ) - agency_url = models.URLField( + url = models.URLField( verbose_name=_("Agency URL"), ) - agency_timezone = models.CharField( + timezone = models.CharField( max_length=255, verbose_name=_("Agency timezone"), ) - agency_lang = models.CharField( + lang = models.CharField( max_length=255, verbose_name=_("Agency language"), blank=True, ) + phone = models.CharField( + max_length=255, + verbose_name=_("Agency phone"), + blank=True, + ) + + email = models.EmailField( + verbose_name=_("Agency email"), + blank=True, + ) + + def __str__(self): + return self.name + class Meta: verbose_name = _("Agency") verbose_name_plural = _("Agencies") class Stop(models.Model): - stop_id = models.CharField( + id = models.CharField( max_length=255, primary_key=True, verbose_name=_("Stop ID"), ) - stop_code = models.CharField( + code = models.CharField( max_length=255, verbose_name=_("Stop code"), blank=True, ) - stop_name = models.CharField( + name = models.CharField( max_length=255, verbose_name=_("Stop name"), ) - stop_desc = models.CharField( + desc = models.CharField( max_length=255, verbose_name=_("Stop description"), blank=True, ) - stop_lon = models.FloatField( + lon = models.FloatField( verbose_name=_("Stop longitude"), ) - stop_lat = models.FloatField( + lat = models.FloatField( verbose_name=_("Stop latitude"), ) @@ -116,7 +142,7 @@ class Stop(models.Model): verbose_name=_("Zone ID"), ) - stop_url = models.URLField( + url = models.URLField( verbose_name=_("Stop URL"), blank=True, ) @@ -132,10 +158,12 @@ class Stop(models.Model): to="Stop", on_delete=models.PROTECT, verbose_name=_("Parent station"), + related_name="children", blank=True, + null=True, ) - stop_timezone = models.CharField( + timezone = models.CharField( max_length=255, verbose_name=_("Stop timezone"), blank=True, @@ -157,71 +185,79 @@ class Stop(models.Model): platform_code = models.CharField( max_length=255, verbose_name=_("Platform code"), + blank=True, ) + def __str__(self): + return f"{self.name} ({self.id})" + class Meta: verbose_name = _("Stop") verbose_name_plural = _("Stops") class Route(models.Model): - route_id = models.CharField( + id = models.CharField( max_length=255, primary_key=True, - verbose_name=_("Route ID"), + verbose_name=_("ID"), ) agency = models.ForeignKey( to="Agency", on_delete=models.CASCADE, verbose_name=_("Agency ID"), + related_name="routes", ) - route_short_name = models.CharField( + short_name = models.CharField( max_length=255, verbose_name=_("Route short name"), ) - route_long_name = models.CharField( + long_name = models.CharField( max_length=255, verbose_name=_("Route long name"), ) - route_desc = models.CharField( + desc = models.CharField( max_length=255, verbose_name=_("Route description"), blank=True, ) - route_type = models.IntegerField( + type = models.IntegerField( verbose_name=_("Route type"), choices=RouteType, ) - route_url = models.URLField( + url = models.URLField( verbose_name=_("Route URL"), blank=True, ) - route_color = models.CharField( + color = models.CharField( max_length=255, verbose_name=_("Route color"), blank=True, ) - route_text_color = models.CharField( + text_color = models.CharField( max_length=255, verbose_name=_("Route text color"), blank=True, ) + def __str__(self): + return f"{self.long_name}" + class Meta: verbose_name = _("Route") verbose_name_plural = _("Routes") class Trip(models.Model): - trip_id = models.CharField( + id = models.CharField( max_length=255, primary_key=True, verbose_name=_("Trip ID"), @@ -230,21 +266,24 @@ class Trip(models.Model): route = models.ForeignKey( to="Route", on_delete=models.CASCADE, - verbose_name=_("Route ID"), + verbose_name=_("Route"), + related_name="trips", ) - service_id = models.CharField( - max_length=255, - verbose_name=_("Service ID"), + service = models.ForeignKey( + to="Calendar", + on_delete=models.CASCADE, + verbose_name=_("Service"), + related_name="trips", ) - trip_headsign = models.CharField( + headsign = models.CharField( max_length=255, verbose_name=_("Trip headsign"), blank=True, ) - trip_short_name = models.CharField( + short_name = models.CharField( max_length=255, verbose_name=_("Trip short name"), blank=True, @@ -253,7 +292,7 @@ class Trip(models.Model): direction_id = models.IntegerField( verbose_name=_("Direction"), choices=Direction, - blank=True, + null=True, ) block_id = models.CharField( @@ -272,25 +311,40 @@ class Trip(models.Model): verbose_name=_("Wheelchair accessible"), choices=AccessInformation, default=AccessInformation.NO_INFORMATION, - blank=True, + null=True, ) bikes_allowed = models.IntegerField( verbose_name=_("Bikes allowed"), choices=AccessInformation, default=AccessInformation.NO_INFORMATION, + null=True, ) + @property + def destination(self): + return self.stop_times.order_by('-stop_sequence').first().stop.name + + def __str__(self): + return f"{self.route.long_name} - {self.id}" + class Meta: verbose_name = _("Trip") verbose_name_plural = _("Trips") class StopTime(models.Model): - trip_id = models.ForeignKey( + id = models.CharField( + max_length=255, + primary_key=True, + verbose_name=_("ID"), + ) + + trip = models.ForeignKey( to="Trip", on_delete=models.CASCADE, - verbose_name=_("Trip ID"), + verbose_name=_("Trip"), + related_name="stop_times", ) arrival_time = models.TimeField( @@ -301,10 +355,16 @@ class StopTime(models.Model): verbose_name=_("Departure time"), ) + arrival_next_day = models.BooleanField( + verbose_name=_("Arrival next day"), + default=False, + ) + stop = models.ForeignKey( to="Stop", on_delete=models.CASCADE, verbose_name=_("Stop ID"), + related_name="stop_times", ) stop_sequence = models.IntegerField( @@ -321,29 +381,32 @@ class StopTime(models.Model): verbose_name=_("Pickup type"), choices=PickupType, default=PickupType.REGULAR, - blank=True, + null=True, ) drop_off_type = models.IntegerField( verbose_name=_("Drop off type"), choices=PickupType, default=PickupType.REGULAR, - blank=True, + null=True, ) timepoint = models.BooleanField( verbose_name=_("Timepoint"), default=True, - blank=True, + null=True, ) + def __str__(self): + return f"{self.trip.route.long_name} - {self.trip_id} - {self.stop.name}" + class Meta: verbose_name = _("Stop time") verbose_name_plural = _("Stop times") class Calendar(models.Model): - service_id = models.CharField( + id = models.CharField( max_length=255, primary_key=True, verbose_name=_("Service ID"), @@ -385,16 +448,32 @@ class Calendar(models.Model): verbose_name=_("End date"), ) + transport_type = models.CharField( + max_length=255, + verbose_name=_("Transport type"), + choices=TransportType, + ) + + def __str__(self): + return self.id + class Meta: verbose_name = _("Calendar") verbose_name_plural = _("Calendars") class CalendarDate(models.Model): - service_id = models.ForeignKey( + id = models.CharField( + max_length=255, + primary_key=True, + verbose_name=_("ID"), + ) + + service = models.ForeignKey( to="Calendar", on_delete=models.CASCADE, - verbose_name=_("Service ID"), + verbose_name=_("Service"), + related_name="dates", ) date = models.DateField( @@ -403,14 +482,30 @@ class CalendarDate(models.Model): exception_type = models.IntegerField( verbose_name=_("Exception type"), + choices=ExceptionType, ) + transport_type = models.CharField( + max_length=255, + verbose_name=_("Transport type"), + choices=TransportType, + ) + + def __str__(self): + return f"{self.service.id} - {self.date} - {self.exception_type}" + class Meta: verbose_name = _("Calendar date") verbose_name_plural = _("Calendar dates") class Transfer(models.Model): + id = models.CharField( + max_length=255, + primary_key=True, + verbose_name=_("ID"), + ) + from_stop = models.ForeignKey( to="Stop", on_delete=models.CASCADE, @@ -442,11 +537,6 @@ class Transfer(models.Model): class FeedInfo(models.Model): - feed_id = models.SmallIntegerField( - primary_key=True, - verbose_name=_("Feed ID"), - ) - feed_publisher_name = models.CharField( max_length=255, verbose_name=_("Feed publisher name"),