Compare commits

...

3 Commits

66 changed files with 2178 additions and 1537 deletions

1
.gitignore vendored
View File

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

View File

@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sncf.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,566 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-10 19:57+0100\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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"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"
#: sncfgtfs/models.py:12
msgid "Renfe"
msgstr "Renfe"
#: sncfgtfs/models.py:13
msgid "ÖBB"
msgstr "ÖBB"
#: sncfgtfs/models.py:17
msgid "Stop/platform"
msgstr "Arrêt / quai"
#: sncfgtfs/models.py:18
msgid "Station"
msgstr "Gare"
#: sncfgtfs/models.py:19
msgid "Entrance/exit"
msgstr "Entrée / sortie"
#: sncfgtfs/models.py:20
msgid "Generic node"
msgstr "Nœud générique"
#: sncfgtfs/models.py:21
msgid "Boarding area"
msgstr "Zone d'embarquement"
#: sncfgtfs/models.py:25
msgid "No information"
msgstr "Pas d'information"
#: sncfgtfs/models.py:26
msgid "Possible"
msgstr "Possible"
#: sncfgtfs/models.py:27 sncfgtfs/models.py:57
msgid "Not possible"
msgstr "Impossible"
#: sncfgtfs/models.py:31
msgid "Regular"
msgstr "Régulier"
#: sncfgtfs/models.py:32
msgid "None"
msgstr "Aucun"
#: sncfgtfs/models.py:33
msgid "Must phone agency"
msgstr "Doit téléphoner à l'agence"
#: sncfgtfs/models.py:34
msgid "Must coordinate with driver"
msgstr "Doit se coordonner avec læ conducteurice"
#: sncfgtfs/models.py:38
msgid "Tram"
msgstr "Tram"
#: sncfgtfs/models.py:39
msgid "Metro"
msgstr "Métro"
#: sncfgtfs/models.py:40
msgid "Rail"
msgstr "Rail"
#: sncfgtfs/models.py:41
msgid "Bus"
msgstr "Bus"
#: sncfgtfs/models.py:42
msgid "Ferry"
msgstr "Ferry"
#: sncfgtfs/models.py:43
msgid "Cable car"
msgstr "Câble"
#: sncfgtfs/models.py:44
msgid "Gondola"
msgstr "Gondole"
#: sncfgtfs/models.py:45
msgid "Funicular"
msgstr "Funiculaire"
#: sncfgtfs/models.py:49
msgid "Outbound"
msgstr "Vers l'extérieur"
#: sncfgtfs/models.py:50
msgid "Inbound"
msgstr "Vers l'intérieur"
#: sncfgtfs/models.py:54
msgid "Recommended"
msgstr "Recommandé"
#: sncfgtfs/models.py:55
msgid "Timed"
msgstr "Correspondance programmée"
#: sncfgtfs/models.py:56
msgid "Minimum time"
msgstr "Temps de correspondance minimum requis"
#: sncfgtfs/models.py:61 sncfgtfs/models.py:67
msgid "Added"
msgstr "Ajouté"
#: sncfgtfs/models.py:62
msgid "Removed"
msgstr "Supprimé"
#: sncfgtfs/models.py:66 sncfgtfs/models.py:76
msgid "Scheduled"
msgstr "Planifié"
#: sncfgtfs/models.py:68 sncfgtfs/models.py:79
msgid "Unscheduled"
msgstr "Non planifié"
#: sncfgtfs/models.py:69
msgid "Canceled"
msgstr "Annulé"
#: sncfgtfs/models.py:70
msgid "Replacement"
msgstr "Remplacé"
#: sncfgtfs/models.py:71
msgid "Duplicated"
msgstr "Dupliqué"
#: sncfgtfs/models.py:72
msgid "Deleted"
msgstr "Supprimé"
#: sncfgtfs/models.py:77
msgid "Skipped"
msgstr "Sauté"
#: sncfgtfs/models.py:78
msgid "No data"
msgstr "Pas de données"
#: sncfgtfs/models.py:86
msgid "Agency ID"
msgstr "ID de l'agence"
#: sncfgtfs/models.py:91
msgid "Agency name"
msgstr "Nom de l'agence"
#: sncfgtfs/models.py:95
msgid "Agency URL"
msgstr "URL de l'agence"
#: sncfgtfs/models.py:100
msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence"
#: sncfgtfs/models.py:105
msgid "Agency language"
msgstr "Langue de l'agence"
#: sncfgtfs/models.py:111
msgid "Agency phone"
msgstr "Téléphone de l'agence"
#: sncfgtfs/models.py:116
msgid "Agency email"
msgstr "Adresse email de l'agence"
#: sncfgtfs/models.py:124 sncfgtfs/models.py:242
msgid "Agency"
msgstr "Agence"
#: sncfgtfs/models.py:125
msgid "Agencies"
msgstr "Agences"
#: sncfgtfs/models.py:133 sncfgtfs/models.py:459
msgid "Stop ID"
msgstr "ID de l'arrêt"
#: sncfgtfs/models.py:138
msgid "Stop code"
msgstr "Code de l'arrêt"
#: sncfgtfs/models.py:144
msgid "Stop name"
msgstr "Nom de l'arrêt"
#: sncfgtfs/models.py:149
msgid "Stop description"
msgstr "Description de l'arrêt"
#: sncfgtfs/models.py:154
msgid "Stop longitude"
msgstr "Longitude de l'arrêt"
#: sncfgtfs/models.py:158
msgid "Stop latitude"
msgstr "Latitude de l'arrêt"
#: sncfgtfs/models.py:163
msgid "Zone ID"
msgstr "ID de la zone"
#: sncfgtfs/models.py:167
msgid "Stop URL"
msgstr "URL de l'arrêt"
#: sncfgtfs/models.py:172
msgid "Location type"
msgstr "Type de localisation"
#: sncfgtfs/models.py:181
msgid "Parent station"
msgstr "Gare parente"
#: sncfgtfs/models.py:189
msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt"
#: sncfgtfs/models.py:195
msgid "Level ID"
msgstr "ID du niveau"
#: sncfgtfs/models.py:200
msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant"
#: sncfgtfs/models.py:208
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
msgid "Stop"
msgstr "Arrêt"
#: sncfgtfs/models.py:228
msgid "Stops"
msgstr "Arrêts"
#: sncfgtfs/models.py:236 sncfgtfs/models.py:438 sncfgtfs/models.py:577
#: sncfgtfs/models.py:609
msgid "ID"
msgstr "Identifiant"
#: sncfgtfs/models.py:248
msgid "Route short name"
msgstr "Nom court de la ligne"
#: sncfgtfs/models.py:253
msgid "Route long name"
msgstr "Nom long de la ligne"
#: sncfgtfs/models.py:258
msgid "Route description"
msgstr "Description de la ligne"
#: sncfgtfs/models.py:263
msgid "Route type"
msgstr "Type de ligne"
#: sncfgtfs/models.py:268
msgid "Route URL"
msgstr "URL de la ligne"
#: sncfgtfs/models.py:274
msgid "Route color"
msgstr "Couleur de la ligne"
#: sncfgtfs/models.py:280
msgid "Route text color"
msgstr "Couleur du texte de la ligne"
#: sncfgtfs/models.py:294 sncfgtfs/models.py:309
msgid "Route"
msgstr "Ligne"
#: sncfgtfs/models.py:295
msgid "Routes"
msgstr "Lignes"
#: sncfgtfs/models.py:303
msgid "Trip ID"
msgstr "ID du trajet"
#: sncfgtfs/models.py:316 sncfgtfs/models.py:583
msgid "Service"
msgstr "Service"
#: sncfgtfs/models.py:322
msgid "Trip headsign"
msgstr "Destination du trajet"
#: sncfgtfs/models.py:328
msgid "Trip short name"
msgstr "Nom court du trajet"
#: sncfgtfs/models.py:333
msgid "Direction"
msgstr "Direction"
#: sncfgtfs/models.py:340
msgid "Block ID"
msgstr "ID du bloc"
#: sncfgtfs/models.py:346
msgid "Shape ID"
msgstr "ID de la forme"
#: sncfgtfs/models.py:351
msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant"
#: sncfgtfs/models.py:358
msgid "Bikes allowed"
msgstr "Vélos autorisés"
#: sncfgtfs/models.py:365
msgid "Last update"
msgstr "Dernière mise à jour"
#: sncfgtfs/models.py:430 sncfgtfs/models.py:444 sncfgtfs/models.py:681
msgid "Trip"
msgstr "Trajet"
#: sncfgtfs/models.py:431
msgid "Trips"
msgstr "Trajets"
#: sncfgtfs/models.py:449 sncfgtfs/models.py:731
msgid "Arrival time"
msgstr "Heure d'arrivée"
#: sncfgtfs/models.py:453 sncfgtfs/models.py:739
msgid "Departure time"
msgstr "Heure de départ"
#: sncfgtfs/models.py:464
msgid "Stop sequence"
msgstr "Séquence de l'arrêt"
#: sncfgtfs/models.py:469
msgid "Stop headsign"
msgstr "Destination de l'arrêt"
#: sncfgtfs/models.py:474
msgid "Pickup type"
msgstr "Type de prise en charge"
#: sncfgtfs/models.py:481
msgid "Drop off type"
msgstr "Type de dépose"
#: sncfgtfs/models.py:488
msgid "Timepoint"
msgstr "Ponctualité"
#: sncfgtfs/models.py:511 sncfgtfs/models.py:721
msgid "Stop time"
msgstr "Heure d'arrêt"
#: sncfgtfs/models.py:512
msgid "Stop times"
msgstr "Heures d'arrêt"
#: sncfgtfs/models.py:519
msgid "Service ID"
msgstr "ID du service"
#: sncfgtfs/models.py:523
msgid "Monday"
msgstr "Lundi"
#: sncfgtfs/models.py:527
msgid "Tuesday"
msgstr "Mardi"
#: sncfgtfs/models.py:531
msgid "Wednesday"
msgstr "Mercredi"
#: sncfgtfs/models.py:535
msgid "Thursday"
msgstr "Jeudi"
#: sncfgtfs/models.py:539
msgid "Friday"
msgstr "Vendredi"
#: sncfgtfs/models.py:543
msgid "Saturday"
msgstr "Samedi"
#: sncfgtfs/models.py:547
msgid "Sunday"
msgstr "Dimanche"
#: sncfgtfs/models.py:551 sncfgtfs/models.py:687
msgid "Start date"
msgstr "Date de début"
#: sncfgtfs/models.py:555
msgid "End date"
msgstr "Date de fin"
#: sncfgtfs/models.py:568
msgid "Calendar"
msgstr "Calendrier"
#: sncfgtfs/models.py:569
msgid "Calendars"
msgstr "Calendriers"
#: sncfgtfs/models.py:588
msgid "Date"
msgstr "Date"
#: sncfgtfs/models.py:592
msgid "Exception type"
msgstr "Type d'exception"
#: sncfgtfs/models.py:600
msgid "Calendar date"
msgstr "Date du calendrier"
#: sncfgtfs/models.py:601
msgid "Calendar dates"
msgstr "Dates du calendrier"
#: sncfgtfs/models.py:615
msgid "From stop"
msgstr "Depuis l'arrêt"
#: sncfgtfs/models.py:622
msgid "To stop"
msgstr "Jusqu'à l'arrêt"
#: sncfgtfs/models.py:627
msgid "Transfer type"
msgstr "Type de correspondance"
#: sncfgtfs/models.py:633
msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum"
#: sncfgtfs/models.py:638
msgid "Transfer"
msgstr "Correspondance"
#: sncfgtfs/models.py:639
msgid "Transfers"
msgstr "Correspondances"
#: sncfgtfs/models.py:646
msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux"
#: sncfgtfs/models.py:650
msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux"
#: sncfgtfs/models.py:655
msgid "Feed language"
msgstr "Langue du flux"
#: sncfgtfs/models.py:659
msgid "Feed start date"
msgstr "Date de début du flux"
#: sncfgtfs/models.py:663
msgid "Feed end date"
msgstr "Date de fin du flux"
#: sncfgtfs/models.py:668
msgid "Feed version"
msgstr "Version du flux"
#: sncfgtfs/models.py:672
msgid "Feed info"
msgstr "Information du flux"
#: sncfgtfs/models.py:673
msgid "Feed infos"
msgstr "Informations du flux"
#: sncfgtfs/models.py:691
msgid "Start time"
msgstr "Heure de début"
#: sncfgtfs/models.py:695 sncfgtfs/models.py:743
msgid "Schedule relationship"
msgstr "Relation de la planification"
#: sncfgtfs/models.py:704 sncfgtfs/models.py:714
msgid "Trip update"
msgstr "Mise à jour du trajet"
#: sncfgtfs/models.py:705
msgid "Trip updates"
msgstr "Mises à jour des trajets"
#: sncfgtfs/models.py:727
msgid "Arrival delay"
msgstr "Retard à l'arrivée"
#: sncfgtfs/models.py:735
msgid "Departure delay"
msgstr "Retard au départ"
#: sncfgtfs/models.py:752
msgid "Stop time update"
msgstr "Mise à jour du temps d'arrêt"
#: sncfgtfs/models.py:753
msgid "Stop time updates"
msgstr "Mises à jour des temps d'arrêt"

