From f2b6557cb7ff2104ea075ea3b62f1cd989dec054 Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Sun, 4 Feb 2024 22:20:09 +0100 Subject: [PATCH] Update GTFS Realtime data --- sncfgtfs/admin.py | 22 +- sncfgtfs/locale/fr/LC_MESSAGES/django.po | 230 ++++++++++-------- .../commands/update_sncf_gtfs_rt.py | 158 ++++++++++++ ..._trip_options_tripupdate_stoptimeupdate.py | 105 ++++++++ sncfgtfs/models.py | 109 ++++++++- 5 files changed, 516 insertions(+), 108 deletions(-) create mode 100644 sncfgtfs/management/commands/update_sncf_gtfs_rt.py create mode 100644 sncfgtfs/migrations/0002_alter_trip_options_tripupdate_stoptimeupdate.py diff --git a/sncfgtfs/admin.py b/sncfgtfs/admin.py index 51da102..a625a4f 100644 --- a/sncfgtfs/admin.py +++ b/sncfgtfs/admin.py @@ -1,8 +1,7 @@ from django.contrib import admin -from django.utils.safestring import mark_safe from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ - Transfer, FeedInfo + Transfer, FeedInfo, StopTimeUpdate, TripUpdate @admin.register(Agency) @@ -33,7 +32,7 @@ class RouteAdmin(admin.ModelAdmin): class TripAdmin(admin.ModelAdmin): list_display = ('id', 'route', 'service', 'headsign', 'direction_id',) list_filter = ('direction_id', 'service__transport_type',) - search_fields = ('id', 'route__long_name', 'service', 'headsign',) + search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',) ordering = ('route', 'service',) @@ -80,3 +79,20 @@ class FeedInfoAdmin(admin.ModelAdmin): search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date', 'end_date', 'version',) ordering = ('publisher_name',) + + +@admin.register(StopTimeUpdate) +class StopTimeUpdateAdmin(admin.ModelAdmin): + list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time', + 'departure_delay', 'departure_time', 'schedule_relationship',) + list_filter = ('schedule_relationship',) + search_fields = ('trip_update__trip_id', 'stop_time__stop__name', 'arrival_time', 'departure_time',) + ordering = ('trip_update', 'stop_time',) + + +@admin.register(TripUpdate) +class TripUpdateAdmin(admin.ModelAdmin): + list_display = ('trip_id', 'start_date', 'start_time',) + search_fields = ('trip_id', 'start_date', 'start_time',) + ordering = ('trip_id', 'start_date', 'start_time',) + autocomplete_fields = ('trip',) diff --git a/sncfgtfs/locale/fr/LC_MESSAGES/django.po b/sncfgtfs/locale/fr/LC_MESSAGES/django.po index 6e50064..ae06903 100644 --- a/sncfgtfs/locale/fr/LC_MESSAGES/django.po +++ b/sncfgtfs/locale/fr/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-01-27 10:44+0100\n" +"POT-Creation-Date: 2024-02-04 22:16+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Emmy D'Anello \n" "Language-Team: LANGUAGE \n" @@ -136,357 +136,395 @@ msgstr "Ajouté" msgid "Removed" msgstr "Supprimé" -#: sncfgtfs/models.py:65 sncfgtfs/models.py:209 +#: sncfgtfs/models.py:62 +msgid "Scheduled" +msgstr "Planifié" + +#: sncfgtfs/models.py:63 +msgid "Skipped" +msgstr "Sauté" + +#: sncfgtfs/models.py:64 +msgid "No data" +msgstr "Pas de données" + +#: sncfgtfs/models.py:65 +msgid "Unscheduled" +msgstr "Non planifié" + +#: sncfgtfs/models.py:72 sncfgtfs/models.py:229 msgid "Agency ID" msgstr "ID de l'agence" -#: sncfgtfs/models.py:71 +#: sncfgtfs/models.py:78 msgid "Agency name" msgstr "Nom de l'agence" -#: sncfgtfs/models.py:75 +#: sncfgtfs/models.py:82 msgid "Agency URL" msgstr "URL de l'agence" -#: sncfgtfs/models.py:80 +#: sncfgtfs/models.py:87 msgid "Agency timezone" msgstr "Fuseau horaire de l'agence" -#: sncfgtfs/models.py:85 +#: sncfgtfs/models.py:92 msgid "Agency language" msgstr "Langue de l'agence" -#: sncfgtfs/models.py:91 +#: sncfgtfs/models.py:98 msgid "Agency phone" msgstr "Téléphone de l'agence" -#: sncfgtfs/models.py:96 +#: sncfgtfs/models.py:103 msgid "Agency email" msgstr "Adresse email de l'agence" -#: sncfgtfs/models.py:104 +#: sncfgtfs/models.py:111 msgid "Agency" msgstr "Agence" -#: sncfgtfs/models.py:105 +#: sncfgtfs/models.py:112 msgid "Agencies" msgstr "Agences" -#: sncfgtfs/models.py:112 sncfgtfs/models.py:366 +#: sncfgtfs/models.py:120 sncfgtfs/models.py:420 msgid "Stop ID" msgstr "ID de l'arrêt" -#: sncfgtfs/models.py:117 +#: sncfgtfs/models.py:125 msgid "Stop code" msgstr "Code de l'arrêt" -#: sncfgtfs/models.py:123 +#: sncfgtfs/models.py:131 msgid "Stop name" msgstr "Nom de l'arrêt" -#: sncfgtfs/models.py:128 +#: sncfgtfs/models.py:136 msgid "Stop description" msgstr "Description de l'arrêt" -#: sncfgtfs/models.py:133 +#: sncfgtfs/models.py:141 msgid "Stop longitude" msgstr "Longitude de l'arrêt" -#: sncfgtfs/models.py:137 +#: sncfgtfs/models.py:145 msgid "Stop latitude" msgstr "Latitude de l'arrêt" -#: sncfgtfs/models.py:142 +#: sncfgtfs/models.py:150 msgid "Zone ID" msgstr "ID de la zone" -#: sncfgtfs/models.py:146 +#: sncfgtfs/models.py:154 msgid "Stop URL" msgstr "URL de l'arrêt" -#: sncfgtfs/models.py:151 +#: sncfgtfs/models.py:159 msgid "Location type" msgstr "Type de localisation" -#: sncfgtfs/models.py:160 +#: sncfgtfs/models.py:168 msgid "Parent station" msgstr "Gare parente" -#: sncfgtfs/models.py:168 +#: sncfgtfs/models.py:176 msgid "Stop timezone" msgstr "Fuseau horaire de l'arrêt" -#: sncfgtfs/models.py:174 +#: sncfgtfs/models.py:182 msgid "Level ID" msgstr "ID du niveau" -#: sncfgtfs/models.py:179 +#: sncfgtfs/models.py:187 msgid "Wheelchair boarding" msgstr "Embarquement en fauteuil roulant" -#: sncfgtfs/models.py:187 +#: sncfgtfs/models.py:195 msgid "Platform code" msgstr "Code du quai" -#: sncfgtfs/models.py:195 +#: sncfgtfs/models.py:214 msgid "Stop" msgstr "Arrêt" -#: sncfgtfs/models.py:196 +#: sncfgtfs/models.py:215 msgid "Stops" msgstr "Arrêts" -#: sncfgtfs/models.py:203 sncfgtfs/models.py:340 sncfgtfs/models.py:469 -#: sncfgtfs/models.py:506 +#: sncfgtfs/models.py:223 sncfgtfs/models.py:399 sncfgtfs/models.py:538 +#: sncfgtfs/models.py:576 msgid "ID" msgstr "Identifiant" -#: sncfgtfs/models.py:215 +#: sncfgtfs/models.py:235 msgid "Route short name" msgstr "Nom court de la ligne" -#: sncfgtfs/models.py:220 +#: sncfgtfs/models.py:240 msgid "Route long name" msgstr "Nom long de la ligne" -#: sncfgtfs/models.py:225 +#: sncfgtfs/models.py:245 msgid "Route description" msgstr "Description de la ligne" -#: sncfgtfs/models.py:230 +#: sncfgtfs/models.py:250 msgid "Route type" msgstr "Type de ligne" -#: sncfgtfs/models.py:235 +#: sncfgtfs/models.py:255 msgid "Route URL" msgstr "URL de la ligne" -#: sncfgtfs/models.py:241 +#: sncfgtfs/models.py:261 msgid "Route color" msgstr "Couleur de la ligne" -#: sncfgtfs/models.py:247 +#: sncfgtfs/models.py:267 msgid "Route text color" msgstr "Couleur du texte de la ligne" -#: sncfgtfs/models.py:255 sncfgtfs/models.py:269 +#: sncfgtfs/models.py:275 sncfgtfs/models.py:290 msgid "Route" msgstr "Ligne" -#: sncfgtfs/models.py:256 +#: sncfgtfs/models.py:276 msgid "Routes" msgstr "Lignes" -#: sncfgtfs/models.py:263 +#: sncfgtfs/models.py:284 msgid "Trip ID" msgstr "ID du trajet" -#: sncfgtfs/models.py:276 sncfgtfs/models.py:475 +#: sncfgtfs/models.py:297 sncfgtfs/models.py:544 msgid "Service" msgstr "Service" -#: sncfgtfs/models.py:282 +#: sncfgtfs/models.py:303 msgid "Trip headsign" msgstr "Destination du trajet" -#: sncfgtfs/models.py:288 +#: sncfgtfs/models.py:309 msgid "Trip short name" msgstr "Nom court du trajet" -#: sncfgtfs/models.py:293 +#: sncfgtfs/models.py:314 msgid "Direction" msgstr "Direction" -#: sncfgtfs/models.py:300 +#: sncfgtfs/models.py:321 msgid "Block ID" msgstr "ID du bloc" -#: sncfgtfs/models.py:306 +#: sncfgtfs/models.py:327 msgid "Shape ID" msgstr "ID de la forme" -#: sncfgtfs/models.py:311 +#: sncfgtfs/models.py:332 msgid "Wheelchair accessible" msgstr "Accessible en fauteuil roulant" -#: sncfgtfs/models.py:318 +#: sncfgtfs/models.py:339 msgid "Bikes allowed" msgstr "Vélos autorisés" -#: sncfgtfs/models.py:332 sncfgtfs/models.py:346 +#: sncfgtfs/models.py:391 sncfgtfs/models.py:405 sncfgtfs/models.py:648 msgid "Trip" msgstr "Trajet" -#: sncfgtfs/models.py:333 +#: sncfgtfs/models.py:392 msgid "Trips" msgstr "Trajets" -#: sncfgtfs/models.py:351 +#: sncfgtfs/models.py:410 sncfgtfs/models.py:698 msgid "Arrival time" msgstr "Heure d'arrivée" -#: sncfgtfs/models.py:355 +#: sncfgtfs/models.py:414 sncfgtfs/models.py:706 msgid "Departure time" msgstr "Heure de départ" -#: sncfgtfs/models.py:359 -msgid "Arrival next day" -msgstr "Arrivée le jour suivant" - -#: sncfgtfs/models.py:371 +#: sncfgtfs/models.py:425 msgid "Stop sequence" msgstr "Séquence de l'arrêt" -#: sncfgtfs/models.py:376 +#: sncfgtfs/models.py:430 msgid "Stop headsign" msgstr "Destination de l'arrêt" -#: sncfgtfs/models.py:381 +#: sncfgtfs/models.py:435 msgid "Pickup type" msgstr "Type de prise en charge" -#: sncfgtfs/models.py:388 +#: sncfgtfs/models.py:442 msgid "Drop off type" msgstr "Type de dépose" -#: sncfgtfs/models.py:395 +#: sncfgtfs/models.py:449 msgid "Timepoint" msgstr "Ponctualité" -#: sncfgtfs/models.py:404 +#: sncfgtfs/models.py:472 sncfgtfs/models.py:688 msgid "Stop time" msgstr "Heure d'arrêt" -#: sncfgtfs/models.py:405 +#: sncfgtfs/models.py:473 msgid "Stop times" msgstr "Heures d'arrêt" -#: sncfgtfs/models.py:412 +#: sncfgtfs/models.py:480 msgid "Service ID" msgstr "ID du service" -#: sncfgtfs/models.py:416 +#: sncfgtfs/models.py:484 msgid "Monday" msgstr "Lundi" -#: sncfgtfs/models.py:420 +#: sncfgtfs/models.py:488 msgid "Tuesday" msgstr "Mardi" -#: sncfgtfs/models.py:424 +#: sncfgtfs/models.py:492 msgid "Wednesday" msgstr "Mercredi" -#: sncfgtfs/models.py:428 +#: sncfgtfs/models.py:496 msgid "Thursday" msgstr "Jeudi" -#: sncfgtfs/models.py:432 +#: sncfgtfs/models.py:500 msgid "Friday" msgstr "Vendredi" -#: sncfgtfs/models.py:436 +#: sncfgtfs/models.py:504 msgid "Saturday" msgstr "Samedi" -#: sncfgtfs/models.py:440 +#: sncfgtfs/models.py:508 msgid "Sunday" msgstr "Dimanche" -#: sncfgtfs/models.py:444 +#: sncfgtfs/models.py:512 sncfgtfs/models.py:654 msgid "Start date" msgstr "Date de début" -#: sncfgtfs/models.py:448 +#: sncfgtfs/models.py:516 msgid "End date" msgstr "Date de fin" -#: sncfgtfs/models.py:453 sncfgtfs/models.py:490 +#: sncfgtfs/models.py:521 sncfgtfs/models.py:559 msgid "Transport type" msgstr "Type de transport" -#: sncfgtfs/models.py:461 +#: sncfgtfs/models.py:529 msgid "Calendar" msgstr "Calendrier" -#: sncfgtfs/models.py:462 +#: sncfgtfs/models.py:530 msgid "Calendars" msgstr "Calendriers" -#: sncfgtfs/models.py:480 +#: sncfgtfs/models.py:549 msgid "Date" msgstr "Date" -#: sncfgtfs/models.py:484 +#: sncfgtfs/models.py:553 msgid "Exception type" msgstr "Type d'exception" -#: sncfgtfs/models.py:498 +#: sncfgtfs/models.py:567 msgid "Calendar date" msgstr "Date du calendrier" -#: sncfgtfs/models.py:499 +#: sncfgtfs/models.py:568 msgid "Calendar dates" msgstr "Dates du calendrier" -#: sncfgtfs/models.py:512 +#: sncfgtfs/models.py:582 msgid "From stop" msgstr "Depuis l'arrêt" -#: sncfgtfs/models.py:519 +#: sncfgtfs/models.py:589 msgid "To stop" msgstr "Jusqu'à l'arrêt" -#: sncfgtfs/models.py:524 +#: sncfgtfs/models.py:594 msgid "Transfer type" msgstr "Type de correspondance" -#: sncfgtfs/models.py:530 +#: sncfgtfs/models.py:600 msgid "Minimum transfer time" msgstr "Temps de correspondance minimum" -#: sncfgtfs/models.py:535 +#: sncfgtfs/models.py:605 msgid "Transfer" msgstr "Correspondance" -#: sncfgtfs/models.py:536 +#: sncfgtfs/models.py:606 msgid "Transfers" msgstr "Correspondances" -#: sncfgtfs/models.py:542 +#: sncfgtfs/models.py:613 msgid "Feed publisher name" msgstr "Nom de l'éditeur du flux" -#: sncfgtfs/models.py:546 +#: sncfgtfs/models.py:617 msgid "Feed publisher URL" msgstr "URL de l'éditeur du flux" -#: sncfgtfs/models.py:551 +#: sncfgtfs/models.py:622 msgid "Feed language" msgstr "Langue du flux" -#: sncfgtfs/models.py:555 +#: sncfgtfs/models.py:626 msgid "Feed start date" msgstr "Date de début du flux" -#: sncfgtfs/models.py:559 +#: sncfgtfs/models.py:630 msgid "Feed end date" msgstr "Date de fin du flux" -#: sncfgtfs/models.py:564 +#: sncfgtfs/models.py:635 msgid "Feed version" msgstr "Version du flux" -#: sncfgtfs/models.py:568 +#: sncfgtfs/models.py:639 msgid "Feed info" msgstr "Information du flux" -#: sncfgtfs/models.py:569 +#: sncfgtfs/models.py:640 msgid "Feed infos" msgstr "Informations du flux" -#~ msgid "Route ID" -#~ msgstr "ID de la ligne" +#: sncfgtfs/models.py:658 +msgid "Start time" +msgstr "Heure de début" -#~ msgid "Feed ID" -#~ msgstr "ID du flux" +#: sncfgtfs/models.py:662 sncfgtfs/models.py:710 +msgid "Schedule relationship" +msgstr "Relation de la planification" + +#: sncfgtfs/models.py:671 sncfgtfs/models.py:681 +msgid "Trip update" +msgstr "Mise à jour du trajet" + +#: sncfgtfs/models.py:672 +msgid "Trip updates" +msgstr "Mises à jour des trajets" + +#: sncfgtfs/models.py:694 +msgid "Arrival delay" +msgstr "Retard à l'arrivée" + +#: sncfgtfs/models.py:702 +msgid "Departure delay" +msgstr "Retard au départ" + +#: sncfgtfs/models.py:719 +msgid "Stop time update" +msgstr "Mise à jour du temps d'arrêt" + +#: sncfgtfs/models.py:720 +msgid "Stop time updates" +msgstr "Mises à jour des temps d'arrêt" diff --git a/sncfgtfs/management/commands/update_sncf_gtfs_rt.py b/sncfgtfs/management/commands/update_sncf_gtfs_rt.py new file mode 100644 index 0000000..b66bbdc --- /dev/null +++ b/sncfgtfs/management/commands/update_sncf_gtfs_rt.py @@ -0,0 +1,158 @@ +from datetime import timedelta, datetime, date, time +from zoneinfo import ZoneInfo + +import requests +from django.core.management import BaseCommand + +from sncfgtfs.gtfs_realtime_pb2 import FeedMessage +from sncfgtfs.models import Calendar, CalendarDate, StopTime, StopTimeUpdate, Trip, TripUpdate, Stop + + +class Command(BaseCommand): + help = "Update the SNCF GTFS Realtime database." + + GTFS_RT_FEEDS = { + "TGV": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates", + "IC": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates", + "TER": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates", + } + + def add_arguments(self, parser): + pass + + def handle(self, *args, **options): + for feed_type, feed_url in self.GTFS_RT_FEEDS.items(): + self.stdout.write(f"Updating {feed_type} feed...") + feed_message = FeedMessage() + feed_message.ParseFromString(requests.get(feed_url).content) + + stop_times_updates = [] + + for entity in feed_message.entity: + if entity.HasField("trip_update"): + trip_update = entity.trip_update + trip_id = trip_update.trip.trip_id + start_date = date(year=int(trip_update.trip.start_date[:4]), + month=int(trip_update.trip.start_date[4:6]), + day=int(trip_update.trip.start_date[6:])) + start_dt = datetime.combine(start_date, time(0), tzinfo=ZoneInfo("Europe/Paris")) + + if trip_update.trip.schedule_relationship == 1: + headsign = trip_id[5:-1] + trip_qs = Trip.objects.all() + trip_ids = trip_qs.values_list('id', flat=True) + for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update): + stop_id = stop_time_update.stop_id + st_queryset = StopTime.objects.filter(stop__parent_station_id=stop_id) + if stop_sequence == 0: + st_queryset = st_queryset.filter(stop_sequence=0) + trip_ids_restrict = trip_ids.intersection(st_queryset.values('trip_id')) + if trip_ids_restrict: + trip_ids = trip_ids_restrict + route_ids = set(Trip.objects.filter(id__in=trip_ids).values_list('route_id', flat=True)) + self.stdout.write(f"{len(route_ids)} routes found on trip for train {headsign}") + if not route_ids: + self.stdout.write(f"Route not found for trip {trip_id}.") + continue + elif len(route_ids) > 1: + self.stdout.write(f"Multiple routes found for trip {trip_id}.") + continue + route_id = route_ids.pop() + + Calendar.objects.update_or_create( + id=f"{feed_type}-new-{headsign}", + defaults={ + "transport_type": feed_type, + "monday": False, + "tuesday": False, + "wednesday": False, + "thursday": False, + "friday": False, + "saturday": False, + "sunday": False, + "start_date": start_date, + "end_date": start_date, + } + ) + CalendarDate.objects.update_or_create( + id=f"{feed_type}-{headsign}-{trip_update.trip.start_date}", + defaults={ + "service_id": f"{feed_type}-new-{headsign}", + "date": trip_update.trip.start_date, + "exception_type": 1, + "transport_type": feed_type, + } + ) + Trip.objects.update_or_create( + id=trip_id, + defaults={ + "route_id": route_id, + "service_id": f"{feed_type}-new-{headsign}", + "headsign": headsign, + "direction_id": trip_update.trip.direction_id, + } + ) + + sample_trip = Trip.objects.filter(id__in=trip_ids).first() + for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update): + stop_id = stop_time_update.stop_id + stop = Stop.objects.get(id=stop_id) + if stop.location_type == 1: + if stop_sequence == 0: + stop = sample_trip.stop_times.get(stop_sequence=0).stop + else: + previous_stop = sample_trip.stop_times.get(stop_sequence=stop_sequence - 1).stop + stop = next(s for s in stop.children.all() \ + if s.location_type == 0 and s.stop_type == previous_stop.stop_type) + stop_id = stop.id + StopTime.objects.update_or_create( + id=f"{trip_id}-{stop_sequence}", + trip_id=trip_id, + defaults={ + "stop_id": stop_id, + "stop_sequence": stop_sequence, + "arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time, + tz=ZoneInfo("Europe/Paris")) - start_dt, + "departure_time": datetime.fromtimestamp(stop_time_update.departure.time, + tz=ZoneInfo("Europe/Paris")) - start_dt, + "pickup_type": 0 if stop_time_update.departure.time else 1, + "drop_off_type": 0 if stop_time_update.arrival.time else 1, + } + ) + + if not Trip.objects.filter(id=trip_id).exists(): + self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.") + continue + + tu, _created = TripUpdate.objects.get_or_create( + trip_id=trip_id, + start_date=trip_update.trip.start_date, + start_time=trip_update.trip.start_time, + schedule_relationship=trip_update.trip.schedule_relationship, + ) + for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update): + if not StopTime.objects.filter(trip_id=trip_id, stop_sequence=stop_sequence).exists(): + self.stdout.write(f"Stop {stop_sequence} does not exist in GTFS feed for trip {trip_id}.") + continue + + st = StopTime.objects.get(trip_id=trip_id, stop_sequence=stop_sequence) + st_update = StopTimeUpdate( + trip_update=tu, + stop_time=st, + arrival_delay=timedelta(seconds=stop_time_update.arrival.delay), + arrival_time=datetime.fromtimestamp(stop_time_update.arrival.time, + tz=ZoneInfo("Europe/Paris")), + departure_delay=timedelta(seconds=stop_time_update.departure.delay), + departure_time=datetime.fromtimestamp(stop_time_update.departure.time, + tz=ZoneInfo("Europe/Paris")), + schedule_relationship=stop_time_update.schedule_relationship or 0, + ) + stop_times_updates.append(st_update) + else: + self.stdout.write(str(entity)) + + StopTimeUpdate.objects.bulk_create(stop_times_updates, + update_conflicts=True, + update_fields=['arrival_delay', 'arrival_time', + 'departure_delay', 'departure_time'], + unique_fields=['trip_update', 'stop_time']) diff --git a/sncfgtfs/migrations/0002_alter_trip_options_tripupdate_stoptimeupdate.py b/sncfgtfs/migrations/0002_alter_trip_options_tripupdate_stoptimeupdate.py new file mode 100644 index 0000000..1be058f --- /dev/null +++ b/sncfgtfs/migrations/0002_alter_trip_options_tripupdate_stoptimeupdate.py @@ -0,0 +1,105 @@ +# Generated by Django 5.0.1 on 2024-02-04 19:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("sncfgtfs", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="trip", + options={"verbose_name": "Trip", "verbose_name_plural": "Trips"}, + ), + migrations.CreateModel( + name="TripUpdate", + fields=[ + ( + "trip", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + related_name="update", + serialize=False, + to="sncfgtfs.trip", + verbose_name="Trip", + ), + ), + ("start_date", models.DateField(verbose_name="Start date")), + ("start_time", models.TimeField(verbose_name="Start time")), + ( + "schedule_relationship", + models.IntegerField( + choices=[ + (0, "Scheduled"), + (1, "Skipped"), + (2, "No data"), + (3, "Unscheduled"), + ], + default=0, + verbose_name="Schedule relationship", + ), + ), + ], + options={ + "verbose_name": "Trip update", + "verbose_name_plural": "Trip updates", + "ordering": ("start_date", "trip"), + "unique_together": {("trip", "start_date", "start_time")}, + }, + ), + migrations.CreateModel( + name="StopTimeUpdate", + fields=[ + ( + "stop_time", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + related_name="update", + serialize=False, + to="sncfgtfs.stoptime", + verbose_name="Stop time", + ), + ), + ("arrival_delay", models.DurationField(verbose_name="Arrival delay")), + ("arrival_time", models.DateTimeField(verbose_name="Arrival time")), + ( + "departure_delay", + models.DurationField(verbose_name="Departure delay"), + ), + ("departure_time", models.DateTimeField(verbose_name="Departure time")), + ( + "schedule_relationship", + models.IntegerField( + choices=[ + (0, "Scheduled"), + (1, "Skipped"), + (2, "No data"), + (3, "Unscheduled"), + ], + default=0, + verbose_name="Schedule relationship", + ), + ), + ( + "trip_update", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stop_time_updates", + to="sncfgtfs.tripupdate", + verbose_name="Trip update", + ), + ), + ], + options={ + "verbose_name": "Stop time update", + "verbose_name_plural": "Stop time updates", + "ordering": ("trip_update", "stop_time"), + "unique_together": {("trip_update", "stop_time")}, + }, + ), + ] diff --git a/sncfgtfs/models.py b/sncfgtfs/models.py index c8e555d..c531b95 100644 --- a/sncfgtfs/models.py +++ b/sncfgtfs/models.py @@ -58,6 +58,13 @@ class ExceptionType(models.IntegerChoices): REMOVED = 2, _("Removed") +class ScheduleRelationship(models.IntegerChoices): + SCHEDULED = 0, _("Scheduled") + SKIPPED = 1, _("Skipped") + NO_DATA = 2, _("No data") + UNSCHEDULED = 3, _("Unscheduled") + + class Agency(models.Model): id = models.CharField( max_length=255, @@ -189,6 +196,17 @@ class Stop(models.Model): blank=True, ) + @property + def stop_type(self): + train_type = self.id.split('StopPoint:OCE')[1].split('-')[0] + if train_type == "Train TER": + train_type = "TER" + elif train_type == "INTERCITES": + train_type = "INTER-CITÉS" + elif train_type == "INTERCITES de nuit": + train_type = "INTER-CITÉS de nuit" + return train_type + def __str__(self): return f"{self.name} ({self.id})" @@ -337,14 +355,7 @@ class Trip(models.Model): if self.service.transport_type == TransportType.TRANSILIEN: return self.route.short_name else: - train_type = self.origin.id.split('StopPoint:OCE')[1].split('-')[0] - if train_type == "Train TER": - train_type = "TER" - elif train_type == "INTERCITES": - train_type = "INTER-CITÉS" - elif train_type == "INTERCITES de nuit": - train_type = "INTER-CITÉS de nuit" - return train_type + return self.origin.stop_type @property def train_number(self): @@ -379,7 +390,6 @@ class Trip(models.Model): class Meta: verbose_name = _("Trip") verbose_name_plural = _("Trips") - ordering = ("id",) class StopTime(models.Model): @@ -629,3 +639,84 @@ class FeedInfo(models.Model): verbose_name = _("Feed info") verbose_name_plural = _("Feed infos") ordering = ("publisher_name",) + + +class TripUpdate(models.Model): + trip = models.OneToOneField( + to="Trip", + on_delete=models.CASCADE, + verbose_name=_("Trip"), + related_name="update", + primary_key=True, + ) + + start_date = models.DateField( + verbose_name=_("Start date"), + ) + + start_time = models.TimeField( + verbose_name=_("Start time"), + ) + + schedule_relationship = models.IntegerField( + verbose_name=_("Schedule relationship"), + choices=ScheduleRelationship, + default=ScheduleRelationship.SCHEDULED, + ) + + def __str__(self): + return str(self.trip) + + class Meta: + verbose_name = _("Trip update") + verbose_name_plural = _("Trip updates") + ordering = ("start_date", "trip",) + unique_together = ("trip", "start_date", "start_time",) + + +class StopTimeUpdate(models.Model): + trip_update = models.ForeignKey( + to="TripUpdate", + on_delete=models.CASCADE, + verbose_name=_("Trip update"), + related_name="stop_time_updates", + ) + + stop_time = models.OneToOneField( + to="StopTime", + on_delete=models.CASCADE, + verbose_name=_("Stop time"), + related_name="update", + primary_key=True, + ) + + arrival_delay = models.DurationField( + verbose_name=_("Arrival delay"), + ) + + arrival_time = models.DateTimeField( + verbose_name=_("Arrival time"), + ) + + departure_delay = models.DurationField( + verbose_name=_("Departure delay"), + ) + + departure_time = models.DateTimeField( + verbose_name=_("Departure time"), + ) + + schedule_relationship = models.IntegerField( + verbose_name=_("Schedule relationship"), + choices=ScheduleRelationship, + default=ScheduleRelationship.SCHEDULED, + ) + + def __str__(self): + return str(self.trip_update) + + class Meta: + verbose_name = _("Stop time update") + verbose_name_plural = _("Stop time updates") + ordering = ("trip_update", "stop_time",) + unique_together = ("trip_update", "stop_time",)