Restructurate GTFS feeds into dedicated models

This commit is contained in:
Emmy D'Anello 2024-05-09 19:28:19 +02:00
parent 820fc0cc19
commit 11949228ee
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
11 changed files with 1594 additions and 950 deletions

1
.gitignore vendored
View File

@ -35,6 +35,7 @@ coverage
secrets.py secrets.py
settings_local.py settings_local.py
*.log *.log
*.txt
media/ media/
output/ output/
/static/ /static/

View File

@ -40,23 +40,23 @@ function AutocompleteStop(params) {
} }
function getOptionGroup(option) { function getOptionGroup(option) {
switch (option.transport_type) { switch (option.gtfs_feed) {
case "TGV": case "FR-SNCF-TGV":
case "IC": case "FR-SNCF-IC":
case "TER": case "FR-SNCF-TER":
return "TGV/TER/Intercités" return "TGV/TER/Intercités"
case "TN": case "FR-IDF-TN":
return "Transilien" return "Transilien"
case "ES": case "FR-EUROSTAR":
return "Eurostar" return "Eurostar"
case "TI": case "IT-FRA-TI":
return "Trenitalia France" return "Trenitalia France"
case "RENFE": case "ES-RENFE":
return "RENFE" return "RENFE"
case "OBB": case "AT-OBB":
return "ÖBB" return "ÖBB"
default: default:
return option.transport_type return option.gtfs_feed
} }
} }

View File

@ -229,10 +229,10 @@ function TrainRow({train, tableType, date, time}) {
} }
function getTrainType(train, trip, route) { function getTrainType(train, trip, route) {
switch (route.transport_type) { switch (route.gtfs_feed) {
case "TGV": case "FR-SNCF-TGV":
case "TER": case "FR-SNCF-IC":
case "IC": case "FR-SNCF-TER":
let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0] let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0]
switch (trainType) { switch (trainType) {
case "Train TER": case "Train TER":
@ -244,20 +244,20 @@ function getTrainType(train, trip, route) {
default: default:
return trainType return trainType
} }
case "TN": case "FR-IDF-TN":
return route.short_name return route.short_name
case "ES": case "FR-EUROSTAR":
return "Eurostar" return "Eurostar"
case "TI": case "IT-FRA-TI":
return "Trenitalia" return "Trenitalia France"
case "RENFE": case "ES-RENFE":
return "RENFE" return "RENFE"
case "OBB": case "AT-OBB":
if (trip.short_name.startsWith("NJ")) if (trip.short_name?.startsWith("NJ"))
return "NJ" return "NJ"
return "ÖBB" return "ÖBB"
default: default:
return "" return trip.short_name?.split(" ")[0]
} }
} }
@ -280,6 +280,7 @@ function getTrainTypeDisplay(trainType) {
case "Eurostar": case "Eurostar":
return <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" /> return <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" />
case "Trenitalia": case "Trenitalia":
case "Trenitalia France":
return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" /> return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" />
case "RENFE": case "RENFE":
return <img src="/renfe.svg" alt="RENFE" width="80%" /> return <img src="/renfe.svg" alt="RENFE" width="80%" />

View File

@ -11,12 +11,11 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from sncf.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \ from sncf.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \
StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \ StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \
FeedInfoSerializer, TripUpdateSerializer, StopTimeUpdateSerializer FeedInfoSerializer, TripUpdateSerializer, StopTimeUpdateSerializer
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, StopTimeUpdate, \
Transfer, FeedInfo, TripUpdate, StopTimeUpdate Transfer, Trip, TripUpdate
CACHE_CONTROL = cache_control(max_age=7200) CACHE_CONTROL = cache_control(max_age=7200)
LAST_MODIFIED = last_modified(lambda *args, **kwargs: datetime.fromisoformat( LAST_MODIFIED = last_modified(lambda *args, **kwargs: GTFSFeed.objects.order_by('-last_modified').first().last_modified)
FeedInfo.objects.get(publisher_name="SNCF_default").version))
LOOKUP_VALUE_REGEX = r"[\w.: |-]+" LOOKUP_VALUE_REGEX = r"[\w.: |-]+"

View File

@ -1,26 +1,38 @@
from django.contrib import admin from django.contrib import admin
from django.forms import BaseInlineFormSet
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \ from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, \
Transfer, FeedInfo, StopTimeUpdate, TripUpdate 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): class CalendarDateInline(admin.TabularInline):
model = CalendarDate model = CalendarDate
extra = 0 extra = 0
formset = LimitModelFormset
class TripInline(admin.TabularInline): class TripInline(admin.TabularInline):
model = Trip model = Trip
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('route', 'service',) autocomplete_fields = ('route', 'service',)
show_change_link = True show_change_link = True
ordering = ('service',) ordering = ('service',)
readonly_fields = ('gtfs_feed',)
class StopTimeInline(admin.TabularInline): class StopTimeInline(admin.TabularInline):
model = StopTime model = StopTime
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('stop',) autocomplete_fields = ('stop',)
readonly_fields = ('id',)
show_change_link = True show_change_link = True
ordering = ('stop_sequence',) ordering = ('stop_sequence',)
@ -28,47 +40,59 @@ class StopTimeInline(admin.TabularInline):
class TripUpdateInline(admin.StackedInline): class TripUpdateInline(admin.StackedInline):
model = TripUpdate model = TripUpdate
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('trip',) autocomplete_fields = ('trip',)
class StopTimeUpdateInline(admin.StackedInline): class StopTimeUpdateInline(admin.StackedInline):
model = StopTimeUpdate model = StopTimeUpdate
extra = 0 extra = 0
formset = LimitModelFormset
autocomplete_fields = ('trip_update', 'stop_time',) 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) @admin.register(Agency)
class AgencyAdmin(admin.ModelAdmin): 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',) search_fields = ('name',)
autocomplete_fields = ('gtfs_feed',)
@admin.register(Stop) @admin.register(Stop)
class StopAdmin(admin.ModelAdmin): class StopAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'lat', 'lon', 'location_type',) list_display = ('name', 'id', 'lat', 'lon', 'location_type',)
list_filter = ('location_type', 'transport_type',) list_filter = ('location_type', 'gtfs_feed',)
search_fields = ('name', 'id',) search_fields = ('name', 'id',)
ordering = ('name',) ordering = ('name',)
autocomplete_fields = ('parent_station',) autocomplete_fields = ('parent_station', 'gtfs_feed',)
@admin.register(Route) @admin.register(Route)
class RouteAdmin(admin.ModelAdmin): class RouteAdmin(admin.ModelAdmin):
list_display = ('short_name', 'long_name', 'id', 'type',) list_display = ('__str__', 'id', 'type', 'gtfs_feed',)
list_filter = ('transport_type', 'type', 'agency',) list_filter = ('gtfs_feed', 'type', 'agency',)
search_fields = ('long_name', 'short_name', 'id',) search_fields = ('long_name', 'short_name', 'id',)
ordering = ('long_name',) ordering = ('long_name',)
autocomplete_fields = ('agency',) autocomplete_fields = ('agency', 'gtfs_feed',)
inlines = (TripInline,) inlines = (TripInline,)
@admin.register(Trip) @admin.register(Trip)
class TripAdmin(admin.ModelAdmin): class TripAdmin(admin.ModelAdmin):
list_display = ('id', 'route', 'service', 'headsign', 'direction_id',) list_display = ('id', 'origin_destination', 'route', 'service', 'headsign', 'direction_id',)
list_filter = ('direction_id', 'route__transport_type',) list_filter = ('direction_id', 'route__gtfs_feed',)
search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',) search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',)
ordering = ('route', 'service',) ordering = ('route', 'service',)
autocomplete_fields = ('route', 'service',) autocomplete_fields = ('route', 'service', 'gtfs_feed',)
inlines = (StopTimeInline, TripUpdateInline,) inlines = (StopTimeInline, TripUpdateInline,)
@ -76,28 +100,30 @@ class TripAdmin(admin.ModelAdmin):
class StopTimeAdmin(admin.ModelAdmin): class StopTimeAdmin(admin.ModelAdmin):
list_display = ('trip', 'stop', 'arrival_time', 'departure_time', list_display = ('trip', 'stop', 'arrival_time', 'departure_time',
'stop_sequence', 'pickup_type', 'drop_off_type',) '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',) search_fields = ('trip__id', 'stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip', 'stop_sequence',) ordering = ('trip', 'stop_sequence',)
autocomplete_fields = ('trip', 'stop',) autocomplete_fields = ('trip', 'stop',)
readonly_fields = ('id',)
inlines = (StopTimeUpdateInline,) inlines = (StopTimeUpdateInline,)
@admin.register(Calendar) @admin.register(Calendar)
class CalendarAdmin(admin.ModelAdmin): 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',) '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',) 'start_date', 'end_date',)
search_fields = ('id', '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,) inlines = (CalendarDateInline, TripInline,)
@admin.register(CalendarDate) @admin.register(CalendarDate)
class CalendarDateAdmin(admin.ModelAdmin): class CalendarDateAdmin(admin.ModelAdmin):
list_display = ('id', 'service_id', 'date', 'exception_type',) 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',) search_fields = ('id', 'date',)
ordering = ('date', 'service_id',) ordering = ('date', 'service_id',)
@ -116,6 +142,7 @@ class FeedInfoAdmin(admin.ModelAdmin):
'end_date', 'version',) 'end_date', 'version',)
search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date', search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date',
'end_date', 'version',) 'end_date', 'version',)
autocomplete_fields = ('gtfs_feed',)
ordering = ('publisher_name',) ordering = ('publisher_name',)
@ -123,7 +150,7 @@ class FeedInfoAdmin(admin.ModelAdmin):
class StopTimeUpdateAdmin(admin.ModelAdmin): class StopTimeUpdateAdmin(admin.ModelAdmin):
list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time', list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time',
'departure_delay', 'departure_time', 'schedule_relationship',) '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',) search_fields = ('trip_update__trip__id', 'stop_time__stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip_update', 'stop_time',) ordering = ('trip_update', 'stop_time',)
autocomplete_fields = ('trip_update', 'stop_time',) autocomplete_fields = ('trip_update', 'stop_time',)

