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

feat: fields and methods to set event type (wildfire or something else) #291

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions client/pyroclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"get-unacknowledged-events": "/events/unacknowledged",
"get-past-events": "/events/past",
"acknowledge-event": "/events/{event_id}/acknowledge",
"set-event-type": "/events/{event_id}/type?event_type={event_type}",
"get-alerts-for-event": "/events/{event_id}/alerts",
#################
# INSTALLATIONS
Expand Down Expand Up @@ -274,6 +275,22 @@ def acknowledge_event(self, event_id: int) -> Response:
self.routes["acknowledge-event"].format(event_id=event_id), headers=self.headers, timeout=self.timeout
)

def set_event_type(self, event_id: int, event_type: str):
"""Set the event type field value

Args:
event_id: ID of the associated event entry
event_type: event type ("wildfire" or other)

Returns:
HTTP response containing the updated event
"""
return requests.put(
self.routes["set-event-type"].format(event_id=event_id, event_type=event_type),
headers=self.headers,
timeout=self.timeout,
)

def get_site_devices(self, site_id: int) -> Response:
"""Fetch the devices that are installed on a specific site

Expand Down
2 changes: 2 additions & 0 deletions client/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def test_client_user(setup, user_client, mock_img):
events = _test_route_return(user_client.get_unacknowledged_events(), list)
event = _test_route_return(user_client.acknowledge_event(events[0]["id"]), dict)
assert event["is_acknowledged"]
type_info = _test_route_return(user_client.set_event_type(events[0]["id"], "wildfire"), dict)
assert type_info["type"] == "wildfire"
_test_route_return(user_client.get_all_alerts(), list)
_test_route_return(user_client.get_ongoing_alerts(), list)
_test_route_return(user_client.get_alerts_for_event(events[0]["id"]), list)
Expand Down
39 changes: 36 additions & 3 deletions src/app/api/endpoints/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# This program is licensed under the Apache License 2.0.
# See LICENSE or go to <https://opensource.org/licenses/Apache-2.0> for full license details.

from datetime import datetime
from typing import List, cast

from fastapi import APIRouter, Depends, Path, Security, status
Expand All @@ -12,10 +13,21 @@
from app.api import crud
from app.api.crud.authorizations import check_group_read, check_group_update, is_admin_access
from app.api.crud.groups import get_entity_group_id
from app.api.deps import get_current_access, get_db
from app.api.deps import get_current_access, get_current_user, get_db
from app.db import alerts, events
from app.models import Access, AccessType, Alert, Device, Event
from app.schemas import Acknowledgement, AcknowledgementOut, AlertOut, EventIn, EventOut, EventUpdate
from app.models import Access, AccessType, Alert, Device, Event, EventType
from app.schemas import (
AccessRead,
Acknowledgement,
AcknowledgementOut,
AlertOut,
EventIn,
EventOut,
EventTypeSetting,
EventTypeSettingOut,
EventUpdate,
UserRead,
)

router = APIRouter()

Expand Down Expand Up @@ -153,3 +165,24 @@ async def fetch_alerts_for_event(
requested_group_id = await get_entity_group_id(events, event_id)
await check_group_read(requester.id, cast(int, requested_group_id))
return await crud.base.database.fetch_all(query=alerts.select().where(alerts.c.event_id == event_id))


@router.put("/{event_id}/type", response_model=EventTypeSettingOut, summary="Redefine the type of an existing event")
async def update_event_type(
event_type: EventType,
event_id: int = Path(..., gt=0),
requester: AccessRead = Security(get_current_access, scopes=[AccessType.admin, AccessType.user]),
user: UserRead = Security(get_current_user, scopes=[AccessType.admin, AccessType.user]),
):
"""
Based on an event_id, redefine the type of the given event
"""
requested_group_id = await get_entity_group_id(events, event_id)
await check_group_update(requester.id, cast(int, requested_group_id))
return await crud.update_entry(
events,
EventTypeSetting(type=event_type, type_set_by=user.id, type_set_ts=datetime.utcnow())
if event_type != EventType.undefined
else EventTypeSetting(type=event_type, type_set_by=None, type_set_ts=None),
event_id,
)
12 changes: 10 additions & 2 deletions src/app/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import enum

from sqlalchemy import Boolean, Column, DateTime, Enum, Float, Integer
from sqlalchemy import Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer
from sqlalchemy.orm import RelationshipProperty, relationship
from sqlalchemy.sql import func

Expand All @@ -16,6 +16,11 @@

class EventType(str, enum.Enum):
wildfire: str = "wildfire"
domestic_fire: str = "domestic fire"
chimney: str = "chimney"
cloud: str = "cloud"
other: str = "other"
undefined: str = "undefined"


class Event(Base):
Expand All @@ -24,13 +29,16 @@ class Event(Base):
id = Column(Integer, primary_key=True)
lat = Column(Float(4, asdecimal=True))
lon = Column(Float(4, asdecimal=True))
type = Column(Enum(EventType), default=EventType.wildfire)
type = Column(Enum(EventType), default=EventType.undefined)
start_ts = Column(DateTime, default=func.now())
end_ts = Column(DateTime, default=None, nullable=True)
is_acknowledged = Column(Boolean, default=False)
created_at = Column(DateTime, default=func.now())
type_set_by = Column(Integer, ForeignKey("users.id"))
type_set_ts = Column(DateTime, default=None, nullable=True)

alerts: RelationshipProperty = relationship("Alert", back_populates="event")
type_setter: RelationshipProperty = relationship("User", back_populates="type_set_events")

def __repr__(self):
return (
Expand Down
1 change: 1 addition & 0 deletions src/app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class User(Base):

access: RelationshipProperty = relationship("Access", uselist=False, back_populates="user")
device: RelationshipProperty = relationship("Device", uselist=False, back_populates="owner")
type_set_events: RelationshipProperty = relationship("Event", back_populates="type_setter")

def __repr__(self):
return f"<User(login='{self.login}', created_at='{self.created_at}'>"
25 changes: 24 additions & 1 deletion src/app/schemas/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@

from .base import _CreatedAt, _FlatLocation, _Id, validate_datetime_none

__all__ = ["EventIn", "EventOut", "EventUpdate", "Acknowledgement", "AcknowledgementOut"]
__all__ = [
"EventIn",
"EventOut",
"EventUpdate",
"Acknowledgement",
"AcknowledgementOut",
"EventTypeSetting",
"EventTypeSettingOut",
]


# Events
Expand All @@ -25,9 +33,12 @@ class EventIn(_FlatLocation):
None, description="timestamp of event end", example=datetime.utcnow().replace(tzinfo=None)
)
is_acknowledged: bool = Field(False, description="whether the event has been acknowledged")
type_set_by: Optional[int] = Field(None, description="id of the user who defined the event type")
type_set_ts: Optional[datetime] = Field(None, description="event type definition timestamp")

_validate_start_ts = validator("start_ts", pre=True, always=True, allow_reuse=True)(validate_datetime_none)
_validate_end_ts = validator("end_ts", pre=True, always=True, allow_reuse=True)(validate_datetime_none)
_validate_type_ts = validator("type_set_ts", pre=True, always=True, allow_reuse=True)(validate_datetime_none)


class EventOut(EventIn, _CreatedAt, _Id):
Expand All @@ -42,6 +53,18 @@ class AcknowledgementOut(Acknowledgement, _Id):
pass


class EventTypeSetting(BaseModel):
type: EventType = Field(..., description="event type")
type_set_by: Optional[int] = Field(None, description="id of the user who defined the event type")
type_set_ts: Optional[datetime] = Field(None, description="event type definition timestamp")

_validate_type_ts = validator("type_set_ts", pre=True, always=True, allow_reuse=True)(validate_datetime_none)


class EventTypeSettingOut(EventTypeSetting, _Id):
pass


class EventUpdate(_FlatLocation):
type: EventType = Field(..., description="event type")
start_ts: datetime = Field(
Expand Down
53 changes: 50 additions & 3 deletions src/tests/routes/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"start_ts": "2020-09-13T08:18:45.447773",
"end_ts": "2020-09-13T08:18:45.447773",
"is_acknowledged": True,
"type_set_by": 2,
"type_set_ts": "2020-10-13T08:18:45.447773",
"created_at": "2020-10-13T08:18:45.447773",
},
{
Expand All @@ -34,16 +36,20 @@
"start_ts": "2020-09-13T08:18:45.447773",
"end_ts": None,
"is_acknowledged": True,
"type_set_by": 2,
"type_set_ts": "2020-10-13T08:18:45.447773",
"created_at": "2020-09-13T08:18:45.447773",
},
{
"id": 3,
"lat": -5.0,
"lon": 3.0,
"type": "wildfire",
"type": "undefined",
"start_ts": "2021-03-13T08:18:45.447773",
"end_ts": "2021-03-13T10:18:45.447773",
"is_acknowledged": False,
"type_set_by": None,
"type_set_ts": None,
"created_at": "2020-09-13T08:18:45.447773",
},
]
Expand Down Expand Up @@ -274,10 +280,12 @@ async def test_create_event(test_app_asyncio, init_test_db, test_db, access_idx,
if response.status_code // 100 == 2:
json_response = response.json()
test_response = {"id": len(EVENT_TABLE) + 1, **payload, "end_ts": None, "is_acknowledged": False}
assert {k: v for k, v in json_response.items() if k not in ("created_at", "start_ts")} == test_response
assert {
k: v for k, v in json_response.items() if k not in ("created_at", "start_ts", "type_set_by", "type_set_ts")
} == test_response
new_event_in_db = await get_entry(test_db, db.events, json_response["id"])
new_event_in_db = dict(**new_event_in_db)
assert new_event_in_db["created_at"] > utc_dt and new_event_in_db["created_at"] < datetime.utcnow()
assert utc_dt < new_event_in_db["created_at"] < datetime.utcnow()


@pytest.mark.parametrize(
Expand Down Expand Up @@ -559,3 +567,42 @@ async def test_fetch_alerts_for_event(
assert response.json() == [
entry for entry in ALERT_TABLE if (entry["event_id"] == event_id and entry["id"] in alerts_group_id)
]


@pytest.mark.parametrize(
"access_idx, event_id, event_type, status_code, status_details",
[
[None, 1, "undefined", 401, "Not authenticated"],
[0, 1, "wildfire", 200, None],
[1, 1, "domestic fire", 200, None],
[1, 1, "undefined", 200, None],
[1, 1, "lightning", 422, None],
[2, 1, "wildfire", 403, "Your access scope is not compatible with this operation."],
],
)
@pytest.mark.asyncio
async def test_update_event_type(
test_app_asyncio, init_test_db, test_db, access_idx, event_id, event_type, status_code, status_details
):
# Create a custom access token
auth = None
if isinstance(access_idx, int):
auth = await pytest.get_token(ACCESS_TABLE[access_idx]["id"], ACCESS_TABLE[access_idx]["scope"].split())

utc_dt = datetime.utcnow()
response = await test_app_asyncio.put(f"/events/{event_id}/type?event_type={event_type}", headers=auth)
assert response.status_code == status_code
if isinstance(status_details, str):
assert response.json()["detail"] == status_details

if response.status_code // 100 == 2:
updated_event = await get_entry(test_db, db.events, event_id)
updated_event = dict(**updated_event)
user_id = next(item["id"] for item in USER_TABLE if item["access_id"] == ACCESS_TABLE[access_idx]["id"])
assert updated_event["type"] == event_type
if event_type != "undefined":
assert updated_event["type_set_by"] == user_id
assert utc_dt < updated_event["type_set_ts"] < datetime.utcnow()
else:
assert updated_event["type_set_by"] is None
assert updated_event["type_set_ts"] is None
4 changes: 3 additions & 1 deletion src/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

def update_only_datetime(entity_as_dict: Dict[str, Any]):
return {
k: parse_time(v) if isinstance(v, str) and k in ("created_at", "start_ts", "end_ts", "last_ping") else v
k: parse_time(v)
if isinstance(v, str) and k in ("created_at", "start_ts", "end_ts", "last_ping", "type_set_ts")
else v
for k, v in entity_as_dict.items()
}

Expand Down
Loading