From 11949228ee077022235ffe0e8d9eec32e7ceb1c0 Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Thu, 9 May 2024 19:28:19 +0200 Subject: [PATCH] Restructurate GTFS feeds into dedicated models --- .gitignore | 1 + sncf-station/src/AutocompleteStop.jsx | 20 +- sncf-station/src/TrainsTable.js | 25 +- sncf/api/views.py | 7 +- sncfgtfs/admin.py | 61 +- sncfgtfs/fixtures/gtfs_feeds.json | 92 ++ sncfgtfs/locale/fr/LC_MESSAGES/django.po | 577 +++++--- .../management/commands/update_sncf_gtfs.py | 194 ++- .../commands/update_sncf_gtfs_rt.py | 179 +-- sncfgtfs/migrations/0001_initial.py | 1176 ++++++++++------- sncfgtfs/models.py | 212 ++- 11 files changed, 1594 insertions(+), 950 deletions(-) create mode 100644 sncfgtfs/fixtures/gtfs_feeds.json diff --git a/.gitignore b/.gitignore index 2a81894..3514b06 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ coverage secrets.py settings_local.py *.log +*.txt media/ output/ /static/ diff --git a/sncf-station/src/AutocompleteStop.jsx b/sncf-station/src/AutocompleteStop.jsx index 675a305..658f6aa 100644 --- a/sncf-station/src/AutocompleteStop.jsx +++ b/sncf-station/src/AutocompleteStop.jsx @@ -40,23 +40,23 @@ function AutocompleteStop(params) { } function getOptionGroup(option) { - switch (option.transport_type) { - case "TGV": - case "IC": - case "TER": + switch (option.gtfs_feed) { + case "FR-SNCF-TGV": + case "FR-SNCF-IC": + case "FR-SNCF-TER": return "TGV/TER/Intercités" - case "TN": + case "FR-IDF-TN": return "Transilien" - case "ES": + case "FR-EUROSTAR": return "Eurostar" - case "TI": + case "IT-FRA-TI": return "Trenitalia France" - case "RENFE": + case "ES-RENFE": return "RENFE" - case "OBB": + case "AT-OBB": return "ÖBB" default: - return option.transport_type + return option.gtfs_feed } } diff --git a/sncf-station/src/TrainsTable.js b/sncf-station/src/TrainsTable.js index bd07a8d..176177d 100644 --- a/sncf-station/src/TrainsTable.js +++ b/sncf-station/src/TrainsTable.js @@ -229,10 +229,10 @@ function TrainRow({train, tableType, date, time}) { } function getTrainType(train, trip, route) { - switch (route.transport_type) { - case "TGV": - case "TER": - case "IC": + switch (route.gtfs_feed) { + case "FR-SNCF-TGV": + case "FR-SNCF-IC": + case "FR-SNCF-TER": let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0] switch (trainType) { case "Train TER": @@ -244,20 +244,20 @@ function getTrainType(train, trip, route) { default: return trainType } - case "TN": + case "FR-IDF-TN": return route.short_name - case "ES": + case "FR-EUROSTAR": return "Eurostar" - case "TI": - return "Trenitalia" - case "RENFE": + case "IT-FRA-TI": + return "Trenitalia France" + case "ES-RENFE": return "RENFE" - case "OBB": - if (trip.short_name.startsWith("NJ")) + case "AT-OBB": + if (trip.short_name?.startsWith("NJ")) return "NJ" return "ÖBB" default: - return "" + return trip.short_name?.split(" ")[0] } } @@ -280,6 +280,7 @@ function getTrainTypeDisplay(trainType) { case "Eurostar": return Eurostar case "Trenitalia": + case "Trenitalia France": return Frecciarossa case "RENFE": return RENFE diff --git a/sncf/api/views.py b/sncf/api/views.py index 2001b81..cfe63af 100644 --- a/sncf/api/views.py +++ b/sncf/api/views.py @@ -11,12 +11,11 @@ from rest_framework.filters import OrderingFilter, SearchFilter from sncf.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \ StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \ FeedInfoSerializer, TripUpdateSerializer, StopTimeUpdateSerializer -from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ - Transfer, FeedInfo, TripUpdate, StopTimeUpdate +from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, StopTimeUpdate, \ + Transfer, Trip, TripUpdate CACHE_CONTROL = cache_control(max_age=7200) -LAST_MODIFIED = last_modified(lambda *args, **kwargs: datetime.fromisoformat( - FeedInfo.objects.get(publisher_name="SNCF_default").version)) +LAST_MODIFIED = last_modified(lambda *args, **kwargs: GTFSFeed.objects.order_by('-last_modified').first().last_modified) LOOKUP_VALUE_REGEX = r"[\w.: |-]+" diff --git a/sncfgtfs/admin.py b/sncfgtfs/admin.py index a3e8f55..688f0fa 100644 --- a/sncfgtfs/admin.py +++ b/sncfgtfs/admin.py @@ -1,26 +1,38 @@ from django.contrib import admin +from django.forms import BaseInlineFormSet -from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ - Transfer, FeedInfo, StopTimeUpdate, TripUpdate +from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, \ + Route, Stop, StopTime, StopTimeUpdate, Transfer, Trip, TripUpdate + + +class LimitModelFormset(BaseInlineFormSet): + """ Base Inline formset to limit inline Model query results. """ + def get_queryset(self): + return super(LimitModelFormset, self).get_queryset()[:50] class CalendarDateInline(admin.TabularInline): model = CalendarDate extra = 0 + formset = LimitModelFormset class TripInline(admin.TabularInline): model = Trip extra = 0 + formset = LimitModelFormset autocomplete_fields = ('route', 'service',) show_change_link = True ordering = ('service',) + readonly_fields = ('gtfs_feed',) class StopTimeInline(admin.TabularInline): model = StopTime extra = 0 + formset = LimitModelFormset autocomplete_fields = ('stop',) + readonly_fields = ('id',) show_change_link = True ordering = ('stop_sequence',) @@ -28,47 +40,59 @@ class StopTimeInline(admin.TabularInline): class TripUpdateInline(admin.StackedInline): model = TripUpdate extra = 0 + formset = LimitModelFormset autocomplete_fields = ('trip',) class StopTimeUpdateInline(admin.StackedInline): model = StopTimeUpdate extra = 0 + formset = LimitModelFormset autocomplete_fields = ('trip_update', 'stop_time',) +@admin.register(GTFSFeed) +class GTFSFeedAdmin(admin.ModelAdmin): + list_display = ('name', 'code', 'country', 'last_modified',) + list_filter = ('country', 'last_modified',) + search_fields = ('name', 'code',) + readonly_fields = ('code',) + + @admin.register(Agency) class AgencyAdmin(admin.ModelAdmin): - list_display = ('name', 'id', 'url', 'timezone',) + list_display = ('name', 'id', 'url', 'timezone', 'gtfs_feed',) + list_filter = ('gtfs_feed', 'timezone',) search_fields = ('name',) + autocomplete_fields = ('gtfs_feed',) @admin.register(Stop) class StopAdmin(admin.ModelAdmin): list_display = ('name', 'id', 'lat', 'lon', 'location_type',) - list_filter = ('location_type', 'transport_type',) + list_filter = ('location_type', 'gtfs_feed',) search_fields = ('name', 'id',) ordering = ('name',) - autocomplete_fields = ('parent_station',) + autocomplete_fields = ('parent_station', 'gtfs_feed',) @admin.register(Route) class RouteAdmin(admin.ModelAdmin): - list_display = ('short_name', 'long_name', 'id', 'type',) - list_filter = ('transport_type', 'type', 'agency',) + list_display = ('__str__', 'id', 'type', 'gtfs_feed',) + list_filter = ('gtfs_feed', 'type', 'agency',) search_fields = ('long_name', 'short_name', 'id',) ordering = ('long_name',) - autocomplete_fields = ('agency',) + autocomplete_fields = ('agency', 'gtfs_feed',) inlines = (TripInline,) @admin.register(Trip) class TripAdmin(admin.ModelAdmin): - list_display = ('id', 'route', 'service', 'headsign', 'direction_id',) - list_filter = ('direction_id', 'route__transport_type',) + list_display = ('id', 'origin_destination', 'route', 'service', 'headsign', 'direction_id',) + list_filter = ('direction_id', 'route__gtfs_feed',) search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',) ordering = ('route', 'service',) - autocomplete_fields = ('route', 'service',) + autocomplete_fields = ('route', 'service', 'gtfs_feed',) inlines = (StopTimeInline, TripUpdateInline,) @@ -76,28 +100,30 @@ class TripAdmin(admin.ModelAdmin): class StopTimeAdmin(admin.ModelAdmin): list_display = ('trip', 'stop', 'arrival_time', 'departure_time', 'stop_sequence', 'pickup_type', 'drop_off_type',) - list_filter = ('pickup_type', 'drop_off_type', 'trip__route__transport_type',) + list_filter = ('pickup_type', 'drop_off_type', 'trip__route__gtfs_feed',) search_fields = ('trip__id', 'stop__name', 'arrival_time', 'departure_time',) ordering = ('trip', 'stop_sequence',) autocomplete_fields = ('trip', 'stop',) + readonly_fields = ('id',) inlines = (StopTimeUpdateInline,) @admin.register(Calendar) class CalendarAdmin(admin.ModelAdmin): - list_display = ('id', 'transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', + list_display = ('id', 'gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'start_date', 'end_date',) - list_filter = ('transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', + list_filter = ('gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'start_date', 'end_date',) search_fields = ('id', 'start_date', 'end_date',) - ordering = ('transport_type', 'id',) + autocomplete_fields = ('gtfs_feed',) + ordering = ('gtfs_feed', 'id',) inlines = (CalendarDateInline, TripInline,) @admin.register(CalendarDate) class CalendarDateAdmin(admin.ModelAdmin): list_display = ('id', 'service_id', 'date', 'exception_type',) - list_filter = ('exception_type', 'date', 'service__transport_type',) + list_filter = ('exception_type', 'date', 'service__gtfs_feed',) search_fields = ('id', 'date',) ordering = ('date', 'service_id',) @@ -116,6 +142,7 @@ class FeedInfoAdmin(admin.ModelAdmin): 'end_date', 'version',) search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date', 'end_date', 'version',) + autocomplete_fields = ('gtfs_feed',) ordering = ('publisher_name',) @@ -123,7 +150,7 @@ class FeedInfoAdmin(admin.ModelAdmin): class StopTimeUpdateAdmin(admin.ModelAdmin): list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time', 'departure_delay', 'departure_time', 'schedule_relationship',) - list_filter = ('schedule_relationship',) + list_filter = ('schedule_relationship', 'trip_update__trip__gtfs_feed',) search_fields = ('trip_update__trip__id', 'stop_time__stop__name', 'arrival_time', 'departure_time',) ordering = ('trip_update', 'stop_time',) autocomplete_fields = ('trip_update', 'stop_time',) diff --git a/sncfgtfs/fixtures/gtfs_feeds.json b/sncfgtfs/fixtures/gtfs_feeds.json new file mode 100644 index 0000000..6b0f7cd --- /dev/null +++ b/sncfgtfs/fixtures/gtfs_feeds.json @@ -0,0 +1,92 @@ +[ + { + "model": "sncfgtfs.gtfsfeed", + "pk": "FR-SNCF-TGV", + "fields": { + "name": "SNCF - TGV", + "country": "FR", + "feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export_gtfs_voyages.zip", + "rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "FR-SNCF-IC", + "fields": { + "name": "SNCF - Intercités", + "country": "FR", + "feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-intercites-gtfs-last.zip", + "rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "FR-SNCF-TER", + "fields": { + "name": "SNCF - TER", + "country": "FR", + "feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip", + "rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "FR-IDF-TN", + "fields": { + "name": "SNCF - Transilien", + "country": "FR", + "feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/transilien-gtfs.zip", + "rt_feed_url": "" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "FR-EUROSTAR", + "fields": { + "name": "Eurostar", + "country": "FR", + "feed_url": "https://www.data.gouv.fr/fr/datasets/r/9089b550-696e-4ae0-87b5-40ea55a14292", + "rt_feed_url": "" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "IT-FRA-TI", + "fields": { + "name": "Trenitalia France", + "country": "FR", + "feed_url": "https://thello.axelor.com/public/gtfs/gtfs.zip", + "rt_feed_url": "https://thello.axelor.com/public/gtfs/GTFS-RT.bin" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "ES-RENFE", + "fields": { + "name": "Renfe", + "country": "ES", + "feed_url": "https://ssl.renfe.com/gtransit/Fichero_AV_LD/google_transit.zip", + "rt_feed_url": "" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "AT-ÖBB", + "fields": { + "name": "ÖBB", + "country": "AT", + "feed_url": "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip", + "rt_feed_url": "" + } + }, + { + "model": "sncfgtfs.gtfsfeed", + "pk": "CH-ALL", + "fields": { + "name": "Transports suisses", + "country": "CH", + "feed_url": "https://opentransportdata.swiss/fr/dataset/timetable-2024-gtfs2020/permalink", + "rt_feed_url": "https://api.opentransportdata.swiss/gtfsrt2020" + } + } +] diff --git a/sncfgtfs/locale/fr/LC_MESSAGES/django.po b/sncfgtfs/locale/fr/LC_MESSAGES/django.po index 4ba18a6..a833d72 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-02-10 19:57+0100\n" +"POT-Creation-Date: 2024-05-09 19:27+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Emmy D'Anello \n" "Language-Team: LANGUAGE \n" @@ -12,555 +12,808 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: sncfgtfs/models.py:6 -msgid "TGV" -msgstr "TGV" - -#: sncfgtfs/models.py:7 -msgid "TER" -msgstr "TER" - -#: sncfgtfs/models.py:8 -msgid "Intercités" -msgstr "Intercités" - -#: sncfgtfs/models.py:9 -msgid "Transilien" -msgstr "Transilien" - -#: sncfgtfs/models.py:10 -msgid "Eurostar" -msgstr "Eurostar" - #: sncfgtfs/models.py:11 -msgid "Trenitalia" -msgstr "Trenitalia" +msgid "Albania" +msgstr "Albanie" #: sncfgtfs/models.py:12 -msgid "Renfe" -msgstr "Renfe" +msgid "Andorra" +msgstr "Andorre" #: sncfgtfs/models.py:13 -msgid "ÖBB" -msgstr "ÖBB" +msgid "Armenia" +msgstr "Arménie" + +#: sncfgtfs/models.py:14 +msgid "Austria" +msgstr "Autriche" + +#: sncfgtfs/models.py:15 +msgid "Azerbaijan" +msgstr "Azerbaijan" + +#: sncfgtfs/models.py:16 +msgid "Belgium" +msgstr "Belgique" #: sncfgtfs/models.py:17 +msgid "Bosnia and Herzegovina" +msgstr " Bosnie-Herzégovine" + +#: sncfgtfs/models.py:18 +msgid "Bulgaria" +msgstr "Bulgarie" + +#: sncfgtfs/models.py:19 +msgid "Croatia" +msgstr "Croatie" + +#: sncfgtfs/models.py:20 +msgid "Cyprus" +msgstr "Chypre" + +#: sncfgtfs/models.py:21 +msgid "Czech Republic" +msgstr "République Tchèque" + +#: sncfgtfs/models.py:22 +msgid "Denmark" +msgstr "Danemark" + +#: sncfgtfs/models.py:23 +msgid "Estonia" +msgstr "Estonie" + +#: sncfgtfs/models.py:24 +msgid "Finland" +msgstr "Finlande" + +#: sncfgtfs/models.py:25 +msgid "France" +msgstr "France" + +#: sncfgtfs/models.py:26 +msgid "Georgia" +msgstr "Géorgie" + +#: sncfgtfs/models.py:27 +msgid "Germany" +msgstr "Allemagne" + +#: sncfgtfs/models.py:28 +msgid "Greece" +msgstr "Grèce" + +#: sncfgtfs/models.py:29 +msgid "Hungary" +msgstr "Hongrie" + +#: sncfgtfs/models.py:30 +msgid "Iceland" +msgstr "Islande" + +#: sncfgtfs/models.py:31 +msgid "Ireland" +msgstr "Irlande" + +#: sncfgtfs/models.py:32 +msgid "Italy" +msgstr "Italie" + +#: sncfgtfs/models.py:33 +msgid "Latvia" +msgstr "Lettonie" + +#: sncfgtfs/models.py:34 +msgid "Liechtenstein" +msgstr "Liechtenstein" + +#: sncfgtfs/models.py:35 +msgid "Lithuania" +msgstr "Lituanie" + +#: sncfgtfs/models.py:36 +msgid "Luxembourg" +msgstr "Luxembourg" + +#: sncfgtfs/models.py:37 +msgid "Malta" +msgstr "Malte" + +#: sncfgtfs/models.py:38 +msgid "Moldova" +msgstr "Moldavie" + +#: sncfgtfs/models.py:39 +msgid "Monaco" +msgstr "Monaco" + +#: sncfgtfs/models.py:40 +msgid "Montenegro" +msgstr "Monténégro" + +#: sncfgtfs/models.py:41 +msgid "Netherlands" +msgstr "Pays-Bas" + +#: sncfgtfs/models.py:42 +msgid "North Macedonia" +msgstr "Macédoine du Nord" + +#: sncfgtfs/models.py:43 +msgid "Norway" +msgstr "Norvège" + +#: sncfgtfs/models.py:44 +msgid "Poland" +msgstr "Pologne" + +#: sncfgtfs/models.py:45 +msgid "Portugal" +msgstr "Portugal" + +#: sncfgtfs/models.py:46 +msgid "Romania" +msgstr "Roumanie" + +#: sncfgtfs/models.py:47 +msgid "San Marino" +msgstr "Saint-Marin" + +#: sncfgtfs/models.py:48 +msgid "Serbia" +msgstr "Serbie" + +#: sncfgtfs/models.py:49 +msgid "Slovakia" +msgstr "Slovaquie" + +#: sncfgtfs/models.py:50 +msgid "Slovenia" +msgstr "Slovénie" + +#: sncfgtfs/models.py:51 +msgid "Spain" +msgstr "Espagne" + +#: sncfgtfs/models.py:52 +msgid "Sweden" +msgstr "Suède" + +#: sncfgtfs/models.py:53 +msgid "Switzerland" +msgstr "Suisse" + +#: sncfgtfs/models.py:54 +msgid "Turkey" +msgstr "Turquie" + +#: sncfgtfs/models.py:55 +msgid "United Kingdom" +msgstr "Royaume-Uni" + +#: sncfgtfs/models.py:56 +msgid "Ukraine" +msgstr "Ukraine" + +#: sncfgtfs/models.py:60 msgid "Stop/platform" msgstr "Arrêt / quai" -#: sncfgtfs/models.py:18 +#: sncfgtfs/models.py:61 msgid "Station" msgstr "Gare" -#: sncfgtfs/models.py:19 +#: sncfgtfs/models.py:62 msgid "Entrance/exit" msgstr "Entrée / sortie" -#: sncfgtfs/models.py:20 +#: sncfgtfs/models.py:63 msgid "Generic node" msgstr "Nœud générique" -#: sncfgtfs/models.py:21 +#: sncfgtfs/models.py:64 msgid "Boarding area" msgstr "Zone d'embarquement" -#: sncfgtfs/models.py:25 +#: sncfgtfs/models.py:68 msgid "No information" msgstr "Pas d'information" -#: sncfgtfs/models.py:26 +#: sncfgtfs/models.py:69 msgid "Possible" msgstr "Possible" -#: sncfgtfs/models.py:27 sncfgtfs/models.py:57 +#: sncfgtfs/models.py:70 sncfgtfs/models.py:100 msgid "Not possible" msgstr "Impossible" -#: sncfgtfs/models.py:31 +#: sncfgtfs/models.py:74 msgid "Regular" msgstr "Régulier" -#: sncfgtfs/models.py:32 +#: sncfgtfs/models.py:75 msgid "None" msgstr "Aucun" -#: sncfgtfs/models.py:33 +#: sncfgtfs/models.py:76 msgid "Must phone agency" msgstr "Doit téléphoner à l'agence" -#: sncfgtfs/models.py:34 +#: sncfgtfs/models.py:77 msgid "Must coordinate with driver" msgstr "Doit se coordonner avec læ conducteurice" -#: sncfgtfs/models.py:38 +#: sncfgtfs/models.py:81 msgid "Tram" msgstr "Tram" -#: sncfgtfs/models.py:39 +#: sncfgtfs/models.py:82 msgid "Metro" msgstr "Métro" -#: sncfgtfs/models.py:40 +#: sncfgtfs/models.py:83 msgid "Rail" msgstr "Rail" -#: sncfgtfs/models.py:41 +#: sncfgtfs/models.py:84 msgid "Bus" msgstr "Bus" -#: sncfgtfs/models.py:42 +#: sncfgtfs/models.py:85 msgid "Ferry" msgstr "Ferry" -#: sncfgtfs/models.py:43 +#: sncfgtfs/models.py:86 msgid "Cable car" msgstr "Câble" -#: sncfgtfs/models.py:44 +#: sncfgtfs/models.py:87 msgid "Gondola" msgstr "Gondole" -#: sncfgtfs/models.py:45 +#: sncfgtfs/models.py:88 msgid "Funicular" msgstr "Funiculaire" -#: sncfgtfs/models.py:49 +#: sncfgtfs/models.py:92 msgid "Outbound" msgstr "Vers l'extérieur" -#: sncfgtfs/models.py:50 +#: sncfgtfs/models.py:93 msgid "Inbound" msgstr "Vers l'intérieur" -#: sncfgtfs/models.py:54 +#: sncfgtfs/models.py:97 msgid "Recommended" msgstr "Recommandé" -#: sncfgtfs/models.py:55 +#: sncfgtfs/models.py:98 msgid "Timed" msgstr "Correspondance programmée" -#: sncfgtfs/models.py:56 +#: sncfgtfs/models.py:99 msgid "Minimum time" msgstr "Temps de correspondance minimum requis" -#: sncfgtfs/models.py:61 sncfgtfs/models.py:67 +#: sncfgtfs/models.py:104 sncfgtfs/models.py:110 msgid "Added" msgstr "Ajouté" -#: sncfgtfs/models.py:62 +#: sncfgtfs/models.py:105 msgid "Removed" msgstr "Supprimé" -#: sncfgtfs/models.py:66 sncfgtfs/models.py:76 +#: sncfgtfs/models.py:109 sncfgtfs/models.py:119 msgid "Scheduled" msgstr "Planifié" -#: sncfgtfs/models.py:68 sncfgtfs/models.py:79 +#: sncfgtfs/models.py:111 sncfgtfs/models.py:122 msgid "Unscheduled" msgstr "Non planifié" -#: sncfgtfs/models.py:69 +#: sncfgtfs/models.py:112 msgid "Canceled" msgstr "Annulé" -#: sncfgtfs/models.py:70 +#: sncfgtfs/models.py:113 msgid "Replacement" msgstr "Remplacé" -#: sncfgtfs/models.py:71 +#: sncfgtfs/models.py:114 msgid "Duplicated" msgstr "Dupliqué" -#: sncfgtfs/models.py:72 +#: sncfgtfs/models.py:115 msgid "Deleted" msgstr "Supprimé" -#: sncfgtfs/models.py:77 +#: sncfgtfs/models.py:120 msgid "Skipped" msgstr "Sauté" -#: sncfgtfs/models.py:78 +#: sncfgtfs/models.py:121 msgid "No data" msgstr "Pas de données" -#: sncfgtfs/models.py:86 +#: sncfgtfs/models.py:129 +msgid "code" +msgstr "code" + +#: sncfgtfs/models.py:130 +msgid "Unique code of the feed." +msgstr "Code unique du flux." + +#: sncfgtfs/models.py:135 +msgid "name" +msgstr "nom" + +#: sncfgtfs/models.py:137 +msgid "Full name that describes the feed." +msgstr "Nom complet qui décrit le flux." + +#: sncfgtfs/models.py:142 +msgid "country" +msgstr "pays" + +#: sncfgtfs/models.py:147 +msgid "feed URL" +msgstr "URL du flux" + +#: sncfgtfs/models.py:148 +msgid "" +"URL to download the GTFS feed. Must point to a ZIP archive. See https://gtfs." +"org/schedule/ for more information." +msgstr "" +"URL où télécharger le flux GTFS. Doit pointer vers une archive ZIP. Voir " +"https://gtfs.org/fr/schedule/ pour plus d'informations." + +#: sncfgtfs/models.py:153 +msgid "realtime feed URL" +msgstr "URL du flux temps réel" + +#: sncfgtfs/models.py:156 +msgid "" +"URL to download the GTFS-Realtime feed, in the GTFS-RT format. See https://" +"gtfs.org/realtime/ for more information." +msgstr "" +"URL où télécharger le flux GTFS-Temps réel, au format GTFS-RT. Voir https://" +"gtfs.org/fr/realtime/ pour plus d'informations." + +#: sncfgtfs/models.py:161 +msgid "last modified date" +msgstr "Date de dernière modification" + +#: sncfgtfs/models.py:168 +msgid "ETag" +msgstr "ETag" + +#: sncfgtfs/models.py:171 +msgid "" +"If applicable, corresponds to the tag of the last downloaded file. If it is " +"not modified, the file is the same." +msgstr "" +"Si applicable, correspond au tag du dernier fichier téléchargé. S'il n'est " +"pas modifié, le fichier est considéré comme identique." + +#: sncfgtfs/models.py:179 sncfgtfs/models.py:226 sncfgtfs/models.py:326 +#: sncfgtfs/models.py:405 sncfgtfs/models.py:486 sncfgtfs/models.py:696 +#: sncfgtfs/models.py:811 +msgid "GTFS feed" +msgstr "flux GTFS" + +#: sncfgtfs/models.py:180 +msgid "GTFS feeds" +msgstr "flux GTFS" + +#: sncfgtfs/models.py:189 msgid "Agency ID" msgstr "ID de l'agence" -#: sncfgtfs/models.py:91 +#: sncfgtfs/models.py:194 msgid "Agency name" msgstr "Nom de l'agence" -#: sncfgtfs/models.py:95 +#: sncfgtfs/models.py:198 msgid "Agency URL" msgstr "URL de l'agence" -#: sncfgtfs/models.py:100 +#: sncfgtfs/models.py:203 msgid "Agency timezone" msgstr "Fuseau horaire de l'agence" -#: sncfgtfs/models.py:105 +#: sncfgtfs/models.py:208 msgid "Agency language" msgstr "Langue de l'agence" -#: sncfgtfs/models.py:111 +#: sncfgtfs/models.py:214 msgid "Agency phone" msgstr "Téléphone de l'agence" -#: sncfgtfs/models.py:116 +#: sncfgtfs/models.py:219 msgid "Agency email" msgstr "Adresse email de l'agence" -#: sncfgtfs/models.py:124 sncfgtfs/models.py:242 +#: sncfgtfs/models.py:233 sncfgtfs/models.py:356 msgid "Agency" msgstr "Agence" -#: sncfgtfs/models.py:125 +#: sncfgtfs/models.py:234 msgid "Agencies" msgstr "Agences" -#: sncfgtfs/models.py:133 sncfgtfs/models.py:459 +#: sncfgtfs/models.py:243 sncfgtfs/models.py:593 msgid "Stop ID" msgstr "ID de l'arrêt" -#: sncfgtfs/models.py:138 +#: sncfgtfs/models.py:248 msgid "Stop code" msgstr "Code de l'arrêt" -#: sncfgtfs/models.py:144 +#: sncfgtfs/models.py:254 msgid "Stop name" msgstr "Nom de l'arrêt" -#: sncfgtfs/models.py:149 +#: sncfgtfs/models.py:259 msgid "Stop description" msgstr "Description de l'arrêt" -#: sncfgtfs/models.py:154 +#: sncfgtfs/models.py:264 msgid "Stop longitude" msgstr "Longitude de l'arrêt" -#: sncfgtfs/models.py:158 +#: sncfgtfs/models.py:268 msgid "Stop latitude" msgstr "Latitude de l'arrêt" -#: sncfgtfs/models.py:163 +#: sncfgtfs/models.py:273 msgid "Zone ID" msgstr "ID de la zone" -#: sncfgtfs/models.py:167 +#: sncfgtfs/models.py:278 msgid "Stop URL" msgstr "URL de l'arrêt" -#: sncfgtfs/models.py:172 +#: sncfgtfs/models.py:283 msgid "Location type" msgstr "Type de localisation" -#: sncfgtfs/models.py:181 +#: sncfgtfs/models.py:292 msgid "Parent station" msgstr "Gare parente" -#: sncfgtfs/models.py:189 +#: sncfgtfs/models.py:300 msgid "Stop timezone" msgstr "Fuseau horaire de l'arrêt" -#: sncfgtfs/models.py:195 +#: sncfgtfs/models.py:306 msgid "Level ID" msgstr "ID du niveau" -#: sncfgtfs/models.py:200 +#: sncfgtfs/models.py:311 msgid "Wheelchair boarding" msgstr "Embarquement en fauteuil roulant" -#: sncfgtfs/models.py:208 +#: sncfgtfs/models.py:319 msgid "Platform code" msgstr "Code du quai" -#: sncfgtfs/models.py:214 sncfgtfs/models.py:286 sncfgtfs/models.py:560 -msgid "Transport type" -msgstr "Type de transport" - -#: sncfgtfs/models.py:227 +#: sncfgtfs/models.py:338 msgid "Stop" msgstr "Arrêt" -#: sncfgtfs/models.py:228 +#: sncfgtfs/models.py:339 msgid "Stops" msgstr "Arrêts" -#: sncfgtfs/models.py:236 sncfgtfs/models.py:438 sncfgtfs/models.py:577 -#: sncfgtfs/models.py:609 +#: sncfgtfs/models.py:350 sncfgtfs/models.py:572 sncfgtfs/models.py:713 +#: sncfgtfs/models.py:746 msgid "ID" msgstr "Identifiant" -#: sncfgtfs/models.py:248 +#: sncfgtfs/models.py:365 msgid "Route short name" msgstr "Nom court de la ligne" -#: sncfgtfs/models.py:253 +#: sncfgtfs/models.py:370 msgid "Route long name" msgstr "Nom long de la ligne" -#: sncfgtfs/models.py:258 +#: sncfgtfs/models.py:376 msgid "Route description" msgstr "Description de la ligne" -#: sncfgtfs/models.py:263 +#: sncfgtfs/models.py:381 msgid "Route type" msgstr "Type de ligne" -#: sncfgtfs/models.py:268 +#: sncfgtfs/models.py:386 msgid "Route URL" msgstr "URL de la ligne" -#: sncfgtfs/models.py:274 +#: sncfgtfs/models.py:392 msgid "Route color" msgstr "Couleur de la ligne" -#: sncfgtfs/models.py:280 +#: sncfgtfs/models.py:398 msgid "Route text color" msgstr "Couleur du texte de la ligne" -#: sncfgtfs/models.py:294 sncfgtfs/models.py:309 +#: sncfgtfs/models.py:412 sncfgtfs/models.py:428 msgid "Route" msgstr "Ligne" -#: sncfgtfs/models.py:295 +#: sncfgtfs/models.py:413 msgid "Routes" msgstr "Lignes" -#: sncfgtfs/models.py:303 +#: sncfgtfs/models.py:422 msgid "Trip ID" msgstr "ID du trajet" -#: sncfgtfs/models.py:316 sncfgtfs/models.py:583 +#: sncfgtfs/models.py:435 sncfgtfs/models.py:719 msgid "Service" msgstr "Service" -#: sncfgtfs/models.py:322 +#: sncfgtfs/models.py:441 msgid "Trip headsign" msgstr "Destination du trajet" -#: sncfgtfs/models.py:328 +#: sncfgtfs/models.py:447 msgid "Trip short name" msgstr "Nom court du trajet" -#: sncfgtfs/models.py:333 +#: sncfgtfs/models.py:452 msgid "Direction" msgstr "Direction" -#: sncfgtfs/models.py:340 +#: sncfgtfs/models.py:459 msgid "Block ID" msgstr "ID du bloc" -#: sncfgtfs/models.py:346 +#: sncfgtfs/models.py:465 msgid "Shape ID" msgstr "ID de la forme" -#: sncfgtfs/models.py:351 +#: sncfgtfs/models.py:470 msgid "Wheelchair accessible" msgstr "Accessible en fauteuil roulant" -#: sncfgtfs/models.py:358 +#: sncfgtfs/models.py:477 msgid "Bikes allowed" msgstr "Vélos autorisés" -#: sncfgtfs/models.py:365 -msgid "Last update" -msgstr "Dernière mise à jour" +#: sncfgtfs/models.py:500 sncfgtfs/models.py:509 sncfgtfs/models.py:552 +#: sncfgtfs/models.py:554 +msgid "Unknown" +msgstr "Inconnu" -#: sncfgtfs/models.py:430 sncfgtfs/models.py:444 sncfgtfs/models.py:681 +#: sncfgtfs/models.py:557 +msgid "Origin → Destination" +msgstr "Origine → Destination" + +#: sncfgtfs/models.py:563 sncfgtfs/models.py:578 sncfgtfs/models.py:825 msgid "Trip" msgstr "Trajet" -#: sncfgtfs/models.py:431 +#: sncfgtfs/models.py:564 msgid "Trips" msgstr "Trajets" -#: sncfgtfs/models.py:449 sncfgtfs/models.py:731 +#: sncfgtfs/models.py:583 sncfgtfs/models.py:876 msgid "Arrival time" msgstr "Heure d'arrivée" -#: sncfgtfs/models.py:453 sncfgtfs/models.py:739 +#: sncfgtfs/models.py:587 sncfgtfs/models.py:884 msgid "Departure time" msgstr "Heure de départ" -#: sncfgtfs/models.py:464 +#: sncfgtfs/models.py:598 msgid "Stop sequence" msgstr "Séquence de l'arrêt" -#: sncfgtfs/models.py:469 +#: sncfgtfs/models.py:603 msgid "Stop headsign" msgstr "Destination de l'arrêt" -#: sncfgtfs/models.py:474 +#: sncfgtfs/models.py:608 msgid "Pickup type" msgstr "Type de prise en charge" -#: sncfgtfs/models.py:481 +#: sncfgtfs/models.py:615 msgid "Drop off type" msgstr "Type de dépose" -#: sncfgtfs/models.py:488 +#: sncfgtfs/models.py:622 msgid "Timepoint" msgstr "Ponctualité" -#: sncfgtfs/models.py:511 sncfgtfs/models.py:721 +#: sncfgtfs/models.py:645 sncfgtfs/models.py:866 msgid "Stop time" msgstr "Heure d'arrêt" -#: sncfgtfs/models.py:512 +#: sncfgtfs/models.py:646 msgid "Stop times" msgstr "Heures d'arrêt" -#: sncfgtfs/models.py:519 +#: sncfgtfs/models.py:654 msgid "Service ID" msgstr "ID du service" -#: sncfgtfs/models.py:523 +#: sncfgtfs/models.py:658 msgid "Monday" msgstr "Lundi" -#: sncfgtfs/models.py:527 +#: sncfgtfs/models.py:662 msgid "Tuesday" msgstr "Mardi" -#: sncfgtfs/models.py:531 +#: sncfgtfs/models.py:666 msgid "Wednesday" msgstr "Mercredi" -#: sncfgtfs/models.py:535 +#: sncfgtfs/models.py:670 msgid "Thursday" msgstr "Jeudi" -#: sncfgtfs/models.py:539 +#: sncfgtfs/models.py:674 msgid "Friday" msgstr "Vendredi" -#: sncfgtfs/models.py:543 +#: sncfgtfs/models.py:678 msgid "Saturday" msgstr "Samedi" -#: sncfgtfs/models.py:547 +#: sncfgtfs/models.py:682 msgid "Sunday" msgstr "Dimanche" -#: sncfgtfs/models.py:551 sncfgtfs/models.py:687 +#: sncfgtfs/models.py:686 sncfgtfs/models.py:831 msgid "Start date" msgstr "Date de début" -#: sncfgtfs/models.py:555 +#: sncfgtfs/models.py:690 msgid "End date" msgstr "Date de fin" -#: sncfgtfs/models.py:568 +#: sncfgtfs/models.py:703 msgid "Calendar" msgstr "Calendrier" -#: sncfgtfs/models.py:569 +#: sncfgtfs/models.py:704 msgid "Calendars" msgstr "Calendriers" -#: sncfgtfs/models.py:588 +#: sncfgtfs/models.py:724 msgid "Date" msgstr "Date" -#: sncfgtfs/models.py:592 +#: sncfgtfs/models.py:728 msgid "Exception type" msgstr "Type d'exception" -#: sncfgtfs/models.py:600 +#: sncfgtfs/models.py:736 msgid "Calendar date" msgstr "Date du calendrier" -#: sncfgtfs/models.py:601 +#: sncfgtfs/models.py:737 msgid "Calendar dates" msgstr "Dates du calendrier" -#: sncfgtfs/models.py:615 +#: sncfgtfs/models.py:752 msgid "From stop" msgstr "Depuis l'arrêt" -#: sncfgtfs/models.py:622 +#: sncfgtfs/models.py:759 msgid "To stop" msgstr "Jusqu'à l'arrêt" -#: sncfgtfs/models.py:627 +#: sncfgtfs/models.py:764 msgid "Transfer type" msgstr "Type de correspondance" -#: sncfgtfs/models.py:633 +#: sncfgtfs/models.py:770 msgid "Minimum transfer time" msgstr "Temps de correspondance minimum" -#: sncfgtfs/models.py:638 +#: sncfgtfs/models.py:775 msgid "Transfer" msgstr "Correspondance" -#: sncfgtfs/models.py:639 +#: sncfgtfs/models.py:776 msgid "Transfers" msgstr "Correspondances" -#: sncfgtfs/models.py:646 +#: sncfgtfs/models.py:783 msgid "Feed publisher name" msgstr "Nom de l'éditeur du flux" -#: sncfgtfs/models.py:650 +#: sncfgtfs/models.py:787 msgid "Feed publisher URL" msgstr "URL de l'éditeur du flux" -#: sncfgtfs/models.py:655 +#: sncfgtfs/models.py:792 msgid "Feed language" msgstr "Langue du flux" -#: sncfgtfs/models.py:659 +#: sncfgtfs/models.py:796 msgid "Feed start date" msgstr "Date de début du flux" -#: sncfgtfs/models.py:663 +#: sncfgtfs/models.py:800 msgid "Feed end date" msgstr "Date de fin du flux" -#: sncfgtfs/models.py:668 +#: sncfgtfs/models.py:805 msgid "Feed version" msgstr "Version du flux" -#: sncfgtfs/models.py:672 +#: sncfgtfs/models.py:815 msgid "Feed info" msgstr "Information du flux" -#: sncfgtfs/models.py:673 +#: sncfgtfs/models.py:816 msgid "Feed infos" msgstr "Informations du flux" -#: sncfgtfs/models.py:691 +#: sncfgtfs/models.py:835 msgid "Start time" msgstr "Heure de début" -#: sncfgtfs/models.py:695 sncfgtfs/models.py:743 +#: sncfgtfs/models.py:839 sncfgtfs/models.py:888 msgid "Schedule relationship" msgstr "Relation de la planification" -#: sncfgtfs/models.py:704 sncfgtfs/models.py:714 +#: sncfgtfs/models.py:848 sncfgtfs/models.py:859 msgid "Trip update" msgstr "Mise à jour du trajet" -#: sncfgtfs/models.py:705 +#: sncfgtfs/models.py:849 msgid "Trip updates" msgstr "Mises à jour des trajets" -#: sncfgtfs/models.py:727 +#: sncfgtfs/models.py:872 msgid "Arrival delay" msgstr "Retard à l'arrivée" -#: sncfgtfs/models.py:735 +#: sncfgtfs/models.py:880 msgid "Departure delay" msgstr "Retard au départ" -#: sncfgtfs/models.py:752 +#: sncfgtfs/models.py:897 msgid "Stop time update" msgstr "Mise à jour du temps d'arrêt" -#: sncfgtfs/models.py:753 +#: sncfgtfs/models.py:898 msgid "Stop time updates" msgstr "Mises à jour des temps d'arrêt" + +#~ msgid "TGV" +#~ msgstr "TGV" + +#~ msgid "TER" +#~ msgstr "TER" + +#~ msgid "Intercités" +#~ msgstr "Intercités" + +#~ msgid "Transilien" +#~ msgstr "Transilien" + +#~ msgid "Eurostar" +#~ msgstr "Eurostar" + +#~ msgid "Trenitalia" +#~ msgstr "Trenitalia" + +#~ msgid "Renfe" +#~ msgstr "Renfe" + +#~ msgid "ÖBB" +#~ msgstr "ÖBB" + +#~ msgid "Last update" +#~ msgstr "Dernière mise à jour" + +#~ msgid "Transport type" +#~ msgstr "Type de transport" diff --git a/sncfgtfs/management/commands/update_sncf_gtfs.py b/sncfgtfs/management/commands/update_sncf_gtfs.py index aecfc21..1e61dc0 100644 --- a/sncfgtfs/management/commands/update_sncf_gtfs.py +++ b/sncfgtfs/management/commands/update_sncf_gtfs.py @@ -2,27 +2,18 @@ import csv from datetime import datetime, timedelta from io import BytesIO from zipfile import ZipFile +from zoneinfo import ZoneInfo import requests from django.core.management import BaseCommand -from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, Route, Stop, StopTime, Transfer, Trip +from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, Transfer, Trip, \ + PickupType 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", - "ES": "https://www.data.gouv.fr/fr/datasets/r/9089b550-696e-4ae0-87b5-40ea55a14292", - "TI": "https://thello.axelor.com/public/gtfs/gtfs.zip", - "RENFE": "https://ssl.renfe.com/gtransit/Fichero_AV_LD/google_transit.zip", - "OBB": "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip", - } - def add_arguments(self, parser): parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode") parser.add_argument('--bulk_size', type=int, default=1000, help="Number of objects to create in bulk.") @@ -30,36 +21,36 @@ class Command(BaseCommand): help="Do not update the database, only print what would be done.") parser.add_argument('--force', '-f', action='store_true', help="Force the update of the database.") - def handle(self, *args, **options): - bulk_size = options['bulk_size'] - dry_run = options['dry_run'] - force = options['force'] + def handle(self, debug: bool = False, bulk_size: int = 100, dry_run: bool = False, force: bool = False, + verbosity: int = 1, *args, **options): if dry_run: self.stdout.write(self.style.WARNING("Dry run mode activated.")) - if not FeedInfo.objects.exists(): - last_update_date = "1970-01-01" - else: - last_update_date = FeedInfo.objects.get(publisher_name='SNCF_default').version - - for url in self.GTFS_FEEDS.values(): - resp = requests.head(url) - if "Last-Modified" not in resp.headers: - continue - last_modified = resp.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: - if not force: - 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: + for gtfs_feed in GTFSFeed.objects.all(): + gtfs_code = gtfs_feed.code + + if not force: + # Check if the source file was updated + resp = requests.head(gtfs_feed.feed_url, allow_redirects=True) + if 'ETag' in resp.headers and gtfs_feed.etag: + if resp.headers['ETag'] == gtfs_feed.etag: + if verbosity >= 1: + self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}.")) + continue + if 'Last-Modified' in resp.headers and gtfs_feed.last_modified: + last_modified = resp.headers['Last-Modified'] + last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \ + .replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1])) + if last_modified <= gtfs_feed.last_modified: + if verbosity >= 1: + self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}.")) + continue + + self.stdout.write(f"Downloading GTFS feed for {gtfs_feed}...") + resp = requests.get(gtfs_feed.feed_url, allow_redirects=True, stream=True) + with ZipFile(BytesIO(resp.content)) as zipfile: def read_file(filename): lines = zipfile.read(filename).decode().replace('\ufeff', '').splitlines() return [line.strip() for line in lines] @@ -67,23 +58,25 @@ class Command(BaseCommand): agencies = [] for agency_dict in csv.DictReader(read_file("agency.txt")): agency_dict: dict - if transport_type == "ES" \ - and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER': - continue + # if gtfs_code == "FR-EUROSTAR" \ + # and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER': + # continue agency = Agency( - id=agency_dict['agency_id'], + id=f"{gtfs_code}-{agency_dict['agency_id']}", name=agency_dict['agency_name'], url=agency_dict['agency_url'], timezone=agency_dict['agency_timezone'], lang=agency_dict.get('agency_lang', "fr"), phone=agency_dict.get('agency_phone', ""), email=agency_dict.get('agency_email', ""), + gtfs_feed=gtfs_feed, ) agencies.append(agency) if agencies and not dry_run: Agency.objects.bulk_create(agencies, update_conflicts=True, - update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email'], + update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email', + 'gtfs_feed'], unique_fields=['id']) agencies.clear() @@ -91,8 +84,10 @@ class Command(BaseCommand): for stop_dict in csv.DictReader(read_file("stops.txt")): stop_dict: dict stop_id = stop_dict['stop_id'] - if transport_type in ["ES", "TI", "RENFE"]: - stop_id = f"{transport_type}-{stop_id}" + stop_id = f"{gtfs_code}-{stop_id}" + + parent_station_id = stop_dict.get('parent_station', None) + parent_station_id = f"{gtfs_code}-{parent_station_id}" if parent_station_id else None stop = Stop( id=stop_id, @@ -102,13 +97,13 @@ class Command(BaseCommand): lon=stop_dict['stop_lon'], zone_id=stop_dict.get('zone_id', ""), url=stop_dict.get('stop_url', ""), - location_type=stop_dict.get('location_type', 1) or 1, - parent_station_id=stop_dict.get('parent_station', None) or None, + location_type=stop_dict.get('location_type', 0) or 0, + parent_station_id=parent_station_id, 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', ""), - transport_type=transport_type, + gtfs_feed=gtfs_feed, ) stops.append(stop) @@ -119,7 +114,7 @@ class Command(BaseCommand): update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url', 'location_type', 'parent_station_id', 'timezone', 'wheelchair_boarding', 'level_id', 'platform_code', - 'transport_type'], + 'gtfs_feed'], unique_fields=['id']) stops.clear() @@ -127,11 +122,10 @@ class Command(BaseCommand): for route_dict in csv.DictReader(read_file("routes.txt")): route_dict: dict route_id = route_dict['route_id'] - if transport_type == "TI": - route_id = f"{transport_type}-{route_id}" + route_id = f"{gtfs_code}-{route_id}" route = Route( id=route_id, - agency_id=route_dict['agency_id'], + agency_id=f"{gtfs_code}-{route_dict['agency_id']}", short_name=route_dict['route_short_name'], long_name=route_dict['route_long_name'], desc=route_dict.get('route_desc', ""), @@ -139,7 +133,7 @@ class Command(BaseCommand): url=route_dict.get('route_url', ""), color=route_dict.get('route_color', ""), text_color=route_dict.get('route_text_color', ""), - transport_type=transport_type, + gtfs_feed=gtfs_feed, ) routes.append(route) @@ -148,7 +142,7 @@ class Command(BaseCommand): update_conflicts=True, update_fields=['agency_id', 'short_name', 'long_name', 'desc', 'type', 'url', 'color', 'text_color', - 'transport_type'], + 'gtfs_feed'], unique_fields=['id']) routes.clear() if routes and not dry_run: @@ -156,17 +150,17 @@ class Command(BaseCommand): update_conflicts=True, update_fields=['agency_id', 'short_name', 'long_name', 'desc', 'type', 'url', 'color', 'text_color', - 'transport_type'], + 'gtfs_feed'], unique_fields=['id']) routes.clear() - Calendar.objects.filter(transport_type=transport_type).delete() + Calendar.objects.filter(gtfs_feed=gtfs_feed).delete() calendars = {} if "calendar.txt" in zipfile.namelist(): for calendar_dict in csv.DictReader(read_file("calendar.txt")): calendar_dict: dict calendar = Calendar( - id=f"{transport_type}-{calendar_dict['service_id']}", + id=f"{gtfs_code}-{calendar_dict['service_id']}", monday=calendar_dict['monday'], tuesday=calendar_dict['tuesday'], wednesday=calendar_dict['wednesday'], @@ -176,7 +170,7 @@ class Command(BaseCommand): sunday=calendar_dict['sunday'], start_date=calendar_dict['start_date'], end_date=calendar_dict['end_date'], - transport_type=transport_type, + gtfs_feed=gtfs_feed, ) calendars[calendar.id] = calendar @@ -185,14 +179,14 @@ class Command(BaseCommand): update_conflicts=True, update_fields=['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'start_date', - 'end_date', 'transport_type'], + 'end_date', 'gtfs_feed'], unique_fields=['id']) calendars.clear() if calendars and not dry_run: Calendar.objects.bulk_create(calendars.values(), update_conflicts=True, update_fields=['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'start_date', - 'end_date', 'transport_type'], + 'end_date', 'gtfs_feed'], unique_fields=['id']) calendars.clear() @@ -200,8 +194,8 @@ class Command(BaseCommand): for calendar_date_dict in csv.DictReader(read_file("calendar_dates.txt")): 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']}", + id=f"{gtfs_code}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}", + service_id=f"{gtfs_code}-{calendar_date_dict['service_id']}", date=calendar_date_dict['date'], exception_type=calendar_date_dict['exception_type'], ) @@ -209,7 +203,7 @@ class Command(BaseCommand): if calendar_date.service_id not in calendars: calendar = Calendar( - id=f"{transport_type}-{calendar_date_dict['service_id']}", + id=f"{gtfs_code}-{calendar_date_dict['service_id']}", monday=False, tuesday=False, wednesday=False, @@ -219,11 +213,11 @@ class Command(BaseCommand): sunday=False, start_date=calendar_date_dict['date'], end_date=calendar_date_dict['date'], - transport_type=transport_type, + gtfs_feed=gtfs_feed, ) calendars[calendar.id] = calendar else: - calendar = calendars[f"{transport_type}-{calendar_date_dict['service_id']}"] + calendar = calendars[f"{gtfs_code}-{calendar_date_dict['service_id']}"] if calendar.start_date > calendar_date.date: calendar.start_date = calendar_date.date if calendar.end_date < calendar_date.date: @@ -233,7 +227,7 @@ class Command(BaseCommand): Calendar.objects.bulk_create(calendars.values(), batch_size=bulk_size, update_conflicts=True, - update_fields=['start_date', 'end_date'], + update_fields=['start_date', 'end_date', 'gtfs_feed'], unique_fields=['id']) CalendarDate.objects.bulk_create(calendar_dates, batch_size=bulk_size, @@ -248,22 +242,12 @@ class Command(BaseCommand): trip_dict: dict trip_id = trip_dict['trip_id'] route_id = trip_dict['route_id'] - if transport_type in ["TGV", "IC", "TER"]: - trip_id, last_update = trip_id.split(':', 1) - last_update = datetime.fromisoformat(last_update) - elif transport_type in ["ES", "RENFE"]: - trip_id = f"{transport_type}-{trip_id}" - last_update = None - elif transport_type == "TI": - trip_id = f"{transport_type}-{trip_id}" - route_id = f"{transport_type}-{route_id}" - last_update = None - else: - last_update = None + trip_id = f"{gtfs_code}-{trip_id}" + route_id = f"{gtfs_code}-{route_id}" trip = Trip( id=trip_id, route_id=route_id, - service_id=f"{transport_type}-{trip_dict['service_id']}", + service_id=f"{gtfs_code}-{trip_dict['service_id']}", headsign=trip_dict.get('trip_headsign', ""), short_name=trip_dict.get('trip_short_name', ""), direction_id=trip_dict.get('direction_id', None) or None, @@ -271,7 +255,7 @@ class Command(BaseCommand): shape_id=trip_dict.get('shape_id', ""), wheelchair_accessible=trip_dict.get('wheelchair_accessible', None), bikes_allowed=trip_dict.get('bikes_allowed', None), - last_update=last_update, + gtfs_feed=gtfs_feed, ) trips.append(trip) @@ -280,7 +264,7 @@ class Command(BaseCommand): update_conflicts=True, update_fields=['route_id', 'service_id', 'headsign', 'short_name', 'direction_id', 'block_id', 'shape_id', - 'wheelchair_accessible', 'bikes_allowed'], + 'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'], unique_fields=['id']) trips.clear() if trips and not dry_run: @@ -288,7 +272,7 @@ class Command(BaseCommand): update_conflicts=True, update_fields=['route_id', 'service_id', 'headsign', 'short_name', 'direction_id', 'block_id', 'shape_id', - 'wheelchair_accessible', 'bikes_allowed'], + 'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'], unique_fields=['id']) trips.clear() @@ -297,14 +281,10 @@ class Command(BaseCommand): stop_time_dict: dict stop_id = stop_time_dict['stop_id'] - if transport_type in ["ES", "TI", "RENFE"]: - stop_id = f"{transport_type}-{stop_id}" + stop_id = f"{gtfs_code}-{stop_id}" trip_id = stop_time_dict['trip_id'] - if transport_type in ["TGV", "IC", "TER"]: - trip_id = trip_id.split(':', 1)[0] - elif transport_type in ["ES", "TI", "RENFE"]: - trip_id = f"{transport_type}-{trip_id}" + trip_id = f"{gtfs_code}-{trip_id}" arr_time = stop_time_dict['arrival_time'] arr_h, arr_m, arr_s = map(int, arr_time.split(':')) @@ -315,19 +295,16 @@ class Command(BaseCommand): pickup_type = stop_time_dict.get('pickup_type', 0) drop_off_type = stop_time_dict.get('drop_off_type', 0) - if transport_type in ["ES", "RENFE", "OBB"]: - if stop_time_dict['stop_sequence'] == "1": - drop_off_type = 1 - elif arr_time == dep_time: - pickup_type = 1 - elif transport_type == "TI": - if stop_time_dict['stop_sequence'] == "0": - drop_off_type = 1 - elif arr_time == dep_time: - pickup_type = 1 + if stop_time_dict['stop_sequence'] == "1": + # First stop + drop_off_type = PickupType.NONE + elif arr_time == dep_time: + # Last stop + pickup_type = PickupType.NONE st = StopTime( - id=f"{trip_id}-{stop_id}-{stop_time_dict['departure_time']}", + id=f"{gtfs_code}-{stop_time_dict['trip_id']}-{stop_time_dict['stop_id']}" + f"-{stop_time_dict['departure_time']}", trip_id=trip_id, arrival_time=timedelta(seconds=arr_time), departure_time=timedelta(seconds=dep_time), @@ -363,14 +340,13 @@ class Command(BaseCommand): transfer_dict: dict from_stop_id = transfer_dict['from_stop_id'] to_stop_id = transfer_dict['to_stop_id'] - if transport_type in ["ES", "RENFE", "OBB"]: - from_stop_id = f"{transport_type}-{from_stop_id}" - to_stop_id = f"{transport_type}-{to_stop_id}" + from_stop_id = f"{gtfs_code}-{from_stop_id}" + to_stop_id = f"{gtfs_code}-{to_stop_id}" transfer = Transfer( - id=f"{from_stop_id}-{to_stop_id}", - from_stop_id=transfer_dict['from_stop_id'], - to_stop_id=transfer_dict['to_stop_id'], + id=f"{transfer_dict['from_stop_id']}-{transfer_dict['to_stop_id']}", + from_stop_id=from_stop_id, + to_stop_id=to_stop_id, transfer_type=transfer_dict['transfer_type'], min_transfer_time=transfer_dict['min_transfer_time'], ) @@ -395,6 +371,7 @@ class Command(BaseCommand): feed_info_dict: dict FeedInfo.objects.update_or_create( publisher_name=feed_info_dict['feed_publisher_name'], + gtfs_feed=gtfs_feed, defaults=dict( publisher_url=feed_info_dict['feed_publisher_url'], lang=feed_info_dict['feed_lang'], @@ -403,3 +380,12 @@ class Command(BaseCommand): version=feed_info_dict.get('feed_version', 1), ) ) + + if 'ETag' in resp.headers: + gtfs_feed.etag = resp.headers['ETag'] + gtfs_feed.save() + if 'Last-Modified' in resp.headers: + last_modified = resp.headers['Last-Modified'] + gtfs_feed.last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \ + .replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1])) + gtfs_feed.save() diff --git a/sncfgtfs/management/commands/update_sncf_gtfs_rt.py b/sncfgtfs/management/commands/update_sncf_gtfs_rt.py index f78c35f..25851a3 100644 --- a/sncfgtfs/management/commands/update_sncf_gtfs_rt.py +++ b/sncfgtfs/management/commands/update_sncf_gtfs_rt.py @@ -5,8 +5,8 @@ import requests from django.core.management import BaseCommand from django.db.models import Q -from sncfgtfs.gtfs_realtime_pb2 import FeedMessage -from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, LocationType, PickupType, \ +from sncfgtfs.gtfs_realtime_pb2 import FeedMessage, TripUpdate as GTFSTripUpdate +from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, GTFSFeed, LocationType, PickupType, \ Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \ Trip, TripUpdate, TripScheduleRelationship @@ -14,34 +14,33 @@ from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, Locat 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", - "TI": "https://thello.axelor.com/public/gtfs/GTFS-RT.bin", - } - def add_arguments(self, parser): parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode") - def handle(self, debug=False, *args, **options): - for feed_type, feed_url in self.GTFS_RT_FEEDS.items(): - self.stdout.write(f"Updating {feed_type} feed...") + def handle(self, debug: bool = False, verbosity: int = 1, *args, **options): + for gtfs_feed in GTFSFeed.objects.all(): + if not gtfs_feed.rt_feed_url: + if verbosity >= 2: + self.stdout.write(self.style.WARNING(f"No GTFS-RT feed found for {gtfs_feed}.")) + continue + + self.stdout.write(f"Updating GTFS-RT feed for {gtfs_feed}…") + + gtfs_code = gtfs_feed.code feed_message = FeedMessage() - feed_message.ParseFromString(requests.get(feed_url).content) + feed_message.ParseFromString(requests.get(gtfs_feed.rt_feed_url, allow_redirects=True).content) stop_times_updates = [] if debug: - with open(f'feed_message-{feed_type}.txt', 'w') as f: + with open(f'feed_message-{gtfs_code}.txt', 'w') as f: f.write(str(feed_message)) for entity in feed_message.entity: if entity.HasField("trip_update"): trip_update = entity.trip_update trip_id = trip_update.trip.trip_id - if feed_type in ["TGV", "IC", "TER"]: - trip_id = trip_id.split(":", 1)[0] + trip_id = f"{gtfs_code}-{trip_id}" start_date = date(year=int(trip_update.trip.start_date[:4]), month=int(trip_update.trip.start_date[4:6]), @@ -50,7 +49,7 @@ class Command(BaseCommand): if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED: # C'est un trajet nouveau. On crée le trajet associé. - self.create_trip(trip_update, trip_id, start_dt, feed_type) + self.create_trip(trip_update, trip_id, start_dt, gtfs_feed) if not Trip.objects.filter(id=trip_id).exists(): self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.") @@ -68,22 +67,19 @@ class Command(BaseCommand): for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update): stop_id = stop_time_update.stop_id - if stop_id.startswith('StopArea:'): - # On est dans le cadre d'une gare. On cherche le quai associé. - if StopTime.objects.filter(trip_id=trip_id, stop__parent_station_id=stop_id).exists(): - # U - stop = StopTime.objects.get(trip_id=trip_id, stop__parent_station_id=stop_id).stop + stop_id = f"{gtfs_code}-{stop_id}" + if StopTime.objects.filter(trip_id=trip_id, stop=stop_id).exists(): + st = StopTime.objects.filter(trip_id=trip_id, stop=stop_id) + if st.count() > 1: + st = st.get(stop_sequence=stop_sequence) else: - stops = [s for s in Stop.objects.filter(parent_station_id=stop_id).all() - for s2 in StopTime.objects.filter(trip_id=trip_id).all() - if s.stop_type in s2.stop.stop_type - or s2.stop.stop_type in s.stop_type] - stop = stops[0] if stops else Stop.objects.get(id=stop_id) - - st, _created = StopTime.objects.update_or_create( - id=f"{trip_id}-{stop.id}", + st = st.first() + else: + # Stop is added + st = StopTime.objects.create( + id=f"{trip_id}-{stop_time_update.stop_id}", trip_id=trip_id, - stop_id=stop.id, + stop_id=stop_id, defaults={ "stop_sequence": stop_sequence, "arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time, @@ -96,23 +92,16 @@ class Command(BaseCommand): else PickupType.NONE), } ) - elif stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED: - st = StopTime.objects.get(Q(stop=stop_id) | Q(stop__parent_station_id=stop_id), - trip_id=trip_id) + + if stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED: if st.pickup_type != PickupType.NONE or st.drop_off_type != PickupType.NONE: st.pickup_type = PickupType.NONE st.drop_off_type = PickupType.NONE st.save() - else: - qs = StopTime.objects.filter(Q(stop=stop_id) | Q(stop__parent_station_id=stop_id), - trip_id=trip_id) - if qs.count() == 1: - st = qs.first() - else: - st = qs.get(stop_sequence=stop_sequence) - if st.stop_sequence != stop_sequence: - st.stop_sequence = stop_sequence - st.save() + + if st.stop_sequence != stop_sequence: + st.stop_sequence = stop_sequence + st.save() st_update = StopTimeUpdate( trip_update=tu, @@ -136,73 +125,22 @@ class Command(BaseCommand): 'departure_delay', 'departure_time'], unique_fields=['trip_update', 'stop_time']) - def create_trip(self, trip_update, trip_id, start_dt, feed_type): + def create_trip(self, trip_update: GTFSTripUpdate, trip_id: str, start_dt: datetime, gtfs_feed: GTFSFeed) -> None: headsign = trip_id[5:-1] - trip_qs = Trip.objects.all() - trip_ids = trip_qs.values_list('id', flat=True) + gtfs_code = gtfs_feed.code - first_stop_queryset = StopTime.objects.filter( - stop__parent_station_id=trip_update.stop_time_update[0].stop_id, - ).values('trip_id') - last_stop_queryset = StopTime.objects.filter( - stop__parent_station_id=trip_update.stop_time_update[-1].stop_id, - ).values('trip_id') - - trip_ids = trip_ids.intersection(first_stop_queryset).intersection(last_stop_queryset) - # print(trip_id, trip_ids) - 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) - # print(stop_sequence, Stop.objects.get(id=stop_id).name, stop_time_update) - # print(trip_ids) - # print(st_queryset.values('trip_id').all()) - trip_ids_restrict = trip_ids.intersection(st_queryset.values('trip_id')) - if trip_ids_restrict: - trip_ids = trip_ids_restrict - else: - stop = Stop.objects.get(id=stop_id) - self.stdout.write(self.style.WARNING(f"Warning: No trip is found passing by stop " - f"{stop.name} ({stop_id})")) - trip_ids = set(trip_ids) - 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 new train {headsign}") - if not route_ids: - origin_id = trip_update.stop_time_update[0].stop_id - origin = Stop.objects.get(id=origin_id) - destination_id = trip_update.stop_time_update[-1].stop_id - destination = Stop.objects.get(id=destination_id) - trip_name = f"{origin.name} - {destination.name}" - trip_reverse_name = f"{destination.name} - {origin.name}" - route_qs = Route.objects.filter(long_name=trip_name, transport_type=feed_type) - route_reverse_qs = Route.objects.filter(long_name=trip_reverse_name, - transport_type=feed_type) - if route_qs.exists(): - route_ids = set(route_qs.values_list('id', flat=True)) - elif route_reverse_qs.exists(): - route_ids = set(route_reverse_qs.values_list('id', flat=True)) - else: - self.stdout.write(f"Route not found for trip {trip_id} ({trip_name}). Creating new one") - route = Route.objects.create( - id=f"CREATED-{trip_name}", - agency=Agency.objects.filter(routes__transport_type=feed_type).first(), - transport_type=feed_type, - type=RouteType.RAIL, - short_name=trip_name, - long_name=trip_name, - ) - route_ids = {route.id} - self.stdout.write(f"Route {route.id} created for trip {trip_id} ({trip_name})") - elif len(route_ids) > 1: - self.stdout.write(f"Multiple routes found for trip {trip_id}.") - self.stdout.write(", ".join(route_ids)) - route_id = route_ids.pop() + route, _created = Route.objects.get_or_create( + id=f"{gtfs_code}-ADDED-{headsign}", + gtfs_feed=gtfs_feed, + type=RouteType.RAIL, + short_name="ADDED", + long_name="ADDED ROUTE", + ) Calendar.objects.update_or_create( - id=f"{feed_type}-new-{headsign}", + id=f"{gtfs_code}-ADDED-{headsign}", defaults={ - "transport_type": feed_type, + "gtfs_feed": gtfs_feed, "monday": False, "tuesday": False, "wednesday": False, @@ -215,9 +153,9 @@ class Command(BaseCommand): } ) CalendarDate.objects.update_or_create( - id=f"{feed_type}-{headsign}-{trip_update.trip.start_date}", + id=f"{gtfs_code}-ADDED-{headsign}-{trip_update.trip.start_date}", defaults={ - "service_id": f"{feed_type}-new-{headsign}", + "service_id": f"{gtfs_code}-ADDED-{headsign}", "date": trip_update.trip.start_date, "exception_type": ExceptionType.ADDED, } @@ -225,32 +163,17 @@ class Command(BaseCommand): Trip.objects.update_or_create( id=trip_id, defaults={ - "route_id": route_id, - "service_id": f"{feed_type}-new-{headsign}", + "route_id": route.id, + "service_id": f"{gtfs_code}-ADDED-{headsign}", "headsign": headsign, "direction_id": trip_update.trip.direction_id, + "gtfs_feed": gtfs_feed, } ) - sample_trip = Trip.objects.filter(id__in=trip_ids, route_id=route_id) - sample_trip = sample_trip.first() if sample_trip.exists() else None 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 == LocationType.STATION: - if not StopTime.objects.filter(trip_id=trip_id).exists(): - if sample_trip: - stop = StopTime.objects.get(trip_id=sample_trip.id, - stop__parent_station_id=stop_id).stop - elif StopTime.objects.filter(trip_id=trip_id, stop__parent_station_id=stop_id).exists(): - stop = StopTime.objects.get(trip_id=trip_id, stop__parent_station_id=stop_id).stop - else: - stops = [s for s in Stop.objects.filter(parent_station_id=stop_id).all() - for s2 in StopTime.objects.filter(trip_id=trip_id).all() - if s.stop_type in s2.stop.stop_type - or s2.stop.stop_type in s.stop_type] - stop = stops[0] if stops else stop - stop_id = stop.id + stop_id = f"{gtfs_code}-{stop_id}" arr_time = datetime.fromtimestamp(stop_time_update.arrival.time, tz=ZoneInfo("Europe/Paris")) - start_dt @@ -263,7 +186,7 @@ class Command(BaseCommand): and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE StopTime.objects.update_or_create( - id=f"{trip_id}-{stop_id}", + id=f"{trip_id}-{stop_time_update.stop_id}", trip_id=trip_id, defaults={ "stop_id": stop_id, diff --git a/sncfgtfs/migrations/0001_initial.py b/sncfgtfs/migrations/0001_initial.py index aa1accb..57caf90 100644 --- a/sncfgtfs/migrations/0001_initial.py +++ b/sncfgtfs/migrations/0001_initial.py @@ -1,141 +1,18 @@ -# Generated by Django 5.0.1 on 2024-02-10 16:30 +# Generated by Django 5.0.1 on 2024-05-09 17:16 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [] operations = [ migrations.CreateModel( - name="Agency", - fields=[ - ( - "id", - models.CharField( - max_length=255, - primary_key=True, - serialize=False, - verbose_name="Agency ID", - ), - ), - ("name", models.CharField(max_length=255, verbose_name="Agency name")), - ("url", models.URLField(verbose_name="Agency URL")), - ( - "timezone", - models.CharField(max_length=255, verbose_name="Agency timezone"), - ), - ( - "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", - "verbose_name_plural": "Agencies", - "ordering": ("name",), - }, - ), - migrations.CreateModel( - name="Calendar", - fields=[ - ( - "id", - models.CharField( - max_length=255, - primary_key=True, - serialize=False, - verbose_name="Service ID", - ), - ), - ("monday", models.BooleanField(verbose_name="Monday")), - ("tuesday", models.BooleanField(verbose_name="Tuesday")), - ("wednesday", models.BooleanField(verbose_name="Wednesday")), - ("thursday", models.BooleanField(verbose_name="Thursday")), - ("friday", models.BooleanField(verbose_name="Friday")), - ("saturday", models.BooleanField(verbose_name="Saturday")), - ("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"), - ("ES", "Eurostar"), - ("TI", "Trenitalia"), - ("RENFE", "Renfe"), - ("OBB", "ÖBB"), - ], - max_length=255, - verbose_name="Transport type", - ), - ), - ], - options={ - "verbose_name": "Calendar", - "verbose_name_plural": "Calendars", - "ordering": ("id",), - }, - ), - migrations.CreateModel( - name="FeedInfo", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "publisher_name", - models.CharField( - max_length=255, verbose_name="Feed publisher name" - ), - ), - ("publisher_url", models.URLField(verbose_name="Feed publisher URL")), - ( - "lang", - models.CharField(max_length=255, verbose_name="Feed language"), - ), - ("start_date", models.DateField(verbose_name="Feed start date")), - ("end_date", models.DateField(verbose_name="Feed end date")), - ( - "version", - models.CharField(max_length=255, verbose_name="Feed version"), - ), - ], - options={ - "verbose_name": "Feed info", - "verbose_name_plural": "Feed infos", - "ordering": ("publisher_name",), - }, - ), - migrations.CreateModel( - name="CalendarDate", + name="StopTime", fields=[ ( "id", @@ -146,282 +23,53 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("date", models.DateField(verbose_name="Date")), + ("arrival_time", models.DurationField(verbose_name="Arrival time")), + ("departure_time", models.DurationField(verbose_name="Departure time")), + ("stop_sequence", models.IntegerField(verbose_name="Stop sequence")), ( - "exception_type", - models.IntegerField( - choices=[(1, "Added"), (2, "Removed")], - verbose_name="Exception type", - ), - ), - ( - "service", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="dates", - to="sncfgtfs.calendar", - verbose_name="Service", - ), - ), - ], - options={ - "verbose_name": "Calendar date", - "verbose_name_plural": "Calendar dates", - "ordering": ("id",), - }, - ), - migrations.CreateModel( - name="Route", - fields=[ - ( - "id", + "stop_headsign", models.CharField( - max_length=255, - primary_key=True, - serialize=False, - verbose_name="ID", + blank=True, max_length=255, verbose_name="Stop headsign" ), ), ( - "short_name", - models.CharField(max_length=255, verbose_name="Route short name"), - ), - ( - "long_name", - models.CharField(max_length=255, verbose_name="Route long name"), - ), - ( - "desc", - models.CharField( - blank=True, max_length=255, verbose_name="Route description" - ), - ), - ( - "type", + "pickup_type", models.IntegerField( choices=[ - (0, "Tram"), - (1, "Metro"), - (2, "Rail"), - (3, "Bus"), - (4, "Ferry"), - (5, "Cable car"), - (6, "Gondola"), - (7, "Funicular"), - ], - verbose_name="Route type", - ), - ), - ("url", models.URLField(blank=True, verbose_name="Route URL")), - ( - "color", - models.CharField( - blank=True, max_length=255, verbose_name="Route color" - ), - ), - ( - "text_color", - models.CharField( - blank=True, max_length=255, verbose_name="Route text color" - ), - ), - ( - "transport_type", - models.CharField( - choices=[ - ("TGV", "TGV"), - ("TER", "TER"), - ("IC", "Intercités"), - ("TN", "Transilien"), - ("ES", "Eurostar"), - ("TI", "Trenitalia"), - ("RENFE", "Renfe"), - ("OBB", "ÖBB"), - ], - max_length=255, - verbose_name="Transport type", - ), - ), - ( - "agency", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="routes", - to="sncfgtfs.agency", - verbose_name="Agency", - ), - ), - ], - options={ - "verbose_name": "Route", - "verbose_name_plural": "Routes", - "ordering": ("id",), - }, - ), - migrations.CreateModel( - name="Stop", - fields=[ - ( - "id", - models.CharField( - max_length=255, - primary_key=True, - serialize=False, - verbose_name="Stop ID", - ), - ), - ( - "code", - models.CharField( - blank=True, max_length=255, verbose_name="Stop code" - ), - ), - ("name", models.CharField(max_length=255, verbose_name="Stop name")), - ( - "desc", - models.CharField( - blank=True, max_length=255, verbose_name="Stop description" - ), - ), - ("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")), - ("url", models.URLField(blank=True, verbose_name="Stop URL")), - ( - "location_type", - models.IntegerField( - blank=True, - choices=[ - (0, "Stop/platform"), - (1, "Station"), - (2, "Entrance/exit"), - (3, "Generic node"), - (4, "Boarding area"), + (0, "Regular"), + (1, "None"), + (2, "Must phone agency"), + (3, "Must coordinate with driver"), ], default=0, - verbose_name="Location type", - ), - ), - ( - "timezone", - models.CharField( - blank=True, max_length=255, verbose_name="Stop timezone" - ), - ), - ( - "level_id", - models.CharField( - blank=True, max_length=255, verbose_name="Level ID" - ), - ), - ( - "wheelchair_boarding", - models.IntegerField( - blank=True, - choices=[ - (0, "No information"), - (1, "Possible"), - (2, "Not possible"), - ], - default=0, - verbose_name="Wheelchair boarding", - ), - ), - ( - "platform_code", - models.CharField( - blank=True, max_length=255, verbose_name="Platform code" - ), - ), - ( - "transport_type", - models.CharField( - choices=[ - ("TGV", "TGV"), - ("TER", "TER"), - ("IC", "Intercités"), - ("TN", "Transilien"), - ("ES", "Eurostar"), - ("TI", "Trenitalia"), - ("RENFE", "Renfe"), - ("OBB", "ÖBB"), - ], - max_length=255, - verbose_name="Transport type", - ), - ), - ( - "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", - ), - ), - ], - options={ - "verbose_name": "Stop", - "verbose_name_plural": "Stops", - "ordering": ("id",), - }, - ), - migrations.CreateModel( - name="Transfer", - fields=[ - ( - "id", - models.CharField( - max_length=255, - primary_key=True, - serialize=False, - verbose_name="ID", + verbose_name="Pickup type", ), ), ( - "transfer_type", + "drop_off_type", models.IntegerField( choices=[ - (0, "Recommended"), - (1, "Timed"), - (2, "Minimum time"), - (3, "Not possible"), + (0, "Regular"), + (1, "None"), + (2, "Must phone agency"), + (3, "Must coordinate with driver"), ], default=0, - verbose_name="Transfer type", + null=True, + verbose_name="Drop off type", ), ), ( - "min_transfer_time", - models.IntegerField( - blank=True, verbose_name="Minimum transfer time" - ), - ), - ( - "from_stop", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="transfers_from", - to="sncfgtfs.stop", - verbose_name="From stop", - ), - ), - ( - "to_stop", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="transfers_to", - to="sncfgtfs.stop", - verbose_name="To stop", + "timepoint", + models.BooleanField( + default=True, null=True, verbose_name="Timepoint" ), ), ], options={ - "verbose_name": "Transfer", - "verbose_name_plural": "Transfers", - "ordering": ("id",), + "verbose_name": "Stop time", + "verbose_name_plural": "Stop times", }, ), migrations.CreateModel( @@ -494,28 +142,6 @@ class Migration(migrations.Migration): verbose_name="Bikes allowed", ), ), - ( - "last_update", - models.DateTimeField(null=True, verbose_name="Last update"), - ), - ( - "route", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="trips", - to="sncfgtfs.route", - verbose_name="Route", - ), - ), - ( - "service", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="trips", - to="sncfgtfs.calendar", - verbose_name="Service", - ), - ), ], options={ "verbose_name": "Trip", @@ -523,7 +149,213 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name="StopTime", + name="GTFSFeed", + fields=[ + ( + "code", + models.CharField( + help_text="Unique code of the feed.", + max_length=64, + primary_key=True, + serialize=False, + verbose_name="code", + ), + ), + ( + "name", + models.CharField( + help_text="Full name that describes the feed.", + max_length=255, + unique=True, + verbose_name="name", + ), + ), + ( + "country", + models.CharField( + choices=[ + ("AL", "Albania"), + ("AD", "Andorra"), + ("AM", "Armenia"), + ("AT", "Austria"), + ("AZ", "Azerbaijan"), + ("BE", "Belgium"), + ("BA", "Bosnia and Herzegovina"), + ("BG", "Bulgaria"), + ("HR", "Croatia"), + ("CY", "Cyprus"), + ("CZ", "Czech Republic"), + ("DK", "Denmark"), + ("EE", "Estonia"), + ("FI", "Finland"), + ("FR", "France"), + ("GE", "Georgia"), + ("DE", "Germany"), + ("GR", "Greece"), + ("HU", "Hungary"), + ("IS", "Iceland"), + ("IE", "Ireland"), + ("IT", "Italy"), + ("LV", "Latvia"), + ("LI", "Liechtenstein"), + ("LT", "Lithuania"), + ("LU", "Luxembourg"), + ("MT", "Malta"), + ("MD", "Moldova"), + ("MC", "Monaco"), + ("ME", "Montenegro"), + ("NL", "Netherlands"), + ("MK", "North Macedonia"), + ("NO", "Norway"), + ("PL", "Poland"), + ("PT", "Portugal"), + ("RO", "Romania"), + ("SM", "San Marino"), + ("RS", "Serbia"), + ("SK", "Slovakia"), + ("SI", "Slovenia"), + ("ES", "Spain"), + ("SE", "Sweden"), + ("CH", "Switzerland"), + ("TR", "Turkey"), + ("GB", "United Kingdom"), + ("UA", "Ukraine"), + ], + max_length=2, + verbose_name="country", + ), + ), + ( + "feed_url", + models.URLField( + help_text="URL to download the GTFS feed. Must point to a ZIP archive. See https://gtfs.org/schedule/ for more information.", + verbose_name="feed URL", + ), + ), + ( + "rt_feed_url", + models.URLField( + blank=True, + default="", + help_text="URL to download the GTFS-Realtime feed, in the GTFS-RT format. See https://gtfs.org/realtime/ for more information.", + verbose_name="realtime feed URL", + ), + ), + ( + "last_modified", + models.DateTimeField( + default=None, null=True, verbose_name="last modified date" + ), + ), + ( + "etag", + models.CharField( + blank=True, + default="", + help_text="If applicable, corresponds to the tag of the last downloaded file. If it is not modified, the file is the same.", + max_length=255, + verbose_name="ETag", + ), + ), + ], + options={ + "verbose_name": "GTFS feed", + "verbose_name_plural": "GTFS feeds", + "ordering": ("country", "name"), + "indexes": [ + models.Index(fields=["name"], name="sncfgtfs_gt_name_43c613_idx") + ], + }, + ), + migrations.CreateModel( + name="Calendar", + fields=[ + ( + "id", + models.CharField( + max_length=255, + primary_key=True, + serialize=False, + verbose_name="Service ID", + ), + ), + ("monday", models.BooleanField(verbose_name="Monday")), + ("tuesday", models.BooleanField(verbose_name="Tuesday")), + ("wednesday", models.BooleanField(verbose_name="Wednesday")), + ("thursday", models.BooleanField(verbose_name="Thursday")), + ("friday", models.BooleanField(verbose_name="Friday")), + ("saturday", models.BooleanField(verbose_name="Saturday")), + ("sunday", models.BooleanField(verbose_name="Sunday")), + ("start_date", models.DateField(verbose_name="Start date")), + ("end_date", models.DateField(verbose_name="End date")), + ( + "gtfs_feed", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sncfgtfs.gtfsfeed", + verbose_name="GTFS feed", + ), + ), + ], + options={ + "verbose_name": "Calendar", + "verbose_name_plural": "Calendars", + "ordering": ("id",), + }, + ), + migrations.CreateModel( + name="Agency", + fields=[ + ( + "id", + models.CharField( + max_length=255, + primary_key=True, + serialize=False, + verbose_name="Agency ID", + ), + ), + ("name", models.CharField(max_length=255, verbose_name="Agency name")), + ("url", models.URLField(verbose_name="Agency URL")), + ( + "timezone", + models.CharField(max_length=255, verbose_name="Agency timezone"), + ), + ( + "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" + ), + ), + ( + "gtfs_feed", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sncfgtfs.gtfsfeed", + verbose_name="GTFS feed", + ), + ), + ], + options={ + "verbose_name": "Agency", + "verbose_name_plural": "Agencies", + "ordering": ("name",), + }, + ), + migrations.CreateModel( + name="Route", fields=[ ( "id", @@ -534,71 +366,289 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("arrival_time", models.DurationField(verbose_name="Arrival time")), - ("departure_time", models.DurationField(verbose_name="Departure time")), - ("stop_sequence", models.IntegerField(verbose_name="Stop sequence")), ( - "stop_headsign", + "short_name", + models.CharField(max_length=255, verbose_name="Route short name"), + ), + ( + "long_name", models.CharField( - blank=True, max_length=255, verbose_name="Stop headsign" + blank=True, max_length=255, verbose_name="Route long name" ), ), ( - "pickup_type", + "desc", + models.CharField( + blank=True, max_length=255, verbose_name="Route description" + ), + ), + ( + "type", models.IntegerField( choices=[ - (0, "Regular"), - (1, "None"), - (2, "Must phone agency"), - (3, "Must coordinate with driver"), + (0, "Tram"), + (1, "Metro"), + (2, "Rail"), + (3, "Bus"), + (4, "Ferry"), + (5, "Cable car"), + (6, "Gondola"), + (7, "Funicular"), ], - default=0, + verbose_name="Route type", + ), + ), + ("url", models.URLField(blank=True, verbose_name="Route URL")), + ( + "color", + models.CharField( + blank=True, max_length=255, verbose_name="Route color" + ), + ), + ( + "text_color", + models.CharField( + blank=True, max_length=255, verbose_name="Route text color" + ), + ), + ( + "agency", + models.ForeignKey( + blank=True, + default=None, null=True, - verbose_name="Pickup type", + on_delete=django.db.models.deletion.CASCADE, + related_name="routes", + to="sncfgtfs.agency", + verbose_name="Agency", ), ), ( - "drop_off_type", - models.IntegerField( - choices=[ - (0, "Regular"), - (1, "None"), - (2, "Must phone agency"), - (3, "Must coordinate with driver"), - ], - default=0, - null=True, - verbose_name="Drop off type", - ), - ), - ( - "timepoint", - models.BooleanField( - default=True, null=True, verbose_name="Timepoint" - ), - ), - ( - "stop", + "gtfs_feed", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name="stop_times", - to="sncfgtfs.stop", - verbose_name="Stop ID", - ), - ), - ( - "trip", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="stop_times", - to="sncfgtfs.trip", - verbose_name="Trip", + to="sncfgtfs.gtfsfeed", + verbose_name="GTFS feed", ), ), ], options={ - "verbose_name": "Stop time", - "verbose_name_plural": "Stop times", + "verbose_name": "Route", + "verbose_name_plural": "Routes", + "ordering": ("id",), + }, + ), + migrations.CreateModel( + name="Stop", + fields=[ + ( + "id", + models.CharField( + max_length=255, + primary_key=True, + serialize=False, + verbose_name="Stop ID", + ), + ), + ( + "code", + models.CharField( + blank=True, max_length=255, verbose_name="Stop code" + ), + ), + ("name", models.CharField(max_length=255, verbose_name="Stop name")), + ( + "desc", + models.CharField( + blank=True, max_length=255, verbose_name="Stop description" + ), + ), + ("lon", models.FloatField(verbose_name="Stop longitude")), + ("lat", models.FloatField(verbose_name="Stop latitude")), + ( + "zone_id", + models.CharField( + blank=True, max_length=255, verbose_name="Zone ID" + ), + ), + ("url", models.URLField(blank=True, verbose_name="Stop URL")), + ( + "location_type", + models.IntegerField( + blank=True, + choices=[ + (0, "Stop/platform"), + (1, "Station"), + (2, "Entrance/exit"), + (3, "Generic node"), + (4, "Boarding area"), + ], + default=0, + verbose_name="Location type", + ), + ), + ( + "timezone", + models.CharField( + blank=True, max_length=255, verbose_name="Stop timezone" + ), + ), + ( + "level_id", + models.CharField( + blank=True, max_length=255, verbose_name="Level ID" + ), + ), + ( + "wheelchair_boarding", + models.IntegerField( + blank=True, + choices=[ + (0, "No information"), + (1, "Possible"), + (2, "Not possible"), + ], + default=0, + verbose_name="Wheelchair boarding", + ), + ), + ( + "platform_code", + models.CharField( + blank=True, max_length=255, verbose_name="Platform code" + ), + ), + ( + "gtfs_feed", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sncfgtfs.gtfsfeed", + verbose_name="GTFS feed", + ), + ), + ( + "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", + ), + ), + ], + options={ + "verbose_name": "Stop", + "verbose_name_plural": "Stops", + "ordering": ("id",), + }, + ), + 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", + ), + ), + ], + options={ + "verbose_name": "Stop time update", + "verbose_name_plural": "Stop time updates", + "ordering": ("trip_update", "stop_time"), + }, + ), + migrations.AddField( + model_name="stoptime", + name="stop", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stop_times", + to="sncfgtfs.stop", + verbose_name="Stop ID", + ), + ), + migrations.CreateModel( + name="Transfer", + fields=[ + ( + "id", + models.CharField( + max_length=255, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "transfer_type", + models.IntegerField( + choices=[ + (0, "Recommended"), + (1, "Timed"), + (2, "Minimum time"), + (3, "Not possible"), + ], + default=0, + verbose_name="Transfer type", + ), + ), + ( + "min_transfer_time", + models.IntegerField( + blank=True, verbose_name="Minimum transfer time" + ), + ), + ( + "from_stop", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="transfers_from", + to="sncfgtfs.stop", + verbose_name="From stop", + ), + ), + ( + "to_stop", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="transfers_to", + to="sncfgtfs.stop", + verbose_name="To stop", + ), + ), + ], + options={ + "verbose_name": "Transfer", + "verbose_name_plural": "Transfers", + "ordering": ("id",), }, ), migrations.CreateModel( @@ -638,58 +688,224 @@ class Migration(migrations.Migration): "verbose_name": "Trip update", "verbose_name_plural": "Trip updates", "ordering": ("start_date", "trip"), - "unique_together": {("trip", "start_date", "start_time")}, }, ), + migrations.AddField( + model_name="trip", + name="gtfs_feed", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sncfgtfs.gtfsfeed", + verbose_name="GTFS feed", + ), + ), + migrations.AddField( + model_name="trip", + name="route", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="trips", + to="sncfgtfs.route", + verbose_name="Route", + ), + ), + migrations.AddField( + model_name="trip", + name="service", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="trips", + to="sncfgtfs.calendar", + verbose_name="Service", + ), + ), + migrations.AddField( + model_name="stoptime", + name="trip", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stop_times", + to="sncfgtfs.trip", + verbose_name="Trip", + ), + ), migrations.CreateModel( - name="StopTimeUpdate", + name="CalendarDate", fields=[ ( - "stop_time", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, + "id", + models.CharField( + max_length=255, primary_key=True, - related_name="update", serialize=False, - to="sncfgtfs.stoptime", - verbose_name="Stop time", + verbose_name="ID", ), ), - ("arrival_delay", models.DurationField(verbose_name="Arrival delay")), - ("arrival_time", models.DateTimeField(verbose_name="Arrival time")), + ("date", models.DateField(verbose_name="Date")), ( - "departure_delay", - models.DurationField(verbose_name="Departure delay"), - ), - ("departure_time", models.DateTimeField(verbose_name="Departure time")), - ( - "schedule_relationship", + "exception_type", models.IntegerField( - choices=[ - (0, "Scheduled"), - (1, "Skipped"), - (2, "No data"), - (3, "Unscheduled"), - ], - default=0, - verbose_name="Schedule relationship", + choices=[(1, "Added"), (2, "Removed")], + verbose_name="Exception type", ), ), ( - "trip_update", + "service", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name="stop_time_updates", - to="sncfgtfs.tripupdate", - verbose_name="Trip update", + related_name="dates", + to="sncfgtfs.calendar", + verbose_name="Service", ), ), ], options={ - "verbose_name": "Stop time update", - "verbose_name_plural": "Stop time updates", - "ordering": ("trip_update", "stop_time"), - "unique_together": {("trip_update", "stop_time")}, + "verbose_name": "Calendar date", + "verbose_name_plural": "Calendar dates", + "ordering": ("id",), + "indexes": [ + models.Index( + fields=["service"], name="sncfgtfs_ca_service_837ec3_idx" + ), + models.Index(fields=["date"], name="sncfgtfs_ca_date_6c4732_idx"), + ], }, ), + migrations.CreateModel( + name="FeedInfo", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "publisher_name", + models.CharField( + max_length=255, verbose_name="Feed publisher name" + ), + ), + ("publisher_url", models.URLField(verbose_name="Feed publisher URL")), + ( + "lang", + models.CharField(max_length=255, verbose_name="Feed language"), + ), + ("start_date", models.DateField(verbose_name="Feed start date")), + ("end_date", models.DateField(verbose_name="Feed end date")), + ( + "version", + models.CharField(max_length=255, verbose_name="Feed version"), + ), + ( + "gtfs_feed", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sncfgtfs.gtfsfeed", + verbose_name="GTFS feed", + ), + ), + ], + options={ + "verbose_name": "Feed info", + "verbose_name_plural": "Feed infos", + "ordering": ("publisher_name",), + "indexes": [ + models.Index( + fields=["gtfs_feed"], name="sncfgtfs_fe_gtfs_fe_b5e3d7_idx" + ) + ], + }, + ), + migrations.AddIndex( + model_name="calendar", + index=models.Index( + fields=["gtfs_feed"], name="sncfgtfs_ca_gtfs_fe_061858_idx" + ), + ), + migrations.AddIndex( + model_name="agency", + index=models.Index(fields=["name"], name="sncfgtfs_ag_name_fe3bcd_idx"), + ), + migrations.AddIndex( + model_name="agency", + index=models.Index( + fields=["gtfs_feed"], name="sncfgtfs_ag_gtfs_fe_588658_idx" + ), + ), + migrations.AddIndex( + model_name="route", + index=models.Index( + fields=["gtfs_feed"], name="sncfgtfs_ro_gtfs_fe_818b6e_idx" + ), + ), + migrations.AddIndex( + model_name="stop", + index=models.Index(fields=["name"], name="sncfgtfs_st_name_63b266_idx"), + ), + migrations.AddIndex( + model_name="stop", + index=models.Index(fields=["code"], name="sncfgtfs_st_code_5b6ba9_idx"), + ), + migrations.AddIndex( + model_name="stop", + index=models.Index( + fields=["gtfs_feed"], name="sncfgtfs_st_gtfs_fe_a59997_idx" + ), + ), + migrations.AddIndex( + model_name="tripupdate", + index=models.Index(fields=["trip"], name="sncfgtfs_tr_trip_id_56a7e3_idx"), + ), + migrations.AlterUniqueTogether( + name="tripupdate", + unique_together={("trip", "start_date", "start_time")}, + ), + migrations.AddField( + model_name="stoptimeupdate", + name="trip_update", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stop_time_updates", + to="sncfgtfs.tripupdate", + verbose_name="Trip update", + ), + ), + migrations.AddIndex( + model_name="trip", + index=models.Index(fields=["route"], name="sncfgtfs_tr_route_i_003721_idx"), + ), + migrations.AddIndex( + model_name="trip", + index=models.Index( + fields=["gtfs_feed"], name="sncfgtfs_tr_gtfs_fe_55db7e_idx" + ), + ), + migrations.AddIndex( + model_name="stoptime", + index=models.Index(fields=["stop"], name="sncfgtfs_st_stop_id_e3012f_idx"), + ), + migrations.AddIndex( + model_name="stoptime", + index=models.Index(fields=["trip"], name="sncfgtfs_st_trip_id_751dca_idx"), + ), + migrations.AddIndex( + model_name="stoptimeupdate", + index=models.Index( + fields=["trip_update"], name="sncfgtfs_st_trip_up_a7fabf_idx" + ), + ), + migrations.AddIndex( + model_name="stoptimeupdate", + index=models.Index( + fields=["stop_time"], name="sncfgtfs_st_stop_ti_96270f_idx" + ), + ), + migrations.AlterUniqueTogether( + name="stoptimeupdate", + unique_together={("trip_update", "stop_time")}, + ), ] diff --git a/sncfgtfs/models.py b/sncfgtfs/models.py index 3ef0b94..9f764f2 100644 --- a/sncfgtfs/models.py +++ b/sncfgtfs/models.py @@ -2,15 +2,58 @@ 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") - EUROSTAR = "ES", _("Eurostar") - TRENITALIA = "TI", _("Trenitalia") - RENFE = "RENFE", _("Renfe") - OBB = "OBB", _("ÖBB") +class Country(models.TextChoices): + """ + Country list by ISO 3166-1 alpha-2 code. + Only countries that are member of the Council of Europe + are listed for now. + """ + ALBANIA = "AL", _("Albania") + ANDORRA = "AD", _("Andorra") + ARMENIA = "AM", _("Armenia") + AUSTRIA = "AT", _("Austria") + AZERBAIJAN = "AZ", _("Azerbaijan") + BELGIUM = "BE", _("Belgium") + BOSNIA_AND_HERZEGOVINA = "BA", _("Bosnia and Herzegovina") + BULGARIA = "BG", _("Bulgaria") + CROATIA = "HR", _("Croatia") + CYPRUS = "CY", _("Cyprus") + CZECH_REPUBLIC = "CZ", _("Czech Republic") + DENMARK = "DK", _("Denmark") + ESTONIA = "EE", _("Estonia") + FINLAND = "FI", _("Finland") + FRANCE = "FR", _("France") + GEORGIA = "GE", _("Georgia") + GERMANY = "DE", _("Germany") + GREECE = "GR", _("Greece") + HUNGARY = "HU", _("Hungary") + ICELAND = "IS", _("Iceland") + IRELAND = "IE", _("Ireland") + ITALY = "IT", _("Italy") + LATVIA = "LV", _("Latvia") + LIECHTENSTEIN = "LI", _("Liechtenstein") + LITHUANIA = "LT", _("Lithuania") + LUXEMBOURG = "LU", _("Luxembourg") + MALTA = "MT", _("Malta") + MOLDOVA = "MD", _("Moldova") + MONACO = "MC", _("Monaco") + MONTENEGRO = "ME", _("Montenegro") + NETHERLANDS = "NL", _("Netherlands") + NORTH_MACEDONIA = "MK", _("North Macedonia") + NORWAY = "NO", _("Norway") + POLAND = "PL", _("Poland") + PORTUGAL = "PT", _("Portugal") + ROMANIA = "RO", _("Romania") + SAN_MARINO = "SM", _("San Marino") + SERBIA = "RS", _("Serbia") + SLOVAKIA = "SK", _("Slovakia") + SLOVENIA = "SI", _("Slovenia") + SPAIN = "ES", _("Spain") + SWEDEN = "SE", _("Sweden") + SWITZERLAND = "CH", _("Switzerland") + TURKEY = "TR", _("Turkey") + UNITED_KINGDOM = "GB", _("United Kingdom") + UKRAINE = "UA", _("Ukraine") class LocationType(models.IntegerChoices): @@ -79,6 +122,66 @@ class StopScheduleRelationship(models.IntegerChoices): UNSCHEDULED = 3, _("Unscheduled") +class GTFSFeed(models.Model): + code = models.CharField( + primary_key=True, + max_length=64, + verbose_name=_("code"), + help_text=_("Unique code of the feed.") + ) + + name = models.CharField( + max_length=255, + verbose_name=_("name"), + unique=True, + help_text=_("Full name that describes the feed."), + ) + + country = models.CharField( + max_length=2, + verbose_name=_("country"), + choices=Country, + ) + + feed_url = models.URLField( + verbose_name=_("feed URL"), + help_text=_("URL to download the GTFS feed. Must point to a ZIP archive. " + "See https://gtfs.org/schedule/ for more information."), + ) + + rt_feed_url = models.URLField( + verbose_name=_("realtime feed URL"), + blank=True, + default="", + help_text=_("URL to download the GTFS-Realtime feed, in the GTFS-RT format. " + "See https://gtfs.org/realtime/ for more information."), + ) + + last_modified = models.DateTimeField( + verbose_name=_("last modified date"), + null=True, + default=None, + ) + + etag = models.CharField( + max_length=255, + verbose_name=_("ETag"), + blank=True, + default="", + help_text=_("If applicable, corresponds to the tag of the last downloaded file. " + "If it is not modified, the file is the same."), + ) + + def __str__(self): + return f"{self.name} ({self.code})" + + class Meta: + verbose_name = _("GTFS feed") + verbose_name_plural = _("GTFS feeds") + ordering = ('country', 'name',) + indexes = (models.Index(fields=['name']),) + + class Agency(models.Model): id = models.CharField( max_length=255, @@ -117,6 +220,12 @@ class Agency(models.Model): blank=True, ) + gtfs_feed = models.ForeignKey( + GTFSFeed, + on_delete=models.CASCADE, + verbose_name=_("GTFS feed"), + ) + def __str__(self): return self.name @@ -124,6 +233,7 @@ class Agency(models.Model): verbose_name = _("Agency") verbose_name_plural = _("Agencies") ordering = ("name",) + indexes = (models.Index(fields=['name']), models.Index(fields=['gtfs_feed']),) class Stop(models.Model): @@ -161,6 +271,7 @@ class Stop(models.Model): zone_id = models.CharField( max_length=255, verbose_name=_("Zone ID"), + blank=True, ) url = models.URLField( @@ -209,10 +320,10 @@ class Stop(models.Model): blank=True, ) - transport_type = models.CharField( - max_length=255, - verbose_name=_("Transport type"), - choices=TransportType, + gtfs_feed = models.ForeignKey( + GTFSFeed, + on_delete=models.CASCADE, + verbose_name=_("GTFS feed"), ) @property @@ -227,6 +338,9 @@ class Stop(models.Model): verbose_name = _("Stop") verbose_name_plural = _("Stops") ordering = ("id",) + indexes = (models.Index(fields=['name']), + models.Index(fields=['code']), + models.Index(fields=['gtfs_feed']),) class Route(models.Model): @@ -241,6 +355,9 @@ class Route(models.Model): on_delete=models.CASCADE, verbose_name=_("Agency"), related_name="routes", + null=True, + blank=True, + default=None, ) short_name = models.CharField( @@ -251,6 +368,7 @@ class Route(models.Model): long_name = models.CharField( max_length=255, verbose_name=_("Route long name"), + blank=True, ) desc = models.CharField( @@ -281,19 +399,20 @@ class Route(models.Model): blank=True, ) - transport_type = models.CharField( - max_length=255, - verbose_name=_("Transport type"), - choices=TransportType, + gtfs_feed = models.ForeignKey( + GTFSFeed, + on_delete=models.CASCADE, + verbose_name=_("GTFS feed"), ) def __str__(self): - return f"{self.long_name}" + return self.long_name or self.short_name class Meta: verbose_name = _("Route") verbose_name_plural = _("Routes") ordering = ("id",) + indexes = (models.Index(fields=['gtfs_feed']),) class Trip(models.Model): @@ -361,21 +480,24 @@ class Trip(models.Model): null=True, ) - last_update = models.DateTimeField( - verbose_name=_("Last update"), - null=True, + gtfs_feed = models.ForeignKey( + GTFSFeed, + on_delete=models.CASCADE, + verbose_name=_("GTFS feed"), ) @property - def origin(self): - return self.stop_times.order_by('stop_sequence').first().stop + def origin(self) -> Stop | None: + return self.stop_times.order_by('stop_sequence').first().stop if self.stop_times.exists() else None @property - def destination(self): - return self.stop_times.order_by('-stop_sequence').first().stop + def destination(self) -> Stop | None: + return self.stop_times.order_by('-stop_sequence').first().stop if self.stop_times.exists() else None @property def departure_time(self): + if not self.stop_times.exists(): + return _("Unknown") dep_time = self.stop_times.order_by('stop_sequence').first().departure_time hours = int(dep_time.total_seconds() // 3600) minutes = int((dep_time.total_seconds() % 3600) // 60) @@ -383,6 +505,8 @@ class Trip(models.Model): @property def arrival_time(self): + if not self.stop_times.exists(): + return _("Unknown") arr_time = self.stop_times.order_by('-stop_sequence').first().arrival_time hours = int(arr_time.total_seconds() // 3600) minutes = int((arr_time.total_seconds() % 3600) // 60) @@ -390,14 +514,14 @@ class Trip(models.Model): @property def train_type(self): - if self.route.transport_type == TransportType.TRANSILIEN: + if self.gtfs_feed.code == "FR-IDF-TN": return self.route.short_name else: return self.origin.stop_type @property def train_number(self): - if self.route.transport_type == TransportType.TRANSILIEN: + if self.gtfs_feed.code == "FR-IDF-TN": return self.short_name else: return self.headsign @@ -422,13 +546,23 @@ class Trip(models.Model): return "404042" return "000000" + @property + def origin_destination(self): + origin = self.origin + origin = origin.name if origin else _("Unknown") + destination = self.destination + destination = destination.name if destination else _("Unknown") + return f"{origin} {self.departure_time} → {destination} {self.arrival_time}" + + origin_destination.fget.short_description = _("Origin → Destination") + def __str__(self): - return f"{self.origin.name} {self.departure_time} → {self.destination.name} {self.arrival_time}" \ - f" - {self.service_id}" + return self.origin_destination class Meta: verbose_name = _("Trip") verbose_name_plural = _("Trips") + indexes = (models.Index(fields=['route']), models.Index(fields=['gtfs_feed']),) class StopTime(models.Model): @@ -510,6 +644,7 @@ class StopTime(models.Model): class Meta: verbose_name = _("Stop time") verbose_name_plural = _("Stop times") + indexes = (models.Index(fields=['stop']), models.Index(fields=['trip']),) class Calendar(models.Model): @@ -555,10 +690,10 @@ class Calendar(models.Model): verbose_name=_("End date"), ) - transport_type = models.CharField( - max_length=255, - verbose_name=_("Transport type"), - choices=TransportType, + gtfs_feed = models.ForeignKey( + GTFSFeed, + on_delete=models.CASCADE, + verbose_name=_("GTFS feed"), ) def __str__(self): @@ -568,6 +703,7 @@ class Calendar(models.Model): verbose_name = _("Calendar") verbose_name_plural = _("Calendars") ordering = ("id",) + indexes = (models.Index(fields=['gtfs_feed']),) class CalendarDate(models.Model): @@ -600,6 +736,7 @@ class CalendarDate(models.Model): verbose_name = _("Calendar date") verbose_name_plural = _("Calendar dates") ordering = ("id",) + indexes = (models.Index(fields=['service']), models.Index(fields=['date']),) class Transfer(models.Model): @@ -668,10 +805,17 @@ class FeedInfo(models.Model): verbose_name=_("Feed version"), ) + gtfs_feed = models.ForeignKey( + GTFSFeed, + on_delete=models.CASCADE, + verbose_name=_("GTFS feed"), + ) + class Meta: verbose_name = _("Feed info") verbose_name_plural = _("Feed infos") ordering = ("publisher_name",) + indexes = (models.Index(fields=['gtfs_feed']),) class TripUpdate(models.Model): @@ -705,6 +849,7 @@ class TripUpdate(models.Model): verbose_name_plural = _("Trip updates") ordering = ("start_date", "trip",) unique_together = ("trip", "start_date", "start_time",) + indexes = (models.Index(fields=['trip']),) class StopTimeUpdate(models.Model): @@ -753,3 +898,4 @@ class StopTimeUpdate(models.Model): verbose_name_plural = _("Stop time updates") ordering = ("trip_update", "stop_time",) unique_together = ("trip_update", "stop_time",) + indexes = (models.Index(fields=['trip_update']), models.Index(fields=['stop_time']),)