View File

@ -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"
}
}
]

View File

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 1.0\n" "Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <ynerant@emy.lu>\n" "Last-Translator: Emmy D'Anello <ynerant@emy.lu>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -12,555 +12,808 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\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 #: sncfgtfs/models.py:11
msgid "Trenitalia" msgid "Albania"
msgstr "Trenitalia" msgstr "Albanie"
#: sncfgtfs/models.py:12 #: sncfgtfs/models.py:12
msgid "Renfe" msgid "Andorra"
msgstr "Renfe" msgstr "Andorre"
#: sncfgtfs/models.py:13 #: sncfgtfs/models.py:13
msgid "ÖBB" msgid "Armenia"
msgstr "ÖBB" 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 #: 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" msgid "Stop/platform"
msgstr "Arrêt / quai" msgstr "Arrêt / quai"
#: sncfgtfs/models.py:18 #: sncfgtfs/models.py:61
msgid "Station" msgid "Station"
msgstr "Gare" msgstr "Gare"
#: sncfgtfs/models.py:19 #: sncfgtfs/models.py:62
msgid "Entrance/exit" msgid "Entrance/exit"
msgstr "Entrée / sortie" msgstr "Entrée / sortie"
#: sncfgtfs/models.py:20 #: sncfgtfs/models.py:63
msgid "Generic node" msgid "Generic node"
msgstr "Nœud générique" msgstr "Nœud générique"
#: sncfgtfs/models.py:21 #: sncfgtfs/models.py:64
msgid "Boarding area" msgid "Boarding area"
msgstr "Zone d'embarquement" msgstr "Zone d'embarquement"
#: sncfgtfs/models.py:25 #: sncfgtfs/models.py:68
msgid "No information" msgid "No information"
msgstr "Pas d'information" msgstr "Pas d'information"
#: sncfgtfs/models.py:26 #: sncfgtfs/models.py:69
msgid "Possible" msgid "Possible"
msgstr "Possible" msgstr "Possible"
#: sncfgtfs/models.py:27 sncfgtfs/models.py:57 #: sncfgtfs/models.py:70 sncfgtfs/models.py:100
msgid "Not possible" msgid "Not possible"
msgstr "Impossible" msgstr "Impossible"
#: sncfgtfs/models.py:31 #: sncfgtfs/models.py:74
msgid "Regular" msgid "Regular"
msgstr "Régulier" msgstr "Régulier"
#: sncfgtfs/models.py:32 #: sncfgtfs/models.py:75
msgid "None" msgid "None"
msgstr "Aucun" msgstr "Aucun"
#: sncfgtfs/models.py:33 #: sncfgtfs/models.py:76
msgid "Must phone agency" msgid "Must phone agency"
msgstr "Doit téléphoner à l'agence" msgstr "Doit téléphoner à l'agence"
#: sncfgtfs/models.py:34 #: sncfgtfs/models.py:77
msgid "Must coordinate with driver" msgid "Must coordinate with driver"
msgstr "Doit se coordonner avec læ conducteurice" msgstr "Doit se coordonner avec læ conducteurice"
#: sncfgtfs/models.py:38 #: sncfgtfs/models.py:81
msgid "Tram" msgid "Tram"
msgstr "Tram" msgstr "Tram"
#: sncfgtfs/models.py:39 #: sncfgtfs/models.py:82
msgid "Metro" msgid "Metro"
msgstr "Métro" msgstr "Métro"
#: sncfgtfs/models.py:40 #: sncfgtfs/models.py:83
msgid "Rail" msgid "Rail"
msgstr "Rail" msgstr "Rail"
#: sncfgtfs/models.py:41 #: sncfgtfs/models.py:84
msgid "Bus" msgid "Bus"
msgstr "Bus" msgstr "Bus"
#: sncfgtfs/models.py:42 #: sncfgtfs/models.py:85
msgid "Ferry" msgid "Ferry"
msgstr "Ferry" msgstr "Ferry"
#: sncfgtfs/models.py:43 #: sncfgtfs/models.py:86
msgid "Cable car" msgid "Cable car"
msgstr "Câble" msgstr "Câble"
#: sncfgtfs/models.py:44 #: sncfgtfs/models.py:87
msgid "Gondola" msgid "Gondola"
msgstr "Gondole" msgstr "Gondole"
#: sncfgtfs/models.py:45 #: sncfgtfs/models.py:88
msgid "Funicular" msgid "Funicular"
msgstr "Funiculaire" msgstr "Funiculaire"
#: sncfgtfs/models.py:49 #: sncfgtfs/models.py:92
msgid "Outbound" msgid "Outbound"
msgstr "Vers l'extérieur" msgstr "Vers l'extérieur"
#: sncfgtfs/models.py:50 #: sncfgtfs/models.py:93
msgid "Inbound" msgid "Inbound"
msgstr "Vers l'intérieur" msgstr "Vers l'intérieur"
#: sncfgtfs/models.py:54 #: sncfgtfs/models.py:97
msgid "Recommended" msgid "Recommended"
msgstr "Recommandé" msgstr "Recommandé"
#: sncfgtfs/models.py:55 #: sncfgtfs/models.py:98
msgid "Timed" msgid "Timed"
msgstr "Correspondance programmée" msgstr "Correspondance programmée"
#: sncfgtfs/models.py:56 #: sncfgtfs/models.py:99
msgid "Minimum time" msgid "Minimum time"
msgstr "Temps de correspondance minimum requis" msgstr "Temps de correspondance minimum requis"
#: sncfgtfs/models.py:61 sncfgtfs/models.py:67 #: sncfgtfs/models.py:104 sncfgtfs/models.py:110
msgid "Added" msgid "Added"
msgstr "Ajouté" msgstr "Ajouté"
#: sncfgtfs/models.py:62 #: sncfgtfs/models.py:105
msgid "Removed" msgid "Removed"
msgstr "Supprimé" msgstr "Supprimé"
#: sncfgtfs/models.py:66 sncfgtfs/models.py:76 #: sncfgtfs/models.py:109 sncfgtfs/models.py:119
msgid "Scheduled" msgid "Scheduled"
msgstr "Planifié" msgstr "Planifié"
#: sncfgtfs/models.py:68 sncfgtfs/models.py:79 #: sncfgtfs/models.py:111 sncfgtfs/models.py:122
msgid "Unscheduled" msgid "Unscheduled"
msgstr "Non planifié" msgstr "Non planifié"
#: sncfgtfs/models.py:69 #: sncfgtfs/models.py:112
msgid "Canceled" msgid "Canceled"
msgstr "Annulé" msgstr "Annulé"
#: sncfgtfs/models.py:70 #: sncfgtfs/models.py:113
msgid "Replacement" msgid "Replacement"
msgstr "Remplacé" msgstr "Remplacé"
#: sncfgtfs/models.py:71 #: sncfgtfs/models.py:114
msgid "Duplicated" msgid "Duplicated"
msgstr "Dupliqué" msgstr "Dupliqué"
#: sncfgtfs/models.py:72 #: sncfgtfs/models.py:115
msgid "Deleted" msgid "Deleted"
msgstr "Supprimé" msgstr "Supprimé"
#: sncfgtfs/models.py:77 #: sncfgtfs/models.py:120
msgid "Skipped" msgid "Skipped"
msgstr "Sauté" msgstr "Sauté"
#: sncfgtfs/models.py:78 #: sncfgtfs/models.py:121
msgid "No data" msgid "No data"
msgstr "Pas de données" 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" msgid "Agency ID"
msgstr "ID de l'agence" msgstr "ID de l'agence"
#: sncfgtfs/models.py:91 #: sncfgtfs/models.py:194
msgid "Agency name" msgid "Agency name"
msgstr "Nom de l'agence" msgstr "Nom de l'agence"
#: sncfgtfs/models.py:95 #: sncfgtfs/models.py:198
msgid "Agency URL" msgid "Agency URL"
msgstr "URL de l'agence" msgstr "URL de l'agence"
#: sncfgtfs/models.py:100 #: sncfgtfs/models.py:203
msgid "Agency timezone" msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence" msgstr "Fuseau horaire de l'agence"
#: sncfgtfs/models.py:105 #: sncfgtfs/models.py:208
msgid "Agency language" msgid "Agency language"
msgstr "Langue de l'agence" msgstr "Langue de l'agence"
#: sncfgtfs/models.py:111 #: sncfgtfs/models.py:214
msgid "Agency phone" msgid "Agency phone"
msgstr "Téléphone de l'agence" msgstr "Téléphone de l'agence"
#: sncfgtfs/models.py:116 #: sncfgtfs/models.py:219
msgid "Agency email" msgid "Agency email"
msgstr "Adresse email de l'agence" msgstr "Adresse email de l'agence"
#: sncfgtfs/models.py:124 sncfgtfs/models.py:242 #: sncfgtfs/models.py:233 sncfgtfs/models.py:356
msgid "Agency" msgid "Agency"
msgstr "Agence" msgstr "Agence"
#: sncfgtfs/models.py:125 #: sncfgtfs/models.py:234
msgid "Agencies" msgid "Agencies"
msgstr "Agences" msgstr "Agences"
#: sncfgtfs/models.py:133 sncfgtfs/models.py:459 #: sncfgtfs/models.py:243 sncfgtfs/models.py:593
msgid "Stop ID" msgid "Stop ID"
msgstr "ID de l'arrêt" msgstr "ID de l'arrêt"
#: sncfgtfs/models.py:138 #: sncfgtfs/models.py:248
msgid "Stop code" msgid "Stop code"
msgstr "Code de l'arrêt" msgstr "Code de l'arrêt"
#: sncfgtfs/models.py:144 #: sncfgtfs/models.py:254
msgid "Stop name" msgid "Stop name"
msgstr "Nom de l'arrêt" msgstr "Nom de l'arrêt"
#: sncfgtfs/models.py:149 #: sncfgtfs/models.py:259
msgid "Stop description" msgid "Stop description"
msgstr "Description de l'arrêt" msgstr "Description de l'arrêt"
#: sncfgtfs/models.py:154 #: sncfgtfs/models.py:264
msgid "Stop longitude" msgid "Stop longitude"
msgstr "Longitude de l'arrêt" msgstr "Longitude de l'arrêt"
#: sncfgtfs/models.py:158 #: sncfgtfs/models.py:268
msgid "Stop latitude" msgid "Stop latitude"
msgstr "Latitude de l'arrêt" msgstr "Latitude de l'arrêt"
#: sncfgtfs/models.py:163 #: sncfgtfs/models.py:273
msgid "Zone ID" msgid "Zone ID"
msgstr "ID de la zone" msgstr "ID de la zone"
#: sncfgtfs/models.py:167 #: sncfgtfs/models.py:278
msgid "Stop URL" msgid "Stop URL"
msgstr "URL de l'arrêt" msgstr "URL de l'arrêt"
#: sncfgtfs/models.py:172 #: sncfgtfs/models.py:283
msgid "Location type" msgid "Location type"
msgstr "Type de localisation" msgstr "Type de localisation"
#: sncfgtfs/models.py:181 #: sncfgtfs/models.py:292
msgid "Parent station" msgid "Parent station"
msgstr "Gare parente" msgstr "Gare parente"
#: sncfgtfs/models.py:189 #: sncfgtfs/models.py:300
msgid "Stop timezone" msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt" msgstr "Fuseau horaire de l'arrêt"
#: sncfgtfs/models.py:195 #: sncfgtfs/models.py:306
msgid "Level ID" msgid "Level ID"
msgstr "ID du niveau" msgstr "ID du niveau"
#: sncfgtfs/models.py:200 #: sncfgtfs/models.py:311
msgid "Wheelchair boarding" msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant" msgstr "Embarquement en fauteuil roulant"
#: sncfgtfs/models.py:208 #: sncfgtfs/models.py:319
msgid "Platform code" msgid "Platform code"
msgstr "Code du quai" msgstr "Code du quai"
#: sncfgtfs/models.py:214 sncfgtfs/models.py:286 sncfgtfs/models.py:560 #: sncfgtfs/models.py:338
msgid "Transport type"
msgstr "Type de transport"
#: sncfgtfs/models.py:227
msgid "Stop" msgid "Stop"
msgstr "Arrêt" msgstr "Arrêt"
#: sncfgtfs/models.py:228 #: sncfgtfs/models.py:339
msgid "Stops" msgid "Stops"
msgstr "Arrêts" msgstr "Arrêts"
#: sncfgtfs/models.py:236 sncfgtfs/models.py:438 sncfgtfs/models.py:577 #: sncfgtfs/models.py:350 sncfgtfs/models.py:572 sncfgtfs/models.py:713
#: sncfgtfs/models.py:609 #: sncfgtfs/models.py:746
msgid "ID" msgid "ID"
msgstr "Identifiant" msgstr "Identifiant"
#: sncfgtfs/models.py:248 #: sncfgtfs/models.py:365
msgid "Route short name" msgid "Route short name"
msgstr "Nom court de la ligne" msgstr "Nom court de la ligne"
#: sncfgtfs/models.py:253 #: sncfgtfs/models.py:370
msgid "Route long name" msgid "Route long name"
msgstr "Nom long de la ligne" msgstr "Nom long de la ligne"
#: sncfgtfs/models.py:258 #: sncfgtfs/models.py:376
msgid "Route description" msgid "Route description"
msgstr "Description de la ligne" msgstr "Description de la ligne"
#: sncfgtfs/models.py:263 #: sncfgtfs/models.py:381
msgid "Route type" msgid "Route type"
msgstr "Type de ligne" msgstr "Type de ligne"
#: sncfgtfs/models.py:268 #: sncfgtfs/models.py:386
msgid "Route URL" msgid "Route URL"
msgstr "URL de la ligne" msgstr "URL de la ligne"
#: sncfgtfs/models.py:274 #: sncfgtfs/models.py:392
msgid "Route color" msgid "Route color"
msgstr "Couleur de la ligne" msgstr "Couleur de la ligne"
#: sncfgtfs/models.py:280 #: sncfgtfs/models.py:398
msgid "Route text color" msgid "Route text color"
msgstr "Couleur du texte de la ligne" 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" msgid "Route"
msgstr "Ligne" msgstr "Ligne"
#: sncfgtfs/models.py:295 #: sncfgtfs/models.py:413
msgid "Routes" msgid "Routes"
msgstr "Lignes" msgstr "Lignes"
#: sncfgtfs/models.py:303 #: sncfgtfs/models.py:422
msgid "Trip ID" msgid "Trip ID"
msgstr "ID du trajet" msgstr "ID du trajet"
#: sncfgtfs/models.py:316 sncfgtfs/models.py:583 #: sncfgtfs/models.py:435 sncfgtfs/models.py:719
msgid "Service" msgid "Service"
msgstr "Service" msgstr "Service"
#: sncfgtfs/models.py:322 #: sncfgtfs/models.py:441
msgid "Trip headsign" msgid "Trip headsign"
msgstr "Destination du trajet" msgstr "Destination du trajet"
#: sncfgtfs/models.py:328 #: sncfgtfs/models.py:447
msgid "Trip short name" msgid "Trip short name"
msgstr "Nom court du trajet" msgstr "Nom court du trajet"
#: sncfgtfs/models.py:333 #: sncfgtfs/models.py:452
msgid "Direction" msgid "Direction"
msgstr "Direction" msgstr "Direction"
#: sncfgtfs/models.py:340 #: sncfgtfs/models.py:459
msgid "Block ID" msgid "Block ID"
msgstr "ID du bloc" msgstr "ID du bloc"
#: sncfgtfs/models.py:346 #: sncfgtfs/models.py:465
msgid "Shape ID" msgid "Shape ID"
msgstr "ID de la forme" msgstr "ID de la forme"
#: sncfgtfs/models.py:351 #: sncfgtfs/models.py:470
msgid "Wheelchair accessible" msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant" msgstr "Accessible en fauteuil roulant"
#: sncfgtfs/models.py:358 #: sncfgtfs/models.py:477
msgid "Bikes allowed" msgid "Bikes allowed"
msgstr "Vélos autorisés" msgstr "Vélos autorisés"
#: sncfgtfs/models.py:365 #: sncfgtfs/models.py:500 sncfgtfs/models.py:509 sncfgtfs/models.py:552
msgid "Last update" #: sncfgtfs/models.py:554
msgstr "Dernière mise à jour" 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" msgid "Trip"
msgstr "Trajet" msgstr "Trajet"
#: sncfgtfs/models.py:431 #: sncfgtfs/models.py:564
msgid "Trips" msgid "Trips"
msgstr "Trajets" msgstr "Trajets"
#: sncfgtfs/models.py:449 sncfgtfs/models.py:731 #: sncfgtfs/models.py:583 sncfgtfs/models.py:876
msgid "Arrival time" msgid "Arrival time"
msgstr "Heure d'arrivée" msgstr "Heure d'arrivée"
#: sncfgtfs/models.py:453 sncfgtfs/models.py:739 #: sncfgtfs/models.py:587 sncfgtfs/models.py:884
msgid "Departure time" msgid "Departure time"
msgstr "Heure de départ" msgstr "Heure de départ"
#: sncfgtfs/models.py:464 #: sncfgtfs/models.py:598
msgid "Stop sequence" msgid "Stop sequence"
msgstr "Séquence de l'arrêt" msgstr "Séquence de l'arrêt"
#: sncfgtfs/models.py:469 #: sncfgtfs/models.py:603
msgid "Stop headsign" msgid "Stop headsign"
msgstr "Destination de l'arrêt" msgstr "Destination de l'arrêt"
#: sncfgtfs/models.py:474 #: sncfgtfs/models.py:608
msgid "Pickup type" msgid "Pickup type"
msgstr "Type de prise en charge" msgstr "Type de prise en charge"
#: sncfgtfs/models.py:481 #: sncfgtfs/models.py:615
msgid "Drop off type" msgid "Drop off type"
msgstr "Type de dépose" msgstr "Type de dépose"
#: sncfgtfs/models.py:488 #: sncfgtfs/models.py:622
msgid "Timepoint" msgid "Timepoint"
msgstr "Ponctualité" msgstr "Ponctualité"
#: sncfgtfs/models.py:511 sncfgtfs/models.py:721 #: sncfgtfs/models.py:645 sncfgtfs/models.py:866
msgid "Stop time" msgid "Stop time"
msgstr "Heure d'arrêt" msgstr "Heure d'arrêt"
#: sncfgtfs/models.py:512 #: sncfgtfs/models.py:646
msgid "Stop times" msgid "Stop times"
msgstr "Heures d'arrêt" msgstr "Heures d'arrêt"
#: sncfgtfs/models.py:519 #: sncfgtfs/models.py:654
msgid "Service ID" msgid "Service ID"
msgstr "ID du service" msgstr "ID du service"
#: sncfgtfs/models.py:523 #: sncfgtfs/models.py:658
msgid "Monday" msgid "Monday"
msgstr "Lundi" msgstr "Lundi"
#: sncfgtfs/models.py:527 #: sncfgtfs/models.py:662
msgid "Tuesday" msgid "Tuesday"
msgstr "Mardi" msgstr "Mardi"
#: sncfgtfs/models.py:531 #: sncfgtfs/models.py:666
msgid "Wednesday" msgid "Wednesday"
msgstr "Mercredi" msgstr "Mercredi"
#: sncfgtfs/models.py:535 #: sncfgtfs/models.py:670
msgid "Thursday" msgid "Thursday"
msgstr "Jeudi" msgstr "Jeudi"
#: sncfgtfs/models.py:539 #: sncfgtfs/models.py:674
msgid "Friday" msgid "Friday"
msgstr "Vendredi" msgstr "Vendredi"
#: sncfgtfs/models.py:543 #: sncfgtfs/models.py:678
msgid "Saturday" msgid "Saturday"
msgstr "Samedi" msgstr "Samedi"
#: sncfgtfs/models.py:547 #: sncfgtfs/models.py:682
msgid "Sunday" msgid "Sunday"
msgstr "Dimanche" msgstr "Dimanche"
#: sncfgtfs/models.py:551 sncfgtfs/models.py:687 #: sncfgtfs/models.py:686 sncfgtfs/models.py:831
msgid "Start date" msgid "Start date"
msgstr "Date de début" msgstr "Date de début"
#: sncfgtfs/models.py:555 #: sncfgtfs/models.py:690
msgid "End date" msgid "End date"
msgstr "Date de fin" msgstr "Date de fin"
#: sncfgtfs/models.py:568 #: sncfgtfs/models.py:703
msgid "Calendar" msgid "Calendar"
msgstr "Calendrier" msgstr "Calendrier"
#: sncfgtfs/models.py:569 #: sncfgtfs/models.py:704
msgid "Calendars" msgid "Calendars"
msgstr "Calendriers" msgstr "Calendriers"
#: sncfgtfs/models.py:588 #: sncfgtfs/models.py:724
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
#: sncfgtfs/models.py:592 #: sncfgtfs/models.py:728
msgid "Exception type" msgid "Exception type"
msgstr "Type d'exception" msgstr "Type d'exception"
#: sncfgtfs/models.py:600 #: sncfgtfs/models.py:736
msgid "Calendar date" msgid "Calendar date"
msgstr "Date du calendrier" msgstr "Date du calendrier"
#: sncfgtfs/models.py:601 #: sncfgtfs/models.py:737
msgid "Calendar dates" msgid "Calendar dates"
msgstr "Dates du calendrier" msgstr "Dates du calendrier"
#: sncfgtfs/models.py:615 #: sncfgtfs/models.py:752
msgid "From stop" msgid "From stop"
msgstr "Depuis l'arrêt" msgstr "Depuis l'arrêt"
#: sncfgtfs/models.py:622 #: sncfgtfs/models.py:759
msgid "To stop" msgid "To stop"
msgstr "Jusqu'à l'arrêt" msgstr "Jusqu'à l'arrêt"
#: sncfgtfs/models.py:627 #: sncfgtfs/models.py:764
msgid "Transfer type" msgid "Transfer type"
msgstr "Type de correspondance" msgstr "Type de correspondance"
#: sncfgtfs/models.py:633 #: sncfgtfs/models.py:770
msgid "Minimum transfer time" msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum" msgstr "Temps de correspondance minimum"
#: sncfgtfs/models.py:638 #: sncfgtfs/models.py:775
msgid "Transfer" msgid "Transfer"
msgstr "Correspondance" msgstr "Correspondance"
#: sncfgtfs/models.py:639 #: sncfgtfs/models.py:776
msgid "Transfers" msgid "Transfers"
msgstr "Correspondances" msgstr "Correspondances"
#: sncfgtfs/models.py:646 #: sncfgtfs/models.py:783
msgid "Feed publisher name" msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux" msgstr "Nom de l'éditeur du flux"
#: sncfgtfs/models.py:650 #: sncfgtfs/models.py:787
msgid "Feed publisher URL" msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux" msgstr "URL de l'éditeur du flux"
#: sncfgtfs/models.py:655 #: sncfgtfs/models.py:792
msgid "Feed language" msgid "Feed language"
msgstr "Langue du flux" msgstr "Langue du flux"
#: sncfgtfs/models.py:659 #: sncfgtfs/models.py:796
msgid "Feed start date" msgid "Feed start date"
msgstr "Date de début du flux" msgstr "Date de début du flux"
#: sncfgtfs/models.py:663 #: sncfgtfs/models.py:800
msgid "Feed end date" msgid "Feed end date"
msgstr "Date de fin du flux" msgstr "Date de fin du flux"
#: sncfgtfs/models.py:668 #: sncfgtfs/models.py:805
msgid "Feed version" msgid "Feed version"
msgstr "Version du flux" msgstr "Version du flux"
#: sncfgtfs/models.py:672 #: sncfgtfs/models.py:815
msgid "Feed info" msgid "Feed info"
msgstr "Information du flux" msgstr "Information du flux"
#: sncfgtfs/models.py:673 #: sncfgtfs/models.py:816
msgid "Feed infos" msgid "Feed infos"
msgstr "Informations du flux" msgstr "Informations du flux"
#: sncfgtfs/models.py:691 #: sncfgtfs/models.py:835
msgid "Start time" msgid "Start time"
msgstr "Heure de début" msgstr "Heure de début"
#: sncfgtfs/models.py:695 sncfgtfs/models.py:743 #: sncfgtfs/models.py:839 sncfgtfs/models.py:888
msgid "Schedule relationship" msgid "Schedule relationship"
msgstr "Relation de la planification" msgstr "Relation de la planification"
#: sncfgtfs/models.py:704 sncfgtfs/models.py:714 #: sncfgtfs/models.py:848 sncfgtfs/models.py:859
msgid "Trip update" msgid "Trip update"
msgstr "Mise à jour du trajet" msgstr "Mise à jour du trajet"
#: sncfgtfs/models.py:705 #: sncfgtfs/models.py:849
msgid "Trip updates" msgid "Trip updates"
msgstr "Mises à jour des trajets" msgstr "Mises à jour des trajets"
#: sncfgtfs/models.py:727 #: sncfgtfs/models.py:872
msgid "Arrival delay" msgid "Arrival delay"
msgstr "Retard à l'arrivée" msgstr "Retard à l'arrivée"
#: sncfgtfs/models.py:735 #: sncfgtfs/models.py:880
msgid "Departure delay" msgid "Departure delay"
msgstr "Retard au départ" msgstr "Retard au départ"
#: sncfgtfs/models.py:752 #: sncfgtfs/models.py:897
msgid "Stop time update" msgid "Stop time update"
msgstr "Mise à jour du temps d'arrêt" msgstr "Mise à jour du temps d'arrêt"
#: sncfgtfs/models.py:753 #: sncfgtfs/models.py:898
msgid "Stop time updates" msgid "Stop time updates"
msgstr "Mises à jour des temps d'arrêt" 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"

