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
settings_local.py
*.log
*.txt
media/
output/
/static/

View File

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

View File

@ -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 <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" />
case "Trenitalia":
case "Trenitalia France":
return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" />
case "RENFE":
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, \
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.: |-]+"

View File

@ -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',)

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 ""
"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 <ynerant@emy.lu>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View File

@ -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
# First stop
drop_off_type = PickupType.NONE
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
# 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()

View File

@ -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,20 +92,13 @@ 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()
@ -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,
route, _created = Route.objects.get_or_create(
id=f"{gtfs_code}-ADDED-{headsign}",
gtfs_feed=gtfs_feed,
type=RouteType.RAIL,
short_name=trip_name,
long_name=trip_name,
short_name="ADDED",
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(
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,

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 _
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']),)