Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jormun]: Fix for disrutions on POI #4307

Merged
merged 5 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions source/jormungandr/jormungandr/interfaces/v1/Journeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,62 @@ def wrapper(*args, **kwargs):
return wrapper


class handle_poi_disruptions(object):
@staticmethod
def is_absent(links, id):
return next((False for link in links if link['id'] == id), True)

def __call__(self, f):
@wraps(f)
def wrapper(*args, **kwargs):
objects = f(*args, **kwargs)
if has_invalid_reponse_code(objects) or journeys_absent(objects):
return objects

def get_disruption_uris(object):
uris = set()
for d in objects[0].get('disruptions', []):
for io in d.get('impacted_objects', []):
if io['pt_object']['embedded_type'] == "poi" and io['pt_object']['id'] == object['id']:
uris.add(d['id'])
if 'poi' not in io['pt_object']:
io['pt_object']['poi'] = object

return uris

def update_for_poi(object):
# Add links in poi object
object_copy = deepcopy(object)
object.setdefault('links', [])
disruption_uris = get_disruption_uris(object_copy)
for disruption_uri in disruption_uris:
if self.is_absent(object['links'], disruption_uri):
object['links'].append(
create_internal_link(_type="disruption", rel="disruptions", id=disruption_uri)
)

# We should only update 'from' object of the first section as well as 'to' object of the last one
# since object poi can only be present in those two cases
# If object is absent in first_section['from'] as well as last_section['to'] for the first journey
# then no need to verify for the remaining journeys
for j in objects[0].get('journeys', []):
if "sections" not in j:
continue

first_sec = j['sections'][0]
last_sec = j['sections'][-1]
if first_sec['from']['embedded_type'] != "poi" and last_sec['to']['embedded_type'] != "poi":
break
if first_sec['from']['embedded_type'] == "poi":
update_for_poi(first_sec['from']['poi'])
if last_sec['to']['embedded_type'] == "poi":
update_for_poi(last_sec['to']['poi'])

return objects

return wrapper