View File

@ -2,27 +2,18 @@ import csv
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from zipfile import ZipFile from zipfile import ZipFile
from zoneinfo import ZoneInfo
import requests import requests
from django.core.management import BaseCommand 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): class Command(BaseCommand):
help = "Update the SNCF GTFS database." 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): def add_arguments(self, parser):
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode") 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.") 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.") 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.") parser.add_argument('--force', '-f', action='store_true', help="Force the update of the database.")
def handle(self, *args, **options): def handle(self, debug: bool = False, bulk_size: int = 100, dry_run: bool = False, force: bool = False,
bulk_size = options['bulk_size'] verbosity: int = 1, *args, **options):
dry_run = options['dry_run']
force = options['force']
if dry_run: if dry_run:
self.stdout.write(self.style.WARNING("Dry run mode activated.")) 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...") self.stdout.write("Updating database...")
for transport_type, feed_url in self.GTFS_FEEDS.items(): for gtfs_feed in GTFSFeed.objects.all():
self.stdout.write(f"Downloading {transport_type} GTFS feed...") gtfs_code = gtfs_feed.code
with ZipFile(BytesIO(requests.get(feed_url).content)) as zipfile:
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): def read_file(filename):
lines = zipfile.read(filename).decode().replace('\ufeff', '').splitlines() lines = zipfile.read(filename).decode().replace('\ufeff', '').splitlines()
return [line.strip() for line in lines] return [line.strip() for line in lines]
@ -67,23 +58,25 @@ class Command(BaseCommand):
agencies = [] agencies = []
for agency_dict in csv.DictReader(read_file("agency.txt")): for agency_dict in csv.DictReader(read_file("agency.txt")):
agency_dict: dict agency_dict: dict
if transport_type == "ES" \ # if gtfs_code == "FR-EUROSTAR" \
and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER': # and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER':
continue # continue
agency = Agency( agency = Agency(
id=agency_dict['agency_id'], id=f"{gtfs_code}-{agency_dict['agency_id']}",
name=agency_dict['agency_name'], name=agency_dict['agency_name'],
url=agency_dict['agency_url'], url=agency_dict['agency_url'],
timezone=agency_dict['agency_timezone'], timezone=agency_dict['agency_timezone'],
lang=agency_dict.get('agency_lang', "fr"), lang=agency_dict.get('agency_lang', "fr"),
phone=agency_dict.get('agency_phone', ""), phone=agency_dict.get('agency_phone', ""),
email=agency_dict.get('agency_email', ""), email=agency_dict.get('agency_email', ""),
gtfs_feed=gtfs_feed,
) )
agencies.append(agency) agencies.append(agency)
if agencies and not dry_run: if agencies and not dry_run:
Agency.objects.bulk_create(agencies, Agency.objects.bulk_create(agencies,
update_conflicts=True, update_conflicts=True,
update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email'], update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email',
'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
agencies.clear() agencies.clear()
@ -91,8 +84,10 @@ class Command(BaseCommand):
for stop_dict in csv.DictReader(read_file("stops.txt")): for stop_dict in csv.DictReader(read_file("stops.txt")):
stop_dict: dict stop_dict: dict
stop_id = stop_dict['stop_id'] stop_id = stop_dict['stop_id']
if transport_type in ["ES", "TI", "RENFE"]: stop_id = f"{gtfs_code}-{stop_id}"
stop_id = f"{transport_type}-{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( stop = Stop(
id=stop_id, id=stop_id,
@ -102,13 +97,13 @@ class Command(BaseCommand):
lon=stop_dict['stop_lon'], lon=stop_dict['stop_lon'],
zone_id=stop_dict.get('zone_id', ""), zone_id=stop_dict.get('zone_id', ""),
url=stop_dict.get('stop_url', ""), url=stop_dict.get('stop_url', ""),
location_type=stop_dict.get('location_type', 1) or 1, location_type=stop_dict.get('location_type', 0) or 0,
parent_station_id=stop_dict.get('parent_station', None) or None, parent_station_id=parent_station_id,
timezone=stop_dict.get('stop_timezone', ""), timezone=stop_dict.get('stop_timezone', ""),
wheelchair_boarding=stop_dict.get('wheelchair_boarding', 0), wheelchair_boarding=stop_dict.get('wheelchair_boarding', 0),
level_id=stop_dict.get('level_id', ""), level_id=stop_dict.get('level_id', ""),
platform_code=stop_dict.get('platform_code', ""), platform_code=stop_dict.get('platform_code', ""),
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
stops.append(stop) stops.append(stop)
@ -119,7 +114,7 @@ class Command(BaseCommand):
update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url', update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url',
'location_type', 'parent_station_id', 'timezone', 'location_type', 'parent_station_id', 'timezone',
'wheelchair_boarding', 'level_id', 'platform_code', 'wheelchair_boarding', 'level_id', 'platform_code',
'transport_type'], 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
stops.clear() stops.clear()
@ -127,11 +122,10 @@ class Command(BaseCommand):
for route_dict in csv.DictReader(read_file("routes.txt")): for route_dict in csv.DictReader(read_file("routes.txt")):
route_dict: dict route_dict: dict
route_id = route_dict['route_id'] route_id = route_dict['route_id']
if transport_type == "TI": route_id = f"{gtfs_code}-{route_id}"
route_id = f"{transport_type}-{route_id}"
route = Route( route = Route(
id=route_id, 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'], short_name=route_dict['route_short_name'],
long_name=route_dict['route_long_name'], long_name=route_dict['route_long_name'],
desc=route_dict.get('route_desc', ""), desc=route_dict.get('route_desc', ""),
@ -139,7 +133,7 @@ class Command(BaseCommand):
url=route_dict.get('route_url', ""), url=route_dict.get('route_url', ""),
color=route_dict.get('route_color', ""), color=route_dict.get('route_color', ""),
text_color=route_dict.get('route_text_color', ""), text_color=route_dict.get('route_text_color', ""),
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
routes.append(route) routes.append(route)
@ -148,7 +142,7 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc', update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color', 'type', 'url', 'color', 'text_color',
'transport_type'], 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
routes.clear() routes.clear()
if routes and not dry_run: if routes and not dry_run:
@ -156,17 +150,17 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc', update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color', 'type', 'url', 'color', 'text_color',
'transport_type'], 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
routes.clear() routes.clear()
Calendar.objects.filter(transport_type=transport_type).delete() Calendar.objects.filter(gtfs_feed=gtfs_feed).delete()
calendars = {} calendars = {}
if "calendar.txt" in zipfile.namelist(): if "calendar.txt" in zipfile.namelist():
for calendar_dict in csv.DictReader(read_file("calendar.txt")): for calendar_dict in csv.DictReader(read_file("calendar.txt")):
calendar_dict: dict calendar_dict: dict
calendar = Calendar( calendar = Calendar(
id=f"{transport_type}-{calendar_dict['service_id']}", id=f"{gtfs_code}-{calendar_dict['service_id']}",
monday=calendar_dict['monday'], monday=calendar_dict['monday'],
tuesday=calendar_dict['tuesday'], tuesday=calendar_dict['tuesday'],
wednesday=calendar_dict['wednesday'], wednesday=calendar_dict['wednesday'],
@ -176,7 +170,7 @@ class Command(BaseCommand):
sunday=calendar_dict['sunday'], sunday=calendar_dict['sunday'],
start_date=calendar_dict['start_date'], start_date=calendar_dict['start_date'],
end_date=calendar_dict['end_date'], end_date=calendar_dict['end_date'],
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
calendars[calendar.id] = calendar calendars[calendar.id] = calendar
@ -185,14 +179,14 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday', update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date', 'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'transport_type'], 'end_date', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
calendars.clear() calendars.clear()
if calendars and not dry_run: if calendars and not dry_run:
Calendar.objects.bulk_create(calendars.values(), update_conflicts=True, Calendar.objects.bulk_create(calendars.values(), update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday', update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date', 'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'transport_type'], 'end_date', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
calendars.clear() calendars.clear()
@ -200,8 +194,8 @@ class Command(BaseCommand):
for calendar_date_dict in csv.DictReader(read_file("calendar_dates.txt")): for calendar_date_dict in csv.DictReader(read_file("calendar_dates.txt")):
calendar_date_dict: dict calendar_date_dict: dict
calendar_date = CalendarDate( calendar_date = CalendarDate(
id=f"{transport_type}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}", id=f"{gtfs_code}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}",
service_id=f"{transport_type}-{calendar_date_dict['service_id']}", service_id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
date=calendar_date_dict['date'], date=calendar_date_dict['date'],
exception_type=calendar_date_dict['exception_type'], exception_type=calendar_date_dict['exception_type'],
) )
@ -209,7 +203,7 @@ class Command(BaseCommand):
if calendar_date.service_id not in calendars: if calendar_date.service_id not in calendars:
calendar = Calendar( calendar = Calendar(
id=f"{transport_type}-{calendar_date_dict['service_id']}", id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
monday=False, monday=False,
tuesday=False, tuesday=False,
wednesday=False, wednesday=False,
@ -219,11 +213,11 @@ class Command(BaseCommand):
sunday=False, sunday=False,
start_date=calendar_date_dict['date'], start_date=calendar_date_dict['date'],
end_date=calendar_date_dict['date'], end_date=calendar_date_dict['date'],
transport_type=transport_type, gtfs_feed=gtfs_feed,
) )
calendars[calendar.id] = calendar calendars[calendar.id] = calendar
else: 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: if calendar.start_date > calendar_date.date:
calendar.start_date = calendar_date.date calendar.start_date = calendar_date.date
if calendar.end_date < calendar_date.date: if calendar.end_date < calendar_date.date:
@ -233,7 +227,7 @@ class Command(BaseCommand):
Calendar.objects.bulk_create(calendars.values(), Calendar.objects.bulk_create(calendars.values(),
batch_size=bulk_size, batch_size=bulk_size,
update_conflicts=True, update_conflicts=True,
update_fields=['start_date', 'end_date'], update_fields=['start_date', 'end_date', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
CalendarDate.objects.bulk_create(calendar_dates, CalendarDate.objects.bulk_create(calendar_dates,
batch_size=bulk_size, batch_size=bulk_size,
@ -248,22 +242,12 @@ class Command(BaseCommand):
trip_dict: dict trip_dict: dict
trip_id = trip_dict['trip_id'] trip_id = trip_dict['trip_id']
route_id = trip_dict['route_id'] route_id = trip_dict['route_id']
if transport_type in ["TGV", "IC", "TER"]: trip_id = f"{gtfs_code}-{trip_id}"
trip_id, last_update = trip_id.split(':', 1) route_id = f"{gtfs_code}-{route_id}"
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 = Trip( trip = Trip(
id=trip_id, id=trip_id,
route_id=route_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', ""), headsign=trip_dict.get('trip_headsign', ""),
short_name=trip_dict.get('trip_short_name', ""), short_name=trip_dict.get('trip_short_name', ""),
direction_id=trip_dict.get('direction_id', None) or None, direction_id=trip_dict.get('direction_id', None) or None,
@ -271,7 +255,7 @@ class Command(BaseCommand):
shape_id=trip_dict.get('shape_id', ""), shape_id=trip_dict.get('shape_id', ""),
wheelchair_accessible=trip_dict.get('wheelchair_accessible', None), wheelchair_accessible=trip_dict.get('wheelchair_accessible', None),
bikes_allowed=trip_dict.get('bikes_allowed', None), bikes_allowed=trip_dict.get('bikes_allowed', None),
last_update=last_update, gtfs_feed=gtfs_feed,
) )
trips.append(trip) trips.append(trip)
@ -280,7 +264,7 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['route_id', 'service_id', 'headsign', 'short_name', update_fields=['route_id', 'service_id', 'headsign', 'short_name',
'direction_id', 'block_id', 'shape_id', 'direction_id', 'block_id', 'shape_id',
'wheelchair_accessible', 'bikes_allowed'], 'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
trips.clear() trips.clear()
if trips and not dry_run: if trips and not dry_run:
@ -288,7 +272,7 @@ class Command(BaseCommand):
update_conflicts=True, update_conflicts=True,
update_fields=['route_id', 'service_id', 'headsign', 'short_name', update_fields=['route_id', 'service_id', 'headsign', 'short_name',
'direction_id', 'block_id', 'shape_id', 'direction_id', 'block_id', 'shape_id',
'wheelchair_accessible', 'bikes_allowed'], 'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
unique_fields=['id']) unique_fields=['id'])
trips.clear() trips.clear()
@ -297,14 +281,10 @@ class Command(BaseCommand):
stop_time_dict: dict stop_time_dict: dict
stop_id = stop_time_dict['stop_id'] stop_id = stop_time_dict['stop_id']
if transport_type in ["ES", "TI", "RENFE"]: stop_id = f"{gtfs_code}-{stop_id}"
stop_id = f"{transport_type}-{stop_id}"
trip_id = stop_time_dict['trip_id'] trip_id = stop_time_dict['trip_id']
if transport_type in ["TGV", "IC", "TER"]: trip_id = f"{gtfs_code}-{trip_id}"
trip_id = trip_id.split(':', 1)[0]
elif transport_type in ["ES", "TI", "RENFE"]:
trip_id = f"{transport_type}-{trip_id}"
arr_time = stop_time_dict['arrival_time'] arr_time = stop_time_dict['arrival_time']
arr_h, arr_m, arr_s = map(int, arr_time.split(':')) 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) pickup_type = stop_time_dict.get('pickup_type', 0)
drop_off_type = stop_time_dict.get('drop_off_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": if stop_time_dict['stop_sequence'] == "1":
drop_off_type = 1 # First stop
drop_off_type = PickupType.NONE
elif arr_time == dep_time: elif arr_time == dep_time:
pickup_type = 1 # Last stop
elif transport_type == "TI": pickup_type = PickupType.NONE
if stop_time_dict['stop_sequence'] == "0":
drop_off_type = 1
elif arr_time == dep_time:
pickup_type = 1
st = StopTime( 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, trip_id=trip_id,
arrival_time=timedelta(seconds=arr_time), arrival_time=timedelta(seconds=arr_time),
departure_time=timedelta(seconds=dep_time), departure_time=timedelta(seconds=dep_time),
@ -363,14 +340,13 @@ class Command(BaseCommand):
transfer_dict: dict transfer_dict: dict
from_stop_id = transfer_dict['from_stop_id'] from_stop_id = transfer_dict['from_stop_id']
to_stop_id = transfer_dict['to_stop_id'] to_stop_id = transfer_dict['to_stop_id']
if transport_type in ["ES", "RENFE", "OBB"]: from_stop_id = f"{gtfs_code}-{from_stop_id}"
from_stop_id = f"{transport_type}-{from_stop_id}" to_stop_id = f"{gtfs_code}-{to_stop_id}"
to_stop_id = f"{transport_type}-{to_stop_id}"
transfer = Transfer( transfer = Transfer(
id=f"{from_stop_id}-{to_stop_id}", id=f"{transfer_dict['from_stop_id']}-{transfer_dict['to_stop_id']}",
from_stop_id=transfer_dict['from_stop_id'], from_stop_id=from_stop_id,
to_stop_id=transfer_dict['to_stop_id'], to_stop_id=to_stop_id,
transfer_type=transfer_dict['transfer_type'], transfer_type=transfer_dict['transfer_type'],
min_transfer_time=transfer_dict['min_transfer_time'], min_transfer_time=transfer_dict['min_transfer_time'],
) )
@ -395,6 +371,7 @@ class Command(BaseCommand):
feed_info_dict: dict feed_info_dict: dict
FeedInfo.objects.update_or_create( FeedInfo.objects.update_or_create(
publisher_name=feed_info_dict['feed_publisher_name'], publisher_name=feed_info_dict['feed_publisher_name'],
gtfs_feed=gtfs_feed,
defaults=dict( defaults=dict(
publisher_url=feed_info_dict['feed_publisher_url'], publisher_url=feed_info_dict['feed_publisher_url'],
lang=feed_info_dict['feed_lang'], lang=feed_info_dict['feed_lang'],
@ -403,3 +380,12 @@ class Command(BaseCommand):
version=feed_info_dict.get('feed_version', 1), 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()

View File

@ -5,8 +5,8 @@ import requests
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.db.models import Q from django.db.models import Q
from sncfgtfs.gtfs_realtime_pb2 import FeedMessage from sncfgtfs.gtfs_realtime_pb2 import FeedMessage, TripUpdate as GTFSTripUpdate
from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, LocationType, PickupType, \ from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, GTFSFeed, LocationType, PickupType, \
Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \ Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \
Trip, TripUpdate, TripScheduleRelationship Trip, TripUpdate, TripScheduleRelationship
@ -14,34 +14,33 @@ from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, Locat
class Command(BaseCommand): class Command(BaseCommand):
help = "Update the SNCF GTFS Realtime database." 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): def add_arguments(self, parser):
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode") parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
def handle(self, debug=False, *args, **options): def handle(self, debug: bool = False, verbosity: int = 1, *args, **options):
for feed_type, feed_url in self.GTFS_RT_FEEDS.items(): for gtfs_feed in GTFSFeed.objects.all():
self.stdout.write(f"Updating {feed_type} feed...") 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 = 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 = [] stop_times_updates = []
if debug: 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)) f.write(str(feed_message))
for entity in feed_message.entity: for entity in feed_message.entity:
if entity.HasField("trip_update"): if entity.HasField("trip_update"):
trip_update = entity.trip_update trip_update = entity.trip_update
trip_id = trip_update.trip.trip_id trip_id = trip_update.trip.trip_id
if feed_type in ["TGV", "IC", "TER"]: trip_id = f"{gtfs_code}-{trip_id}"
trip_id = trip_id.split(":", 1)[0]
start_date = date(year=int(trip_update.trip.start_date[:4]), start_date = date(year=int(trip_update.trip.start_date[:4]),
month=int(trip_update.trip.start_date[4:6]), month=int(trip_update.trip.start_date[4:6]),
@ -50,7 +49,7 @@ class Command(BaseCommand):
if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED: if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED:
# C'est un trajet nouveau. On crée le trajet associé. # 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(): if not Trip.objects.filter(id=trip_id).exists():
self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.") 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): for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id stop_id = stop_time_update.stop_id
if stop_id.startswith('StopArea:'): stop_id = f"{gtfs_code}-{stop_id}"
# On est dans le cadre d'une gare. On cherche le quai associé. if StopTime.objects.filter(trip_id=trip_id, stop=stop_id).exists():
if StopTime.objects.filter(trip_id=trip_id, stop__parent_station_id=stop_id).exists(): st = StopTime.objects.filter(trip_id=trip_id, stop=stop_id)
# U if st.count() > 1:
stop = StopTime.objects.get(trip_id=trip_id, stop__parent_station_id=stop_id).stop st = st.get(stop_sequence=stop_sequence)
else: else:
stops = [s for s in Stop.objects.filter(parent_station_id=stop_id).all() st = st.first()
for s2 in StopTime.objects.filter(trip_id=trip_id).all() else:
if s.stop_type in s2.stop.stop_type # Stop is added
or s2.stop.stop_type in s.stop_type] st = StopTime.objects.create(
stop = stops[0] if stops else Stop.objects.get(id=stop_id) id=f"{trip_id}-{stop_time_update.stop_id}",
st, _created = StopTime.objects.update_or_create(
id=f"{trip_id}-{stop.id}",
trip_id=trip_id, trip_id=trip_id,
stop_id=stop.id, stop_id=stop_id,
defaults={ defaults={
"stop_sequence": stop_sequence, "stop_sequence": stop_sequence,
"arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time, "arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time,
@ -96,20 +92,13 @@ class Command(BaseCommand):
else PickupType.NONE), 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), if stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED:
trip_id=trip_id)
if st.pickup_type != PickupType.NONE or st.drop_off_type != PickupType.NONE: if st.pickup_type != PickupType.NONE or st.drop_off_type != PickupType.NONE:
st.pickup_type = PickupType.NONE st.pickup_type = PickupType.NONE
st.drop_off_type = PickupType.NONE st.drop_off_type = PickupType.NONE
st.save() 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: if st.stop_sequence != stop_sequence:
st.stop_sequence = stop_sequence st.stop_sequence = stop_sequence
st.save() st.save()
@ -136,73 +125,22 @@ class Command(BaseCommand):
'departure_delay', 'departure_time'], 'departure_delay', 'departure_time'],
unique_fields=['trip_update', 'stop_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] headsign = trip_id[5:-1]
trip_qs = Trip.objects.all() gtfs_code = gtfs_feed.code
trip_ids = trip_qs.values_list('id', flat=True)
first_stop_queryset = StopTime.objects.filter( route, _created = Route.objects.get_or_create(
stop__parent_station_id=trip_update.stop_time_update[0].stop_id, id=f"{gtfs_code}-ADDED-{headsign}",
).values('trip_id') gtfs_feed=gtfs_feed,
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, type=RouteType.RAIL,
short_name=trip_name, short_name="ADDED",
long_name=trip_name, long_name="ADDED ROUTE",
) )
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()
Calendar.objects.update_or_create( Calendar.objects.update_or_create(
id=f"{feed_type}-new-{headsign}", id=f"{gtfs_code}-ADDED-{headsign}",
defaults={ defaults={
"transport_type": feed_type, "gtfs_feed": gtfs_feed,
"monday": False, "monday": False,
"tuesday": False, "tuesday": False,
"wednesday": False, "wednesday": False,
@ -215,9 +153,9 @@ class Command(BaseCommand):
} }
) )
CalendarDate.objects.update_or_create( 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={ defaults={
"service_id": f"{feed_type}-new-{headsign}", "service_id": f"{gtfs_code}-ADDED-{headsign}",
"date": trip_update.trip.start_date, "date": trip_update.trip.start_date,
"exception_type": ExceptionType.ADDED, "exception_type": ExceptionType.ADDED,
} }
@ -225,32 +163,17 @@ class Command(BaseCommand):
Trip.objects.update_or_create( Trip.objects.update_or_create(
id=trip_id, id=trip_id,
defaults={ defaults={
"route_id": route_id, "route_id": route.id,
"service_id": f"{feed_type}-new-{headsign}", "service_id": f"{gtfs_code}-ADDED-{headsign}",
"headsign": headsign, "headsign": headsign,
"direction_id": trip_update.trip.direction_id, "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): for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id stop_id = stop_time_update.stop_id
stop = Stop.objects.get(id=stop_id) stop_id = f"{gtfs_code}-{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
arr_time = datetime.fromtimestamp(stop_time_update.arrival.time, arr_time = datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt 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 and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE
StopTime.objects.update_or_create( StopTime.objects.update_or_create(
id=f"{trip_id}-{stop_id}", id=f"{trip_id}-{stop_time_update.stop_id}",
trip_id=trip_id, trip_id=trip_id,
defaults={ defaults={
"stop_id": stop_id, "stop_id": stop_id,

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,58 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class TransportType(models.TextChoices): class Country(models.TextChoices):
TGV = "TGV", _("TGV") """
TER = "TER", _("TER") Country list by ISO 3166-1 alpha-2 code.
INTERCITES = "IC", _("Intercités") Only countries that are member of the Council of Europe
TRANSILIEN = "TN", _("Transilien") are listed for now.
EUROSTAR = "ES", _("Eurostar") """
TRENITALIA = "TI", _("Trenitalia") ALBANIA = "AL", _("Albania")
RENFE = "RENFE", _("Renfe") ANDORRA = "AD", _("Andorra")
OBB = "OBB", _("ÖBB") 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): class LocationType(models.IntegerChoices):
@ -79,6 +122,66 @@ class StopScheduleRelationship(models.IntegerChoices):
UNSCHEDULED = 3, _("Unscheduled") 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): class Agency(models.Model):
id = models.CharField( id = models.CharField(
max_length=255, max_length=255,
@ -117,6 +220,12 @@ class Agency(models.Model):
blank=True, blank=True,
) )
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -124,6 +233,7 @@ class Agency(models.Model):
verbose_name = _("Agency") verbose_name = _("Agency")
verbose_name_plural = _("Agencies") verbose_name_plural = _("Agencies")
ordering = ("name",) ordering = ("name",)
indexes = (models.Index(fields=['name']), models.Index(fields=['gtfs_feed']),)
class Stop(models.Model): class Stop(models.Model):
@ -161,6 +271,7 @@ class Stop(models.Model):
zone_id = models.CharField( zone_id = models.CharField(
max_length=255, max_length=255,
verbose_name=_("Zone ID"), verbose_name=_("Zone ID"),
blank=True,
) )
url = models.URLField( url = models.URLField(
@ -209,10 +320,10 @@ class Stop(models.Model):
blank=True, blank=True,
) )
transport_type = models.CharField( gtfs_feed = models.ForeignKey(
max_length=255, GTFSFeed,
verbose_name=_("Transport type"), on_delete=models.CASCADE,
choices=TransportType, verbose_name=_("GTFS feed"),
) )
@property @property
@ -227,6 +338,9 @@ class Stop(models.Model):
verbose_name = _("Stop") verbose_name = _("Stop")
verbose_name_plural = _("Stops") verbose_name_plural = _("Stops")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['name']),
models.Index(fields=['code']),
models.Index(fields=['gtfs_feed']),)
class Route(models.Model): class Route(models.Model):
@ -241,6 +355,9 @@ class Route(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_("Agency"), verbose_name=_("Agency"),
related_name="routes", related_name="routes",
null=True,
blank=True,
default=None,
) )
short_name = models.CharField( short_name = models.CharField(
@ -251,6 +368,7 @@ class Route(models.Model):
long_name = models.CharField( long_name = models.CharField(
max_length=255, max_length=255,
verbose_name=_("Route long name"), verbose_name=_("Route long name"),
blank=True,
) )
desc = models.CharField( desc = models.CharField(
@ -281,19 +399,20 @@ class Route(models.Model):
blank=True, blank=True,
) )
transport_type = models.CharField( gtfs_feed = models.ForeignKey(
max_length=255, GTFSFeed,
verbose_name=_("Transport type"), on_delete=models.CASCADE,
choices=TransportType, verbose_name=_("GTFS feed"),
) )
def __str__(self): def __str__(self):
return f"{self.long_name}" return self.long_name or self.short_name
class Meta: class Meta:
verbose_name = _("Route") verbose_name = _("Route")
verbose_name_plural = _("Routes") verbose_name_plural = _("Routes")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['gtfs_feed']),)
class Trip(models.Model): class Trip(models.Model):
@ -361,21 +480,24 @@ class Trip(models.Model):
null=True, null=True,
) )
last_update = models.DateTimeField( gtfs_feed = models.ForeignKey(
verbose_name=_("Last update"), GTFSFeed,
null=True, on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
) )
@property @property
def origin(self): def origin(self) -> Stop | None:
return self.stop_times.order_by('stop_sequence').first().stop return self.stop_times.order_by('stop_sequence').first().stop if self.stop_times.exists() else None
@property @property
def destination(self): def destination(self) -> Stop | None:
return self.stop_times.order_by('-stop_sequence').first().stop return self.stop_times.order_by('-stop_sequence').first().stop if self.stop_times.exists() else None
@property @property
def departure_time(self): def departure_time(self):
if not self.stop_times.exists():
return _("Unknown")
dep_time = self.stop_times.order_by('stop_sequence').first().departure_time dep_time = self.stop_times.order_by('stop_sequence').first().departure_time
hours = int(dep_time.total_seconds() // 3600) hours = int(dep_time.total_seconds() // 3600)
minutes = int((dep_time.total_seconds() % 3600) // 60) minutes = int((dep_time.total_seconds() % 3600) // 60)
@ -383,6 +505,8 @@ class Trip(models.Model):
@property @property
def arrival_time(self): def arrival_time(self):
if not self.stop_times.exists():
return _("Unknown")
arr_time = self.stop_times.order_by('-stop_sequence').first().arrival_time arr_time = self.stop_times.order_by('-stop_sequence').first().arrival_time
hours = int(arr_time.total_seconds() // 3600) hours = int(arr_time.total_seconds() // 3600)
minutes = int((arr_time.total_seconds() % 3600) // 60) minutes = int((arr_time.total_seconds() % 3600) // 60)
@ -390,14 +514,14 @@ class Trip(models.Model):
@property @property
def train_type(self): def train_type(self):
if self.route.transport_type == TransportType.TRANSILIEN: if self.gtfs_feed.code == "FR-IDF-TN":
return self.route.short_name return self.route.short_name
else: else:
return self.origin.stop_type return self.origin.stop_type
@property @property
def train_number(self): def train_number(self):
if self.route.transport_type == TransportType.TRANSILIEN: if self.gtfs_feed.code == "FR-IDF-TN":
return self.short_name return self.short_name
else: else:
return self.headsign return self.headsign
@ -422,13 +546,23 @@ class Trip(models.Model):
return "404042" return "404042"
return "000000" 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): def __str__(self):
return f"{self.origin.name} {self.departure_time}{self.destination.name} {self.arrival_time}" \ return self.origin_destination
f" - {self.service_id}"
class Meta: class Meta:
verbose_name = _("Trip") verbose_name = _("Trip")
verbose_name_plural = _("Trips") verbose_name_plural = _("Trips")
indexes = (models.Index(fields=['route']), models.Index(fields=['gtfs_feed']),)
class StopTime(models.Model): class StopTime(models.Model):
@ -510,6 +644,7 @@ class StopTime(models.Model):
class Meta: class Meta:
verbose_name = _("Stop time") verbose_name = _("Stop time")
verbose_name_plural = _("Stop times") verbose_name_plural = _("Stop times")
indexes = (models.Index(fields=['stop']), models.Index(fields=['trip']),)
class Calendar(models.Model): class Calendar(models.Model):
@ -555,10 +690,10 @@ class Calendar(models.Model):
verbose_name=_("End date"), verbose_name=_("End date"),
) )
transport_type = models.CharField( gtfs_feed = models.ForeignKey(
max_length=255, GTFSFeed,
verbose_name=_("Transport type"), on_delete=models.CASCADE,
choices=TransportType, verbose_name=_("GTFS feed"),
) )
def __str__(self): def __str__(self):
@ -568,6 +703,7 @@ class Calendar(models.Model):
verbose_name = _("Calendar") verbose_name = _("Calendar")
verbose_name_plural = _("Calendars") verbose_name_plural = _("Calendars")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['gtfs_feed']),)
class CalendarDate(models.Model): class CalendarDate(models.Model):
@ -600,6 +736,7 @@ class CalendarDate(models.Model):
verbose_name = _("Calendar date") verbose_name = _("Calendar date")
verbose_name_plural = _("Calendar dates") verbose_name_plural = _("Calendar dates")
ordering = ("id",) ordering = ("id",)
indexes = (models.Index(fields=['service']), models.Index(fields=['date']),)
class Transfer(models.Model): class Transfer(models.Model):
@ -668,10 +805,17 @@ class FeedInfo(models.Model):
verbose_name=_("Feed version"), verbose_name=_("Feed version"),
) )
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
class Meta: class Meta:
verbose_name = _("Feed info") verbose_name = _("Feed info")
verbose_name_plural = _("Feed infos") verbose_name_plural = _("Feed infos")
ordering = ("publisher_name",) ordering = ("publisher_name",)
indexes = (models.Index(fields=['gtfs_feed']),)
class TripUpdate(models.Model): class TripUpdate(models.Model):
@ -705,6 +849,7 @@ class TripUpdate(models.Model):
verbose_name_plural = _("Trip updates") verbose_name_plural = _("Trip updates")
ordering = ("start_date", "trip",) ordering = ("start_date", "trip",)
unique_together = ("trip", "start_date", "start_time",) unique_together = ("trip", "start_date", "start_time",)
indexes = (models.Index(fields=['trip']),)
class StopTimeUpdate(models.Model): class StopTimeUpdate(models.Model):
@ -753,3 +898,4 @@ class StopTimeUpdate(models.Model):
verbose_name_plural = _("Stop time updates") verbose_name_plural = _("Stop time updates")
ordering = ("trip_update", "stop_time",) ordering = ("trip_update", "stop_time",)
unique_together = ("trip_update", "stop_time",) unique_together = ("trip_update", "stop_time",)
indexes = (models.Index(fields=['trip_update']), models.Index(fields=['stop_time']),)