From 2a7e25f196f82d99215a59e955f42c8e26ff822d Mon Sep 17 00:00:00 2001 From: Justin Johnson Date: Thu, 28 Dec 2023 12:48:06 -0800 Subject: [PATCH] DBC22-1374 DBC22-1375 DBC22-1377 --- src/backend/apps/event/enums.py | 2 + .../event/migrations/0011_event_closed.py | 18 +++++ src/backend/apps/event/models.py | 3 +- src/backend/apps/event/serializers.py | 10 +++ src/backend/apps/event/tasks.py | 7 +- src/backend/apps/feed/client.py | 18 ++++- src/backend/apps/feed/constants.py | 1 + src/backend/apps/feed/serializers.py | 77 ++++++++++++------- src/backend/config/settings/drivebc.py | 1 + src/frontend/src/Components/data/events.js | 11 +-- src/frontend/src/Components/map/helper.js | 2 +- src/frontend/src/Components/map/mapPopup.js | 6 +- 12 files changed, 111 insertions(+), 45 deletions(-) create mode 100644 src/backend/apps/event/migrations/0011_event_closed.py diff --git a/src/backend/apps/event/enums.py b/src/backend/apps/event/enums.py index cf18ba592..8a7b21fb3 100644 --- a/src/backend/apps/event/enums.py +++ b/src/backend/apps/event/enums.py @@ -103,6 +103,7 @@ class EVENT_DIRECTION: 'event_sub_type', 'status', 'severity', + 'closed', 'direction', 'last_updated', 'location', @@ -116,6 +117,7 @@ class EVENT_DIRECTION: class EVENT_DISPLAY_CATEGORY: + CLOSURE = 'closures' MAJOR_DELAYS = 'majorEvents' MINOR_DELAYS = 'minorEvents' FUTURE_DELAYS = 'futureEvents' diff --git a/src/backend/apps/event/migrations/0011_event_closed.py b/src/backend/apps/event/migrations/0011_event_closed.py new file mode 100644 index 000000000..bcc011efc --- /dev/null +++ b/src/backend/apps/event/migrations/0011_event_closed.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-12-28 19:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0010_event_end_event_schedule_event_start'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='closed', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/backend/apps/event/models.py b/src/backend/apps/event/models.py index 4675137e9..7d6a8ed9f 100644 --- a/src/backend/apps/event/models.py +++ b/src/backend/apps/event/models.py @@ -13,6 +13,7 @@ class Event(BaseModel): # General status status = models.CharField(max_length=32) severity = models.CharField(max_length=32) + closed = models.BooleanField(default=False) # Location direction = models.CharField(max_length=32) @@ -26,7 +27,7 @@ class Event(BaseModel): last_updated = models.DateTimeField() # Schedule - schedule = models.JSONField(default={}) + schedule = models.JSONField(default={}) # Scheduled start and end start = models.DateTimeField(null=True) diff --git a/src/backend/apps/event/serializers.py b/src/backend/apps/event/serializers.py index e101ac766..55f66fbf1 100644 --- a/src/backend/apps/event/serializers.py +++ b/src/backend/apps/event/serializers.py @@ -23,6 +23,7 @@ class EventSerializer(serializers.ModelSerializer): direction_display = serializers.SerializerMethodField() route_display = serializers.SerializerMethodField() schedule = ScheduleSerializer() + severity = serializers.SerializerMethodField() class Meta: model = Event @@ -52,6 +53,9 @@ def get_route_display(self, obj): return res def get_display_category(self, obj): + if obj.closed: + return EVENT_DISPLAY_CATEGORY.CLOSURE + if obj.event_sub_type in EVENT_DISPLAY_CATEGORY_MAP: return EVENT_DISPLAY_CATEGORY_MAP[obj.event_sub_type] @@ -61,3 +65,9 @@ def get_display_category(self, obj): return EVENT_DISPLAY_CATEGORY.MAJOR_DELAYS \ if obj.severity == EVENT_SEVERITY.MAJOR \ else EVENT_DISPLAY_CATEGORY.MINOR_DELAYS + + def get_severity(self, obj): + if obj.closed: + return 'CLOSURE' + + return obj.severity \ No newline at end of file diff --git a/src/backend/apps/event/tasks.py b/src/backend/apps/event/tasks.py index 553a2e055..ccc14d03e 100644 --- a/src/backend/apps/event/tasks.py +++ b/src/backend/apps/event/tasks.py @@ -58,10 +58,15 @@ def populate_event_from_data(new_event_data): def populate_all_event_data(): - feed_data = FeedClient().get_event_list()['events'] + client = FeedClient() + closures = client.get_closures_dict() + feed_data = client.get_event_list()['events'] active_event_ids = [] for event_data in feed_data: + id = event_data.get("id", "").split("/")[-1] + event_data["closed"] = closures.get(id, False) + populate_event_from_data(event_data) # Event is active diff --git a/src/backend/apps/feed/client.py b/src/backend/apps/feed/client.py index 5f6d89c79..cfb7ea326 100644 --- a/src/backend/apps/feed/client.py +++ b/src/backend/apps/feed/client.py @@ -3,8 +3,9 @@ from urllib.parse import urljoin import httpx -from apps.feed.constants import INLAND_FERRY, OPEN511, WEBCAM +from apps.feed.constants import DIT, INLAND_FERRY, OPEN511, WEBCAM from apps.feed.serializers import ( + CarsClosureEventSerializer, EventAPISerializer, EventFeedSerializer, FerryAPISerializer, @@ -28,6 +29,9 @@ def __init__(self): OPEN511: { "base_url": settings.DRIVEBC_OPEN_511_API_BASE_URL, }, + DIT: { + "base_url": settings.DRIVEBC_DIT_API_BASE_URL, + }, INLAND_FERRY: { "base_url": settings.DRIVEBC_INLAND_FERRY_API_BASE_URL, }, @@ -84,7 +88,7 @@ def get_list_feed(self, resource_type, resource_name, serializer_cls, params=Non params = {} endpoint = self._get_endpoint(resource_type, resource_name) response_data = self._process_get_request(endpoint, params, resource_type) - serializer = serializer_cls(data=response_data) + serializer = serializer_cls(data=response_data, many=isinstance(response_data, list)) try: serializer.is_valid(raise_exception=True) @@ -125,6 +129,16 @@ def get_event_list(self): {"format": "json", "limit": 500} ) + def get_closures_dict(self): + """ Return a dict of :True for fast lookup of closed events by . """ + + events = self.get_list_feed( + DIT, 'dbcevents', CarsClosureEventSerializer, + {"format": "json", "limit": 500} + ) + + return {event["id"]:True for event in events if event["closed"]} + # Ferries def get_ferries_list(self): return self.get_list_feed( diff --git a/src/backend/apps/feed/constants.py b/src/backend/apps/feed/constants.py index fe6bf6225..9cc154c96 100644 --- a/src/backend/apps/feed/constants.py +++ b/src/backend/apps/feed/constants.py @@ -1,6 +1,7 @@ ROUTE_PLANNER = "route_planner" WEBCAM = "webcam" OPEN511 = "open511" +DIT = "dit" INLAND_FERRY = "inland_ferry" DIRECTIONS = { diff --git a/src/backend/apps/feed/serializers.py b/src/backend/apps/feed/serializers.py index 57edbf80a..cc1d934fe 100644 --- a/src/backend/apps/feed/serializers.py +++ b/src/backend/apps/feed/serializers.py @@ -53,6 +53,55 @@ class WebcamAPISerializer(serializers.Serializer): # Event +class CarsClosureEventSerializer(serializers.Serializer): + """ + Serializer to take CARS API events and retrieve ID and closed flag. + + As of January 2024, the CARS API events have the following structure + that we need: + + { + 'event-id': , + ..., + 'details': [ + { + 'category': , + 'code': , + }, + ... + ], + ... + } + + An event is consider to be marking a road closed if the category is + 'traffic_pattern' and the code is one that starts with 'closed' + (e.g., 'closed', 'closed ahead', 'closed for repairs'). Other subcategories + include the word 'closed' not at the beginning ('right lane closed') and + do no indicate a complete closure. + + TODO: Get list of closed subcategories for explicit enumeration. + """ + + id = serializers.CharField() + closed = serializers.BooleanField(default=False) + + def to_internal_value(self, data): + data["id"] = data["event-id"] + + data["closed"] = False + for detail in data.get("details", []): + if data["closed"]: break + + for description in detail.get("descriptions", []): + if data["closed"]: break + + kind = description.get("kind", {}) + data["closed"] = (kind.get("category") == "traffic_pattern" and + kind.get("code").startswith("closed")) + + return super().to_internal_value(data) + + class EventFeedSerializer(serializers.Serializer): id = serializers.CharField(max_length=32) @@ -66,6 +115,7 @@ class EventFeedSerializer(serializers.Serializer): # General status status = serializers.CharField(max_length=32) severity = serializers.CharField(max_length=32) + closed = serializers.BooleanField(default=False) # Location roads = EventRoadsField(source="*") @@ -80,23 +130,6 @@ class EventFeedSerializer(serializers.Serializer): schedule = serializers.JSONField() def to_internal_value(self, data): - # mapping CARS API fields to Open511 fields - # data['id'] = data["event-id"] - # details = data.get('open511-event-details', {}) - # data['event_type'] = details['event_type_description'] - # data['event_sub_type'] = details['event_subtype'] - # data['severity'] = data['representation']['priority']['name'].upper() - # data['updated'] = datetime.fromtimestamp(data['update-time']['time']/1000, - # pytz.timezone(data['update-time']['timeZoneId'])).isoformat() - # data['created'] = data['updated'] # hack because CARS API doesn't include event creation time - # data['status'] = EVENT_STATUS.ACTIVE - # data['roads'] = { - # 'to': details['event_road_to'], - # 'from': details['event_road_from'], - # 'name': 'Other roads', - # 'direction': DIRECTIONS.get(details['event_road_direction'], 'NONE') - # } - # data['geography'] = data['geometry'] internal_data = super().to_internal_value(data) schedule = internal_data.get('schedule', {}) @@ -112,16 +145,6 @@ def to_internal_value(self, data): return internal_data - def get_closed(self, obj): - for detail in self.initial_data.get('details', []): - for desc in detail.get('descriptions', []): - kind = desc.get('kind', {}) - if (kind.get('category') == 'traffic_pattern' and - kind.get('code') == 'closed'): - return True - - return False - class EventAPISerializer(serializers.Serializer): events = EventFeedSerializer(many=True) diff --git a/src/backend/config/settings/drivebc.py b/src/backend/config/settings/drivebc.py index c7b6b7cd3..d9ded1ed5 100644 --- a/src/backend/config/settings/drivebc.py +++ b/src/backend/config/settings/drivebc.py @@ -11,3 +11,4 @@ DRIVEBC_WEBCAM_API_BASE_URL = env("DRIVEBC_WEBCAM_API_BASE_URL") DRIVEBC_OPEN_511_API_BASE_URL = env("DRIVEBC_OPEN_511_API_BASE_URL") DRIVEBC_INLAND_FERRY_API_BASE_URL = env("DRIVEBC_INLAND_FERRY_API_BASE_URL") +DRIVEBC_DIT_API_BASE_URL = env("DRIVEBC_DIT_API_BASE_URL") diff --git a/src/frontend/src/Components/data/events.js b/src/frontend/src/Components/data/events.js index 8b57ee42c..926379e4f 100644 --- a/src/frontend/src/Components/data/events.js +++ b/src/frontend/src/Components/data/events.js @@ -4,16 +4,7 @@ export function getEvents(routePoints) { const payload = routePoints ? { route: routePoints } : {}; return get(`${window.API_HOST}/api/events/`, payload) - .then((data) => { - data.forEach((datum) => { - datum.roadIsClosed = !! datum.description.match(/Road closed(\.| )/); - if (datum.roadIsClosed) { - datum.severity = 'CLOSURE'; - datum.display_category = 'closures'; - } - }) - return data; - }) + .then((data) => data) .catch((error) => { console.log(error); }); diff --git a/src/frontend/src/Components/map/helper.js b/src/frontend/src/Components/map/helper.js index b8abd89e3..bb8325626 100644 --- a/src/frontend/src/Components/map/helper.js +++ b/src/frontend/src/Components/map/helper.js @@ -10,7 +10,7 @@ import { closureStyles, eventStyles } from '../data/featureStyleDefinitions.js'; // Static assets export const getEventIcon = (event, state) => { - if (event.get('roadIsClosed')) { + if (event.get('closed')) { return closureStyles[state]; } const severity = event.get('severity').toLowerCase(); diff --git a/src/frontend/src/Components/map/mapPopup.js b/src/frontend/src/Components/map/mapPopup.js index f739b4584..b40e748a7 100644 --- a/src/frontend/src/Components/map/mapPopup.js +++ b/src/frontend/src/Components/map/mapPopup.js @@ -10,7 +10,7 @@ import parse from 'html-react-parser'; import colocatedCamIcon from '../../images/colocated-camera.svg'; const displayCategoryMap = { - closure: 'Closure', + closures: 'Closure', majorEvents: 'Major Delay', minorEvents: 'Minor Delay', futureEvents: 'Future Delay', @@ -112,10 +112,10 @@ export function getEventPopup(eventFeature) {
- +
-

{ eventData.roadIsClosed ? 'Closure' : displayCategoryMap[eventData.display_category]}

+

{ displayCategoryMap[eventData.display_category]}