class rig_journey(object):
"""
decorator to rig journeys in order to put back the requested origin/destination in the journeys
Expand Down Expand Up @@ -693,6 +749,7 @@ def __init__(self):
@add_debug_info()
@add_fare_links()
@add_journey_href()
@handle_poi_disruptions()
@rig_journey()
@get_serializer(serpy=api.JourneysSerializer)
@ManageError()
Expand Down
64 changes: 64 additions & 0 deletions source/jormungandr/jormungandr/interfaces/v1/test/journey_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
from jormungandr.exceptions import RegionNotFound
from jormungandr.interfaces.v1.journey_common import compute_regions, sort_regions
from navitiacommon import models
import pytz
from flask import g
import jormungandr.scenarios.tests.helpers_tests as helpers_tests
from jormungandr.interfaces.v1.serializer import api
from jormungandr import app
from jormungandr.interfaces.v1.decorators import get_serializer
from jormungandr.interfaces.v1.Journeys import handle_poi_disruptions, rig_journey
from jormungandr.interfaces.v1.errors import ManageError


class MockInstance:
Expand Down Expand Up @@ -210,3 +218,59 @@ def test_sorted_regions(self):
assert regions[2].name == self.regions['netherlands'].name
assert regions[3].name == self.regions['france'].name
assert regions[4].name == self.regions['equador'].name


@handle_poi_disruptions()
@rig_journey()
@get_serializer(serpy=api.JourneysSerializer)
@ManageError()
def get_response_with_poi_and_disruptions():
return helpers_tests.get_pb_response_with_journeys_and_disruptions()


def handle_poi_disruptions_test():
"""
Both departure and arrival points are POI
Only one disruption exist in the response on poi_uri_a
journey 1 : walking + PT + walking
journey 2 : walking
"""
with app.app_context():
with app.test_request_context():
g.timezone = pytz.utc
g.origin_detail = helpers_tests.get_json_entry_point(id='poi_uri_a', name='poi_name_a')
g.destination_detail = helpers_tests.get_json_entry_point(id='poi_uri_b', name='poi_name_b')

# get response in json with two journeys and one disruption
resp = get_response_with_poi_and_disruptions()
assert len(resp[0].get("journeys", 0)) == 2

# Journey 1: a disruption exist for poi_uri_a: the poi in journey should have links and
# impacted_object should have object poi
origin = resp[0].get("journeys", 0)[0]['sections'][0]['from']['poi']
assert origin['id'] == "poi_uri_a"
assert len(origin['links']) == 1
impacted_object = resp[0]['disruptions'][0]['impacted_objects'][0]['pt_object']['poi']
assert impacted_object['id'] == "poi_uri_a"
assert "links" not in impacted_object

# No disruption exist for poi_uri_b: the poi in journey doesn't have links and object poi is absent
# in impacted_object
destination = resp[0].get("journeys", 0)[0]['sections'][2]['to']['poi']
assert destination['id'] == "poi_uri_b"
assert len(destination["links"]) == 0

# Journey 2: a disruption exist for poi_uri_a: the poi in journey should have links and
# impacted_object should have object poi
origin = resp[0].get("journeys", 0)[1]['sections'][0]['from']['poi']
assert origin['id'] == "poi_uri_a"
assert len(origin['links']) == 1
impacted_object = resp[0]['disruptions'][0]['impacted_objects'][0]['pt_object']['poi']
assert impacted_object['id'] == "poi_uri_a"
assert "links" not in impacted_object

# No disruption exist for poi_uri_b: the poi in journey doesn't have links and object poi is absent
# in impacted_object
destination = resp[0].get("journeys", 0)[1]['sections'][0]['to']['poi']
assert destination['id'] == "poi_uri_b"
assert len(destination["links"]) == 0
28 changes: 8 additions & 20 deletions source/jormungandr/jormungandr/scenarios/new_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,44 +498,32 @@ def update_total_co2_emission(pb_resp):

def update_disruptions_on_pois(instance, pb_resp):
"""
Maintain a set of uri from all journey.section.origin and journey.section.destination of type Poi,
Maintain a set of uri from g.origin_detail and g.destination_detail of type Poi,
call loki with api api_disruptions&pois[]...
For each disruption on poi, add disruption id in the attribute links and add disruptions in the response
"""
if not pb_resp.journeys:
return
# Add uri of all the pois in a set
poi_uris = set()
poi_objets = []
since_datetime = date_to_timestamp(datetime.utcnow())
until_datetime = date_to_timestamp(datetime.utcnow())
for j in pb_resp.journeys:
for s in j.sections:
if s.origin.embedded_type == type_pb2.POI:
poi_uris.add(s.origin.uri)
poi_objets.append(s.origin.poi)
since_datetime = min(since_datetime, s.begin_date_time)

if s.destination.embedded_type == type_pb2.POI:
poi_uris.add(s.destination.uri)
poi_objets.append(s.destination.poi)
until_datetime = max(until_datetime, s.end_date_time)
# Here we manage origin and destination of type POI
if g.origin_detail and g.origin_detail.get('embedded_type') == "poi":
poi_uris.add(g.origin_detail.get('id'))

if g.destination_detail and g.destination_detail.get('embedded_type') == "poi":
poi_uris.add(g.destination_detail.get('id'))

if since_datetime >= until_datetime:
since_datetime = until_datetime - 1

# Get disruptions for poi_uris calling loki with api poi_disruptions and poi_uris in param
poi_disruptions = get_disruptions_on_poi(instance, poi_uris, since_datetime, until_datetime)
if poi_disruptions is None:
return

# For each poi in pt_objects:
# add impact_uris from resp_poi and
# copy object poi in impact.impacted_objects
for pt_object in poi_objets:
impact_uris = get_impact_uris_for_poi(poi_disruptions, pt_object)
for impact_uri in impact_uris:
pt_object.impact_uris.append(impact_uri)

# Add all impacts from resp_poi to the response
add_disruptions(pb_resp, poi_disruptions)

Expand Down
79 changes: 76 additions & 3 deletions source/jormungandr/jormungandr/scenarios/tests/helpers_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ def fill_best_boarding_position_test():
assert response_pb2.BoardingPosition.BACK not in journey.sections[0].best_boarding_positions


def get_response_with_a_disruption_on_poi():
def get_response_with_a_disruption_on_poi(uri="poi_uri", name="poi_name_from_loki"):
start_period = "20240712T165200"
end_period = "20240812T165200"
response = response_pb2.Response()
Expand All @@ -531,8 +531,8 @@ def get_response_with_a_disruption_on_poi():

# poi = make_pt_object(type_pb2.POI, lon=1, lat=2, uri='poi:test_uri')
# impacted_object.pt_object.CopyFrom(poi)
impacted_object.pt_object.name = "poi_name_from_loki"
impacted_object.pt_object.uri = "poi_uri"
impacted_object.pt_object.name = name
impacted_object.pt_object.uri = uri
impacted_object.pt_object.embedded_type = type_pb2.POI
impact.updated_at = utils.str_to_time_stamp(u'20240712T205200')
application_period = impact.application_periods.add()
Expand Down Expand Up @@ -623,6 +623,79 @@ def get_journey_with_pois():
return response


def get_pb_response_with_journeys_and_disruptions():
response = response_pb2.Response()

# Add a journey : walking address to stop_point + PT stop_point to stop_point + walking toward address
journey = response.journeys.add()
section = journey.sections.add()
section.type = response_pb2.STREET_NETWORK
section.street_network.mode = response_pb2.Walking
section.origin.uri = 'address_a'
section.origin.embedded_type = type_pb2.ADDRESS
section.destination.uri = 'stop_point_a'
section.destination.embedded_type = type_pb2.STOP_POINT
section.destination.stop_point.uri = 'stop_point_a'
section.destination.stop_point.name = 'stop_point_name_a'
section.destination.stop_point.coord.lon = 1.0
section.destination.stop_point.coord.lat = 2.0

section = journey.sections.add()
section.type = response_pb2.PUBLIC_TRANSPORT
section.origin.uri = 'stop_point_a'
section.origin.embedded_type = type_pb2.STOP_POINT
section.origin.stop_point.uri = 'stop_point_a'
section.origin.stop_point.name = 'stop_point_name_a'
section.origin.stop_point.coord.lon = 1.0
section.origin.stop_point.coord.lat = 2.0
section.destination.uri = 'stop_point_b'
section.destination.embedded_type = type_pb2.STOP_POINT
section.destination.stop_point.uri = 'stop_point_b'
section.destination.stop_point.name = 'stop_point_name_b'
section.destination.stop_point.coord.lon = 3.0
section.destination.stop_point.coord.lat = 4.0

section = journey.sections.add()
section.type = response_pb2.STREET_NETWORK
section.street_network.mode = response_pb2.Walking
section.origin.uri = 'stop_point_b'
section.origin.embedded_type = type_pb2.STOP_POINT
section.origin.stop_point.uri = 'stop_point_b'
section.origin.stop_point.name = 'stop_point_name_b'
section.origin.stop_point.coord.lon = 3.0
section.origin.stop_point.coord.lat = 4.0
section.destination.uri = 'address_b'
section.destination.embedded_type = type_pb2.ADDRESS

# Add a journey : walking address to address
journey = response.journeys.add()
section = journey.sections.add()
section.type = response_pb2.STREET_NETWORK
section.street_network.mode = response_pb2.Walking
section.origin.uri = 'address_a'
section.origin.embedded_type = type_pb2.ADDRESS
section.destination.uri = 'address_b'
section.destination.embedded_type = type_pb2.ADDRESS

# Add disruption on poi 'poi_uri_a':
pb_disruptions = get_response_with_a_disruption_on_poi(uri="poi_uri_a", name="poi_name_a")
response.impacts.extend(pb_disruptions.impacts)
response.status_code = 200
return response


def get_json_entry_point(id="poi_uri", name="poi_name_from_kraken"):
entry_point = {}
entry_point['id'] = id
entry_point['name'] = name
entry_point['embedded_type'] = "poi"
object = {}
object['id'] = id
object['name'] = name
entry_point['poi'] = object
return entry_point


def verify_poi_in_impacted_objects(object, poi_empty=True):
assert object.name == "poi_name_from_loki"
assert object.uri == "poi_uri"
Expand Down
61 changes: 33 additions & 28 deletions source/jormungandr/jormungandr/scenarios/tests/new_default_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,36 +805,41 @@ def __init__(self, name="fake_instance", olympics_forbidden_uris=None):


def journey_with_disruptions_on_poi_test(mocker):
instance = lambda: None
# As in navitia, object poi in the response of places_nearby doesn't have any impact
response_journey_with_pois = helpers_tests.get_journey_with_pois()
assert len(response_journey_with_pois.impacts) == 0
assert len(response_journey_with_pois.journeys) == 1
journey = response_journey_with_pois.journeys[0]
assert len(journey.sections) == 3

# Prepare disruptions on poi as response of end point poi_disruptions of loki
# pt_object poi as impacted object is absent in the response of poi_disruptions
disruptions_with_poi = helpers_tests.get_response_with_a_disruption_on_poi()
assert len(disruptions_with_poi.impacts) == 1
assert disruptions_with_poi.impacts[0].uri == "test_impact_uri"
assert len(disruptions_with_poi.impacts[0].impacted_objects) == 1
object = disruptions_with_poi.impacts[0].impacted_objects[0].pt_object
helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=True)

mock = mocker.patch(
'jormungandr.scenarios.new_default.get_disruptions_on_poi', return_value=disruptions_with_poi
)
update_disruptions_on_pois(instance, response_journey_with_pois)
with app.app_context():
instance = lambda: None
g.origin_detail = helpers_tests.get_json_entry_point(id='poi_uri', name='poi_name_from_kraken')
g.destination_detail = helpers_tests.get_json_entry_point(id='poi_b', name='poi_n_name')
# As in navitia, object poi in the response of places_nearby doesn't have any impact
response_journey_with_pois = helpers_tests.get_journey_with_pois()
assert len(response_journey_with_pois.impacts) == 0
assert len(response_journey_with_pois.journeys) == 1
journey = response_journey_with_pois.journeys[0]
assert len(journey.sections) == 3

# Prepare disruptions on poi as response of end point poi_disruptions of loki
# pt_object poi as impacted object is absent in the response of poi_disruptions
disruptions_with_poi = helpers_tests.get_response_with_a_disruption_on_poi()
assert len(disruptions_with_poi.impacts) == 1
assert disruptions_with_poi.impacts[0].uri == "test_impact_uri"
assert len(disruptions_with_poi.impacts[0].impacted_objects) == 1
object = disruptions_with_poi.impacts[0].impacted_objects[0].pt_object
helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=True)

mock = mocker.patch(
'jormungandr.scenarios.new_default.get_disruptions_on_poi', return_value=disruptions_with_poi
)
update_disruptions_on_pois(instance, response_journey_with_pois)

assert len(response_journey_with_pois.impacts) == 1
impact = response_journey_with_pois.impacts[0]
assert len(impact.impacted_objects) == 1
object = impact.impacted_objects[0].pt_object

assert len(response_journey_with_pois.impacts) == 1
impact = response_journey_with_pois.impacts[0]
assert len(impact.impacted_objects) == 1
object = impact.impacted_objects[0].pt_object
helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=False)
# In this state we haven't yet managed the final response so poi object is empty
helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=True)

mock.assert_called_once()
return
mock.assert_called_once()
return


def journey_with_booking_rule_test():
Expand Down
Loading