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
case "Trenitalia":
+ case "Trenitalia France":
return
case "RENFE":
return
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']),)