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: add fields to store when an event was acknowledged and by whom #278

Closed
wants to merge 3 commits into from
Closed
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
2 changes: 1 addition & 1 deletion src/Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM pyroapi:python3.8-alpine3.10
FROM pyronear/pyro-api:python3.8-alpine3.10

# copy requirements file
COPY requirements-dev.txt requirements-dev.txt
Expand Down
26 changes: 21 additions & 5 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,19 @@
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.schemas import (
AccessRead,
Acknowledgement,
AcknowledgementOut,
AlertOut,
EventIn,
EventOut,
EventUpdate,
UserRead,
)

router = APIRouter()

Expand Down Expand Up @@ -96,14 +106,20 @@ async def update_event(

@router.put("/{event_id}/acknowledge", response_model=AcknowledgementOut, summary="Acknowledge an existing event")
async def acknowledge_event(
event_id: int = Path(..., gt=0), requester=Security(get_current_access, scopes=[AccessType.admin, AccessType.user])
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 a event_id, acknowledge the specified event
Based on an event_id, acknowledge the specified 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, Acknowledgement(is_acknowledged=True), event_id)
return await crud.update_entry(
events,
Acknowledgement(is_acknowledged=True, acknowledged_by=user.id, acknowledged_ts=datetime.utcnow()),
event_id,
)


@router.delete("/{event_id}/", response_model=EventOut, summary="Delete a specific event")
Expand Down
5 changes: 4 additions & 1 deletion 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 @@ -28,9 +28,12 @@ class Event(Base):
start_ts = Column(DateTime, default=func.now())
end_ts = Column(DateTime, default=None, nullable=True)
is_acknowledged = Column(Boolean, default=False)
acknowledged_by = Column(Integer, ForeignKey("users.id"))
acknowledged_ts = Column(DateTime, default=None, nullable=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to follow the other named fields, perhaps a "acknowledged_at"?

created_at = Column(DateTime, default=func.now())

alerts: RelationshipProperty = relationship("Alert", back_populates="event")
acknowledger: RelationshipProperty = relationship("User", back_populates="acknowledged_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")
acknowledged_events: RelationshipProperty = relationship("Event", back_populates="acknowledger")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmh, that's a tricky column to maintain I think?
We could get that information by querying the event table with the user.id. Especially for French regions were everyone uses the same login, that field value could become huge otherwise


def __repr__(self):
return f"<User(login='{self.login}', created_at='{self.created_at}'>"
7 changes: 7 additions & 0 deletions src/app/schemas/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,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")
acknowledged_by: Optional[int] = Field(None, description="id of the user who acknowledged the event")
acknowledged_ts: Optional[datetime] = Field(None, description="event acknowledgement 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_ack_ts = validator("acknowledged_ts", pre=True, always=True, allow_reuse=True)(validate_datetime_none)


class EventOut(EventIn, _CreatedAt, _Id):
Expand All @@ -36,6 +39,10 @@ class EventOut(EventIn, _CreatedAt, _Id):

class Acknowledgement(BaseModel):
is_acknowledged: bool = Field(False, description="whether the event has been acknowledged")
acknowledged_by: Optional[int] = Field(None, description="id of the user who acknowledged the event")
acknowledged_ts: Optional[datetime] = Field(None, description="event acknowledgement timestamp")

_validate_ack_ts = validator("acknowledged_ts", pre=True, always=True, allow_reuse=True)(validate_datetime_none)


class AcknowledgementOut(Acknowledgement, _Id):
Expand Down
18 changes: 16 additions & 2 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,
"acknowledged_by": 2,
"acknowledged_ts": "2020-09-13T08:18:45.447773",
"created_at": "2020-10-13T08:18:45.447773",
},
{
Expand All @@ -34,6 +36,8 @@
"start_ts": "2020-09-13T08:18:45.447773",
"end_ts": None,
"is_acknowledged": True,
"acknowledged_by": 2,
"acknowledged_ts": "2020-09-13T08:18:45.447773",
"created_at": "2020-09-13T08:18:45.447773",
},
{
Expand All @@ -44,6 +48,8 @@
"start_ts": "2021-03-13T08:18:45.447773",
"end_ts": "2021-03-13T10:18:45.447773",
"is_acknowledged": False,
"acknowledged_by": None,
"acknowledged_ts": None,
"created_at": "2020-09-13T08:18:45.447773",
},
]
Expand Down Expand Up @@ -274,10 +280,14 @@ 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", "acknowledged_ts", "acknowledged_by")
} == 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 @@ -474,6 +484,7 @@ async def test_acknowledge_event(
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}/acknowledge", headers=auth)
assert response.status_code == status_code
if isinstance(status_details, str):
Expand All @@ -482,7 +493,10 @@ async def test_acknowledge_event(
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["is_acknowledged"]
assert updated_event["acknowledged_by"] == user_id
assert utc_dt < updated_event["acknowledged_ts"] < datetime.utcnow()


@pytest.mark.parametrize(
Expand Down
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", "acknowledged_ts")
else v
for k, v in entity_as_dict.items()
}

Expand Down