View File

@ -1,272 +0,0 @@
from datetime import timedelta, datetime, date, time
from zoneinfo import ZoneInfo
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, \
Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \
Trip, TripUpdate, TripScheduleRelationship
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):
pass
def handle(self, *args, **options):
for feed_type, feed_url in self.GTFS_RT_FEEDS.items():
self.stdout.write(f"Updating {feed_type} feed...")
feed_message = FeedMessage()
feed_message.ParseFromString(requests.get(feed_url).content)
stop_times_updates = []
for entity in feed_message.entity:
if entity.HasField("trip_update"):
trip_update = entity.trip_update
trip_id = trip_update.trip.trip_id
if feed_type in ["TGV", "IC", "TER"]:
trip_id = trip_id.split(":", 1)[0]
start_date = date(year=int(trip_update.trip.start_date[:4]),
month=int(trip_update.trip.start_date[4:6]),
day=int(trip_update.trip.start_date[6:]))
start_dt = datetime.combine(start_date, time(0), tzinfo=ZoneInfo("Europe/Paris"))
if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED:
# C'est un trajet nouveau. On crée le trajet associé.
self.create_trip(trip_update, trip_id, start_dt, feed_type)
if not Trip.objects.filter(id=trip_id).exists():
self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.")
continue
# Création du TripUpdate
tu, _created = TripUpdate.objects.update_or_create(
trip_id=trip_id,
start_date=trip_update.trip.start_date,
start_time=trip_update.trip.start_time,
defaults=dict(
schedule_relationship=trip_update.trip.schedule_relationship,
)
)
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
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}",
trip_id=trip_id,
stop_id=stop.id,
defaults={
"stop_sequence": stop_sequence,
"arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"departure_time": datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"pickup_type": (PickupType.REGULAR if stop_time_update.departure.time
else PickupType.NONE),
"drop_off_type": (PickupType.REGULAR if stop_time_update.arrival.time
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 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()
st_update = StopTimeUpdate(
trip_update=tu,
stop_time=st,
arrival_delay=timedelta(seconds=stop_time_update.arrival.delay),
arrival_time=datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")),
departure_delay=timedelta(seconds=stop_time_update.departure.delay),
departure_time=datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")),
schedule_relationship=stop_time_update.schedule_relationship
or StopScheduleRelationship.SCHEDULED,
)
stop_times_updates.append(st_update)
else:
self.stdout.write(str(entity))
StopTimeUpdate.objects.bulk_create(stop_times_updates,
update_conflicts=True,
update_fields=['arrival_delay', 'arrival_time',
'departure_delay', 'departure_time'],
unique_fields=['trip_update', 'stop_time'])
def create_trip(self, trip_update, trip_id, start_dt, feed_type):
headsign = trip_id[5:-1]
trip_qs = Trip.objects.all()
trip_ids = trip_qs.values_list('id', flat=True)
first_stop_queryset = StopTime.objects.filter(
stop__parent_station_id=trip_update.stop_time_update[0].stop_id,
).values('trip_id')
last_stop_queryset = StopTime.objects.filter(
stop__parent_station_id=trip_update.stop_time_update[-1].stop_id,
).values('trip_id')
trip_ids = trip_ids.intersection(first_stop_queryset).intersection(last_stop_queryset)
# print(trip_id, trip_ids)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
st_queryset = StopTime.objects.filter(stop__parent_station_id=stop_id)
if stop_sequence == 0:
st_queryset = st_queryset.filter(stop_sequence=0)
# print(stop_sequence, Stop.objects.get(id=stop_id).name, stop_time_update)
# print(trip_ids)
# print(st_queryset.values('trip_id').all())
trip_ids_restrict = trip_ids.intersection(st_queryset.values('trip_id'))
if trip_ids_restrict:
trip_ids = trip_ids_restrict
else:
stop = Stop.objects.get(id=stop_id)
self.stdout.write(self.style.WARNING(f"Warning: No trip is found passing by stop "
f"{stop.name} ({stop_id})"))
trip_ids = set(trip_ids)
route_ids = set(Trip.objects.filter(id__in=trip_ids).values_list('route_id', flat=True))
self.stdout.write(f"{len(route_ids)} routes found on trip for new train {headsign}")
if not route_ids:
origin_id = trip_update.stop_time_update[0].stop_id
origin = Stop.objects.get(id=origin_id)
destination_id = trip_update.stop_time_update[-1].stop_id
destination = Stop.objects.get(id=destination_id)
trip_name = f"{origin.name} - {destination.name}"
trip_reverse_name = f"{destination.name} - {origin.name}"
route_qs = Route.objects.filter(long_name=trip_name, transport_type=feed_type)
route_reverse_qs = Route.objects.filter(long_name=trip_reverse_name,
transport_type=feed_type)
if route_qs.exists():
route_ids = set(route_qs.values_list('id', flat=True))
elif route_reverse_qs.exists():
route_ids = set(route_reverse_qs.values_list('id', flat=True))
else:
self.stdout.write(f"Route not found for trip {trip_id} ({trip_name}). Creating new one")
route = Route.objects.create(
id=f"CREATED-{trip_name}",
agency=Agency.objects.filter(routes__transport_type=feed_type).first(),
transport_type=feed_type,
type=RouteType.RAIL,
short_name=trip_name,
long_name=trip_name,
)
route_ids = {route.id}
self.stdout.write(f"Route {route.id} created for trip {trip_id} ({trip_name})")
elif len(route_ids) > 1:
self.stdout.write(f"Multiple routes found for trip {trip_id}.")
self.stdout.write(", ".join(route_ids))
route_id = route_ids.pop()
Calendar.objects.update_or_create(
id=f"{feed_type}-new-{headsign}",
defaults={
"transport_type": feed_type,
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
"start_date": start_dt.date(),
"end_date": start_dt.date(),
}
)
CalendarDate.objects.update_or_create(
id=f"{feed_type}-{headsign}-{trip_update.trip.start_date}",
defaults={
"service_id": f"{feed_type}-new-{headsign}",
"date": trip_update.trip.start_date,
"exception_type": ExceptionType.ADDED,
}
)
Trip.objects.update_or_create(
id=trip_id,
defaults={
"route_id": route_id,
"service_id": f"{feed_type}-new-{headsign}",
"headsign": headsign,
"direction_id": trip_update.trip.direction_id,
}
)
sample_trip = Trip.objects.filter(id__in=trip_ids, 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
arr_time = datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
dep_time = datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
pickup_type = PickupType.REGULAR if stop_time_update.departure.time and stop_sequence > 0 \
else PickupType.NONE
drop_off_type = PickupType.REGULAR if stop_time_update.arrival.time \
and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE
StopTime.objects.update_or_create(
id=f"{trip_id}-{stop_id}",
trip_id=trip_id,
defaults={
"stop_id": stop_id,
"stop_sequence": stop_sequence,
"arrival_time": arr_time,
"departure_time": dep_time,
"pickup_type": pickup_type,
"drop_off_type": drop_off_type,
}
)

View File

@ -1,6 +0,0 @@
Django>=5.0,<6.0
django-cors-headers
django-filter
djangorestframework
protobuf
requests

View File

@ -1,11 +1,11 @@
{
"name": "sncf-station",
"name": "trainvel-front",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sncf-station",
"name": "trainvel-front",
"version": "0.1.0",
"dependencies": {
"@emotion/react": "^11.11.3",

View File

@ -1,5 +1,5 @@
{
"name": "sncf-station",
"name": "trainvel-front",
"version": "0.1.0",
"private": true,
"dependencies": {

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Écrans en gare affichant les horaires des trains des gares SNCF."
content="Écrans en gare affichant les horaires des trains des gares."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -18,7 +18,7 @@ function App() {
element: <Home />,
},
{
path: "/station/:stopId",
path: "/station/:theme/:stopId",
element: <Station />
}
])

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

@ -5,11 +5,11 @@ function Home() {
const navigate = useNavigate()
function onStationSelected(event, stop) {
navigate(`/station/${stop.id}/`)
navigate(`/station/sncf/${stop.id}/`)
}
return <>
<h1>Horaires SNCF</h1>
<h1>Horaires des trains</h1>
<h2>
Choisissez une gare dont vous désirez connaître le tableau des prochains départs et arrivées :
</h2>

View File

@ -12,7 +12,7 @@ function DateTimeSelector({stop, date, time}) {
function onStationSelected(event, stop) {
if (stop !== null)
navigate(`/station/${stop.id}/`)
navigate(`/station/sncf/${stop.id}/`)
}
return <>
@ -32,7 +32,7 @@ function DateTimeSelector({stop, date, time}) {
}
function Station() {
let {stopId} = useParams()
let {stopId, theme} = useParams()
let [searchParams, _setSearchParams] = useSearchParams()
const now = new Date()
let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`

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

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -3,4 +3,4 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "sncf.api"
name = "trainvel.api"

View File

@ -1,6 +1,6 @@
from rest_framework import serializers
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
from trainvel.gtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
Transfer, FeedInfo, TripUpdate, StopTimeUpdate

View File

@ -8,15 +8,14 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter, SearchFilter
from sncf.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \
from trainvel.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 trainvel.gtfs.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,5 +1,5 @@
"""
ASGI config for sncf project.
ASGI config for trainvel project.
It exposes the ASGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sncf.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
application = get_asgi_application()

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 trainvel.gtfs.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

@ -1,6 +1,6 @@
from django.apps import AppConfig
class SncfgtfsConfig(AppConfig):
class TrainvelGTFSConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "sncfgtfs"
name = "trainvel.gtfs"

View File

@ -0,0 +1,92 @@
[
{
"model": "gtfs.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": "gtfs.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": "gtfs.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": "gtfs.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": "gtfs.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": "gtfs.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": "gtfs.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": "gtfs.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": "gtfs.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

@ -0,0 +1,821 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-09 19:34+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: trainvel/gtfs/models.py:11
msgid "Albania"
msgstr "Albanie"
#: trainvel/gtfs/models.py:12
msgid "Andorra"
msgstr "Andorre"
#: trainvel/gtfs/models.py:13
msgid "Armenia"
msgstr "Arménie"
#: trainvel/gtfs/models.py:14
msgid "Austria"
msgstr "Autriche"
#: trainvel/gtfs/models.py:15
msgid "Azerbaijan"
msgstr "Azerbaijan"
#: trainvel/gtfs/models.py:16
msgid "Belgium"
msgstr "Belgique"
#: trainvel/gtfs/models.py:17
msgid "Bosnia and Herzegovina"
msgstr " Bosnie-Herzégovine"
#: trainvel/gtfs/models.py:18
msgid "Bulgaria"
msgstr "Bulgarie"
#: trainvel/gtfs/models.py:19
msgid "Croatia"
msgstr "Croatie"
#: trainvel/gtfs/models.py:20
msgid "Cyprus"
msgstr "Chypre"
#: trainvel/gtfs/models.py:21
msgid "Czech Republic"
msgstr "République Tchèque"
#: trainvel/gtfs/models.py:22
msgid "Denmark"
msgstr "Danemark"
#: trainvel/gtfs/models.py:23
msgid "Estonia"
msgstr "Estonie"
#: trainvel/gtfs/models.py:24
msgid "Finland"
msgstr "Finlande"
#: trainvel/gtfs/models.py:25
msgid "France"
msgstr "France"
#: trainvel/gtfs/models.py:26
msgid "Georgia"
msgstr "Géorgie"
#: trainvel/gtfs/models.py:27
msgid "Germany"
msgstr "Allemagne"
#: trainvel/gtfs/models.py:28
msgid "Greece"
msgstr "Grèce"
#: trainvel/gtfs/models.py:29
msgid "Hungary"
msgstr "Hongrie"
#: trainvel/gtfs/models.py:30
msgid "Iceland"
msgstr "Islande"
#: trainvel/gtfs/models.py:31
msgid "Ireland"
msgstr "Irlande"
#: trainvel/gtfs/models.py:32
msgid "Italy"
msgstr "Italie"
#: trainvel/gtfs/models.py:33
msgid "Latvia"
msgstr "Lettonie"
#: trainvel/gtfs/models.py:34
msgid "Liechtenstein"
msgstr "Liechtenstein"
#: trainvel/gtfs/models.py:35
msgid "Lithuania"
msgstr "Lituanie"
#: trainvel/gtfs/models.py:36
msgid "Luxembourg"
msgstr "Luxembourg"
#: trainvel/gtfs/models.py:37
msgid "Malta"
msgstr "Malte"
#: trainvel/gtfs/models.py:38
msgid "Moldova"
msgstr "Moldavie"
#: trainvel/gtfs/models.py:39
msgid "Monaco"
msgstr "Monaco"
#: trainvel/gtfs/models.py:40
msgid "Montenegro"
msgstr "Monténégro"
#: trainvel/gtfs/models.py:41
msgid "Netherlands"
msgstr "Pays-Bas"
#: trainvel/gtfs/models.py:42
msgid "North Macedonia"
msgstr "Macédoine du Nord"
#: trainvel/gtfs/models.py:43
msgid "Norway"
msgstr "Norvège"
#: trainvel/gtfs/models.py:44
msgid "Poland"
msgstr "Pologne"
#: trainvel/gtfs/models.py:45
msgid "Portugal"
msgstr "Portugal"
#: trainvel/gtfs/models.py:46
msgid "Romania"
msgstr "Roumanie"
#: trainvel/gtfs/models.py:47
msgid "San Marino"
msgstr "Saint-Marin"
#: trainvel/gtfs/models.py:48
msgid "Serbia"
msgstr "Serbie"
#: trainvel/gtfs/models.py:49
msgid "Slovakia"
msgstr "Slovaquie"
#: trainvel/gtfs/models.py:50
msgid "Slovenia"
msgstr "Slovénie"
#: trainvel/gtfs/models.py:51
msgid "Spain"
msgstr "Espagne"
#: trainvel/gtfs/models.py:52
msgid "Sweden"
msgstr "Suède"
#: trainvel/gtfs/models.py:53
msgid "Switzerland"
msgstr "Suisse"
#: trainvel/gtfs/models.py:54
msgid "Turkey"
msgstr "Turquie"
#: trainvel/gtfs/models.py:55
msgid "United Kingdom"
msgstr "Royaume-Uni"
#: trainvel/gtfs/models.py:56
msgid "Ukraine"
msgstr "Ukraine"
#: trainvel/gtfs/models.py:60
msgid "Stop/platform"
msgstr "Arrêt / quai"
#: trainvel/gtfs/models.py:61
msgid "Station"
msgstr "Gare"
#: trainvel/gtfs/models.py:62
msgid "Entrance/exit"
msgstr "Entrée / sortie"
#: trainvel/gtfs/models.py:63
msgid "Generic node"
msgstr "Nœud générique"
#: trainvel/gtfs/models.py:64
msgid "Boarding area"
msgstr "Zone d'embarquement"
#: trainvel/gtfs/models.py:68
msgid "No information"
msgstr "Pas d'information"
#: trainvel/gtfs/models.py:69
msgid "Possible"
msgstr "Possible"
#: trainvel/gtfs/models.py:70 trainvel/gtfs/models.py:100
msgid "Not possible"
msgstr "Impossible"
#: trainvel/gtfs/models.py:74
msgid "Regular"
msgstr "Régulier"
#: trainvel/gtfs/models.py:75
msgid "None"
msgstr "Aucun"
#: trainvel/gtfs/models.py:76
msgid "Must phone agency"
msgstr "Doit téléphoner à l'agence"
#: trainvel/gtfs/models.py:77
msgid "Must coordinate with driver"
msgstr "Doit se coordonner avec læ conducteurice"
#: trainvel/gtfs/models.py:81
msgid "Tram"
msgstr "Tram"
#: trainvel/gtfs/models.py:82
msgid "Metro"
msgstr "Métro"
#: trainvel/gtfs/models.py:83
msgid "Rail"
msgstr "Rail"
#: trainvel/gtfs/models.py:84
msgid "Bus"
msgstr "Bus"
#: trainvel/gtfs/models.py:85
msgid "Ferry"
msgstr "Ferry"
#: trainvel/gtfs/models.py:86
msgid "Cable car"
msgstr "Câble"
#: trainvel/gtfs/models.py:87
msgid "Gondola"
msgstr "Gondole"
#: trainvel/gtfs/models.py:88
msgid "Funicular"
msgstr "Funiculaire"
#: trainvel/gtfs/models.py:92
msgid "Outbound"
msgstr "Vers l'extérieur"
#: trainvel/gtfs/models.py:93
msgid "Inbound"
msgstr "Vers l'intérieur"
#: trainvel/gtfs/models.py:97
msgid "Recommended"
msgstr "Recommandé"
#: trainvel/gtfs/models.py:98
msgid "Timed"
msgstr "Correspondance programmée"
#: trainvel/gtfs/models.py:99
msgid "Minimum time"
msgstr "Temps de correspondance minimum requis"
#: trainvel/gtfs/models.py:104 trainvel/gtfs/models.py:110
msgid "Added"
msgstr "Ajouté"
#: trainvel/gtfs/models.py:105
msgid "Removed"
msgstr "Supprimé"
#: trainvel/gtfs/models.py:109 trainvel/gtfs/models.py:119
msgid "Scheduled"
msgstr "Planifié"
#: trainvel/gtfs/models.py:111 trainvel/gtfs/models.py:122
msgid "Unscheduled"
msgstr "Non planifié"
#: trainvel/gtfs/models.py:112
msgid "Canceled"
msgstr "Annulé"
#: trainvel/gtfs/models.py:113
msgid "Replacement"
msgstr "Remplacé"
#: trainvel/gtfs/models.py:114
msgid "Duplicated"
msgstr "Dupliqué"
#: trainvel/gtfs/models.py:115
msgid "Deleted"
msgstr "Supprimé"
#: trainvel/gtfs/models.py:120
msgid "Skipped"
msgstr "Sauté"
#: trainvel/gtfs/models.py:121
msgid "No data"
msgstr "Pas de données"
#: trainvel/gtfs/models.py:129
msgid "code"
msgstr "code"
#: trainvel/gtfs/models.py:130
msgid "Unique code of the feed."
msgstr "Code unique du flux."
#: trainvel/gtfs/models.py:135
msgid "name"
msgstr "nom"
#: trainvel/gtfs/models.py:137
msgid "Full name that describes the feed."
msgstr "Nom complet qui décrit le flux."
#: trainvel/gtfs/models.py:142
msgid "country"
msgstr "pays"
#: trainvel/gtfs/models.py:147
msgid "feed URL"
msgstr "URL du flux"
#: trainvel/gtfs/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."
#: trainvel/gtfs/models.py:153
msgid "realtime feed URL"
msgstr "URL du flux temps réel"
#: trainvel/gtfs/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."
#: trainvel/gtfs/models.py:161
msgid "last modified date"
msgstr "Date de dernière modification"
#: trainvel/gtfs/models.py:168
msgid "ETag"
msgstr "ETag"
#: trainvel/gtfs/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."
#: trainvel/gtfs/models.py:179 trainvel/gtfs/models.py:226
#: trainvel/gtfs/models.py:326 trainvel/gtfs/models.py:405
#: trainvel/gtfs/models.py:486 trainvel/gtfs/models.py:696
#: trainvel/gtfs/models.py:811
msgid "GTFS feed"
msgstr "flux GTFS"
#: trainvel/gtfs/models.py:180
msgid "GTFS feeds"
msgstr "flux GTFS"
#: trainvel/gtfs/models.py:189
msgid "Agency ID"
msgstr "ID de l'agence"
#: trainvel/gtfs/models.py:194
msgid "Agency name"
msgstr "Nom de l'agence"
#: trainvel/gtfs/models.py:198
msgid "Agency URL"
msgstr "URL de l'agence"
#: trainvel/gtfs/models.py:203
msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence"
#: trainvel/gtfs/models.py:208
msgid "Agency language"
msgstr "Langue de l'agence"
#: trainvel/gtfs/models.py:214
msgid "Agency phone"
msgstr "Téléphone de l'agence"
#: trainvel/gtfs/models.py:219
msgid "Agency email"
msgstr "Adresse email de l'agence"
#: trainvel/gtfs/models.py:233 trainvel/gtfs/models.py:356
msgid "Agency"
msgstr "Agence"
#: trainvel/gtfs/models.py:234
msgid "Agencies"
msgstr "Agences"
#: trainvel/gtfs/models.py:243 trainvel/gtfs/models.py:593
msgid "Stop ID"
msgstr "ID de l'arrêt"
#: trainvel/gtfs/models.py:248
msgid "Stop code"
msgstr "Code de l'arrêt"
#: trainvel/gtfs/models.py:254
msgid "Stop name"
msgstr "Nom de l'arrêt"
#: trainvel/gtfs/models.py:259
msgid "Stop description"
msgstr "Description de l'arrêt"
#: trainvel/gtfs/models.py:264
msgid "Stop longitude"
msgstr "Longitude de l'arrêt"
#: trainvel/gtfs/models.py:268
msgid "Stop latitude"
msgstr "Latitude de l'arrêt"
#: trainvel/gtfs/models.py:273
msgid "Zone ID"
msgstr "ID de la zone"
#: trainvel/gtfs/models.py:278
msgid "Stop URL"
msgstr "URL de l'arrêt"
#: trainvel/gtfs/models.py:283
msgid "Location type"
msgstr "Type de localisation"
#: trainvel/gtfs/models.py:292
msgid "Parent station"
msgstr "Gare parente"
#: trainvel/gtfs/models.py:300
msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt"
#: trainvel/gtfs/models.py:306
msgid "Level ID"
msgstr "ID du niveau"
#: trainvel/gtfs/models.py:311
msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant"
#: trainvel/gtfs/models.py:319
msgid "Platform code"
msgstr "Code du quai"
#: trainvel/gtfs/models.py:338
msgid "Stop"
msgstr "Arrêt"
#: trainvel/gtfs/models.py:339
msgid "Stops"
msgstr "Arrêts"
#: trainvel/gtfs/models.py:350 trainvel/gtfs/models.py:572
#: trainvel/gtfs/models.py:713 trainvel/gtfs/models.py:746
msgid "ID"
msgstr "Identifiant"
#: trainvel/gtfs/models.py:365
msgid "Route short name"
msgstr "Nom court de la ligne"
#: trainvel/gtfs/models.py:370
msgid "Route long name"
msgstr "Nom long de la ligne"
#: trainvel/gtfs/models.py:376
msgid "Route description"
msgstr "Description de la ligne"
#: trainvel/gtfs/models.py:381
msgid "Route type"
msgstr "Type de ligne"
#: trainvel/gtfs/models.py:386
msgid "Route URL"
msgstr "URL de la ligne"
#: trainvel/gtfs/models.py:392
msgid "Route color"
msgstr "Couleur de la ligne"
#: trainvel/gtfs/models.py:398
msgid "Route text color"
msgstr "Couleur du texte de la ligne"
#: trainvel/gtfs/models.py:412 trainvel/gtfs/models.py:428
msgid "Route"
msgstr "Ligne"
#: trainvel/gtfs/models.py:413
msgid "Routes"
msgstr "Lignes"
#: trainvel/gtfs/models.py:422
msgid "Trip ID"
msgstr "ID du trajet"
#: trainvel/gtfs/models.py:435 trainvel/gtfs/models.py:719
msgid "Service"
msgstr "Service"
#: trainvel/gtfs/models.py:441
msgid "Trip headsign"
msgstr "Destination du trajet"
#: trainvel/gtfs/models.py:447
msgid "Trip short name"
msgstr "Nom court du trajet"
#: trainvel/gtfs/models.py:452
msgid "Direction"
msgstr "Direction"
#: trainvel/gtfs/models.py:459
msgid "Block ID"
msgstr "ID du bloc"
#: trainvel/gtfs/models.py:465
msgid "Shape ID"
msgstr "ID de la forme"
#: trainvel/gtfs/models.py:470
msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant"
#: trainvel/gtfs/models.py:477
msgid "Bikes allowed"
msgstr "Vélos autorisés"
#: trainvel/gtfs/models.py:500 trainvel/gtfs/models.py:509
#: trainvel/gtfs/models.py:552 trainvel/gtfs/models.py:554
msgid "Unknown"
msgstr "Inconnu"
#: trainvel/gtfs/models.py:557
msgid "Origin → Destination"
msgstr "Origine → Destination"
#: trainvel/gtfs/models.py:563 trainvel/gtfs/models.py:578
#: trainvel/gtfs/models.py:825
msgid "Trip"
msgstr "Trajet"
#: trainvel/gtfs/models.py:564
msgid "Trips"
msgstr "Trajets"
#: trainvel/gtfs/models.py:583 trainvel/gtfs/models.py:876
msgid "Arrival time"
msgstr "Heure d'arrivée"
#: trainvel/gtfs/models.py:587 trainvel/gtfs/models.py:884
msgid "Departure time"
msgstr "Heure de départ"
#: trainvel/gtfs/models.py:598
msgid "Stop sequence"
msgstr "Séquence de l'arrêt"
#: trainvel/gtfs/models.py:603
msgid "Stop headsign"
msgstr "Destination de l'arrêt"
#: trainvel/gtfs/models.py:608
msgid "Pickup type"
msgstr "Type de prise en charge"
#: trainvel/gtfs/models.py:615
msgid "Drop off type"
msgstr "Type de dépose"
#: trainvel/gtfs/models.py:622
msgid "Timepoint"
msgstr "Ponctualité"
#: trainvel/gtfs/models.py:645 trainvel/gtfs/models.py:866
msgid "Stop time"
msgstr "Heure d'arrêt"
#: trainvel/gtfs/models.py:646
msgid "Stop times"
msgstr "Heures d'arrêt"
#: trainvel/gtfs/models.py:654
msgid "Service ID"
msgstr "ID du service"
#: trainvel/gtfs/models.py:658
msgid "Monday"
msgstr "Lundi"
#: trainvel/gtfs/models.py:662
msgid "Tuesday"
msgstr "Mardi"
#: trainvel/gtfs/models.py:666
msgid "Wednesday"
msgstr "Mercredi"
#: trainvel/gtfs/models.py:670
msgid "Thursday"
msgstr "Jeudi"
#: trainvel/gtfs/models.py:674
msgid "Friday"
msgstr "Vendredi"
#: trainvel/gtfs/models.py:678
msgid "Saturday"
msgstr "Samedi"
#: trainvel/gtfs/models.py:682
msgid "Sunday"
msgstr "Dimanche"
#: trainvel/gtfs/models.py:686 trainvel/gtfs/models.py:831
msgid "Start date"
msgstr "Date de début"
#: trainvel/gtfs/models.py:690
msgid "End date"
msgstr "Date de fin"
#: trainvel/gtfs/models.py:703
msgid "Calendar"
msgstr "Calendrier"
#: trainvel/gtfs/models.py:704
msgid "Calendars"
msgstr "Calendriers"
#: trainvel/gtfs/models.py:724
msgid "Date"
msgstr "Date"
#: trainvel/gtfs/models.py:728
msgid "Exception type"
msgstr "Type d'exception"
#: trainvel/gtfs/models.py:736
msgid "Calendar date"
msgstr "Date du calendrier"
#: trainvel/gtfs/models.py:737
msgid "Calendar dates"
msgstr "Dates du calendrier"
#: trainvel/gtfs/models.py:752
msgid "From stop"
msgstr "Depuis l'arrêt"
#: trainvel/gtfs/models.py:759
msgid "To stop"
msgstr "Jusqu'à l'arrêt"
#: trainvel/gtfs/models.py:764
msgid "Transfer type"
msgstr "Type de correspondance"
#: trainvel/gtfs/models.py:770
msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum"
#: trainvel/gtfs/models.py:775
msgid "Transfer"
msgstr "Correspondance"
#: trainvel/gtfs/models.py:776
msgid "Transfers"
msgstr "Correspondances"
#: trainvel/gtfs/models.py:783
msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux"
#: trainvel/gtfs/models.py:787
msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux"
#: trainvel/gtfs/models.py:792
msgid "Feed language"
msgstr "Langue du flux"
#: trainvel/gtfs/models.py:796
msgid "Feed start date"
msgstr "Date de début du flux"
#: trainvel/gtfs/models.py:800
msgid "Feed end date"
msgstr "Date de fin du flux"
#: trainvel/gtfs/models.py:805
msgid "Feed version"
msgstr "Version du flux"
#: trainvel/gtfs/models.py:815
msgid "Feed info"
msgstr "Information du flux"
#: trainvel/gtfs/models.py:816
msgid "Feed infos"
msgstr "Informations du flux"
#: trainvel/gtfs/models.py:835
msgid "Start time"
msgstr "Heure de début"
#: trainvel/gtfs/models.py:839 trainvel/gtfs/models.py:888
msgid "Schedule relationship"
msgstr "Relation de la planification"
#: trainvel/gtfs/models.py:848 trainvel/gtfs/models.py:859
msgid "Trip update"
msgstr "Mise à jour du trajet"
#: trainvel/gtfs/models.py:849
msgid "Trip updates"
msgstr "Mises à jour des trajets"
#: trainvel/gtfs/models.py:872
msgid "Arrival delay"
msgstr "Retard à l'arrivée"
#: trainvel/gtfs/models.py:880
msgid "Departure delay"
msgstr "Retard au départ"
#: trainvel/gtfs/models.py:897
msgid "Stop time update"
msgstr "Mise à jour du temps d'arrêt"
#: trainvel/gtfs/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,63 +2,55 @@ 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 trainvel.gtfs.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",
}
help = "Update the Trainvel GTFS database."
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.")
parser.add_argument('--dry-run', action='store_true',
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]
@ -66,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()
@ -90,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,
@ -101,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)
@ -118,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()
@ -126,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', ""),
@ -138,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)
@ -147,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:
@ -155,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'],
@ -175,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
@ -184,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()
@ -199,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'],
)
@ -208,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,
@ -218,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:
@ -232,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,
@ -247,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,
@ -270,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)
@ -279,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:
@ -287,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()
@ -296,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(':'))
@ -314,19 +295,16 @@ class Command(BaseCommand):
pickup_type = stop_time_dict.get('pickup_type', 0)
drop_off_type = stop_time_dict.get('drop_off_type', 0)
if transport_type in ["ES", "RENFE", "OBB"]:
if stop_time_dict['stop_sequence'] == "1":
drop_off_type = 1
elif arr_time == dep_time:
pickup_type = 1
elif transport_type == "TI":
if stop_time_dict['stop_sequence'] == "0":
drop_off_type = 1
elif arr_time == dep_time:
pickup_type = 1
if stop_time_dict['stop_sequence'] == "1":
# First stop
drop_off_type = PickupType.NONE
elif arr_time == dep_time:
# Last stop
pickup_type = PickupType.NONE
st = StopTime(
id=f"{trip_id}-{stop_id}-{stop_time_dict['departure_time']}",
id=f"{gtfs_code}-{stop_time_dict['trip_id']}-{stop_time_dict['stop_id']}"
f"-{stop_time_dict['departure_time']}",
trip_id=trip_id,
arrival_time=timedelta(seconds=arr_time),
departure_time=timedelta(seconds=dep_time),
@ -362,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'],
)
@ -394,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'],
@ -402,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

@ -0,0 +1,198 @@
from datetime import timedelta, datetime, date, time
from zoneinfo import ZoneInfo
import requests
from django.core.management import BaseCommand
from trainvel.gtfs.gtfs_realtime_pb2 import FeedMessage, TripUpdate as GTFSTripUpdate
from trainvel.gtfs.models import Agency, Calendar, CalendarDate, ExceptionType, GTFSFeed, PickupType, \
Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \
Trip, TripUpdate, TripScheduleRelationship
class Command(BaseCommand):
help = "Update the Trainvel GTFS Realtime database."
def add_arguments(self, parser):
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
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(gtfs_feed.rt_feed_url, allow_redirects=True).content)
stop_times_updates = []
if debug:
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
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]),
day=int(trip_update.trip.start_date[6:]))
start_dt = datetime.combine(start_date, time(0), tzinfo=ZoneInfo("Europe/Paris"))
if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED:
# C'est un trajet nouveau. On crée le trajet associé.
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.")
continue
# Création du TripUpdate
tu, _created = TripUpdate.objects.update_or_create(
trip_id=trip_id,
start_date=trip_update.trip.start_date,
start_time=trip_update.trip.start_time,
defaults=dict(
schedule_relationship=trip_update.trip.schedule_relationship,
)
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
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:
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,
defaults={
"stop_sequence": stop_sequence,
"arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"departure_time": datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"pickup_type": (PickupType.REGULAR if stop_time_update.departure.time
else PickupType.NONE),
"drop_off_type": (PickupType.REGULAR if stop_time_update.arrival.time
else PickupType.NONE),
}
)
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()
if st.stop_sequence != stop_sequence:
st.stop_sequence = stop_sequence
st.save()
st_update = StopTimeUpdate(
trip_update=tu,
stop_time=st,
arrival_delay=timedelta(seconds=stop_time_update.arrival.delay),
arrival_time=datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")),
departure_delay=timedelta(seconds=stop_time_update.departure.delay),
departure_time=datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")),
schedule_relationship=stop_time_update.schedule_relationship
or StopScheduleRelationship.SCHEDULED,
)
stop_times_updates.append(st_update)
else:
self.stdout.write(str(entity))
StopTimeUpdate.objects.bulk_create(stop_times_updates,
update_conflicts=True,
update_fields=['arrival_delay', 'arrival_time',
'departure_delay', 'departure_time'],
unique_fields=['trip_update', 'stop_time'])
def create_trip(self, trip_update: GTFSTripUpdate, trip_id: str, start_dt: datetime, gtfs_feed: GTFSFeed) -> None:
headsign = trip_id[5:-1]
gtfs_code = gtfs_feed.code
route, _created = Route.objects.get_or_create(
id=f"{gtfs_code}-ADDED-{headsign}",
gtfs_feed=gtfs_feed,
type=RouteType.RAIL,
short_name="ADDED",
long_name="ADDED ROUTE",
)
Calendar.objects.update_or_create(
id=f"{gtfs_code}-ADDED-{headsign}",
defaults={
"gtfs_feed": gtfs_feed,
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
"start_date": start_dt.date(),
"end_date": start_dt.date(),
}
)
CalendarDate.objects.update_or_create(
id=f"{gtfs_code}-ADDED-{headsign}-{trip_update.trip.start_date}",
defaults={
"service_id": f"{gtfs_code}-ADDED-{headsign}",
"date": trip_update.trip.start_date,
"exception_type": ExceptionType.ADDED,
}
)
Trip.objects.update_or_create(
id=trip_id,
defaults={
"route_id": route.id,
"service_id": f"{gtfs_code}-ADDED-{headsign}",
"headsign": headsign,
"direction_id": trip_update.trip.direction_id,
"gtfs_feed": gtfs_feed,
}
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
stop_id = f"{gtfs_code}-{stop_id}"
arr_time = datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
dep_time = datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
pickup_type = PickupType.REGULAR if stop_time_update.departure.time and stop_sequence > 0 \
else PickupType.NONE
drop_off_type = PickupType.REGULAR if stop_time_update.arrival.time \
and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE
StopTime.objects.update_or_create(
id=f"{trip_id}-{stop_time_update.stop_id}",
trip_id=trip_id,
defaults={
"stop_id": stop_id,
"stop_sequence": stop_sequence,
"arrival_time": arr_time,
"departure_time": dep_time,
"pickup_type": pickup_type,
"drop_off_type": drop_off_type,
}
)

View File

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

View File

@ -1,5 +1,5 @@
"""
Django settings for sncf project.
Django settings for trainvel project.
Generated by 'django-admin startproject' using Django 5.0.1.
@ -44,8 +44,8 @@ INSTALLED_APPS = [
"django_filters",
"rest_framework",
"sncf.api",
"sncfgtfs",
"trainvel.api",
"trainvel.gtfs",
]
MIDDLEWARE = [
@ -69,7 +69,7 @@ CORS_ALLOW_HEADERS = (
'Cache-Control',
)
ROOT_URLCONF = "sncf.urls"
ROOT_URLCONF = "trainvel.urls"
TEMPLATES = [
{
@ -87,7 +87,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = "sncf.wsgi.application"
WSGI_APPLICATION = "trainvel.wsgi.application"
# Database

View File

@ -11,8 +11,8 @@ CORS_ALLOWED_ORIGINS = [
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "sncf",
"USER": "sncf",
"NAME": "trainvel",
"USER": "trainvel",
"PASSWORD": "CHANGE ME",
"HOST": "localhost",
"PORT": "5432",

View File

@ -1,5 +1,5 @@
"""
URL configuration for sncf project.
URL configuration for trainvel project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
@ -18,7 +18,7 @@ from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from sncf.api.views import AgencyViewSet, StopViewSet, RouteViewSet, TripViewSet, StopTimeViewSet, \
from trainvel.api.views import AgencyViewSet, StopViewSet, RouteViewSet, TripViewSet, StopTimeViewSet, \
CalendarViewSet, CalendarDateViewSet, TransferViewSet, FeedInfoViewSet, NextDeparturesViewSet, NextArrivalsViewSet, \
TripUpdateViewSet, StopTimeUpdateViewSet

View File

@ -1,5 +1,5 @@
"""
WSGI config for sncf project.
WSGI config for trainvel project.
It exposes the WSGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sncf.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
application = get_wsgi_application()