Skip to content

Commit

Permalink
feat: add detour notifications to notifications module
Browse files Browse the repository at this point in the history
feat(ex/notifications/detour): add virtual fields to detour notification schema
feat(ex/notifications): query detour info from notifications
  • Loading branch information
firestack committed Sep 25, 2024
1 parent 3e40475 commit c231cfb
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 3 deletions.
68 changes: 67 additions & 1 deletion lib/notifications/db/detour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@ defmodule Notifications.Db.Detour do
@derive {Jason.Encoder,
only: [
:__struct__,
:status
:status,
:headsign,
:route,
:direction,
:origin
]}

typed_schema "detour_notifications" do
belongs_to :detour, Skate.Detours.Db.Detour
has_one :notification, Notifications.Db.Notification

field :status, Ecto.Enum, values: [:activated]

# Derived from the associated detour
field :headsign, :any, virtual: true
field :route, :any, virtual: true
field :direction, :any, virtual: true
field :origin, :any, virtual: true
end

def changeset(
Expand All @@ -33,4 +43,60 @@ defmodule Notifications.Db.Detour do
:status
])
end

defmodule Queries do
@moduledoc """
Defines composable queries for retrieving `Notifications.Db.Detour` info.
"""

import Ecto.Query

@doc """
The "base" query that queries `Notifications.Db.Detour`'s without restriction
"""
def base() do
from(d in Notifications.Db.Detour, as: :detour_notification, select_merge: d)
end

@doc """
Retrieves detour information for notifications from the `Notifications.Db.Detour` table
"""
def get_derived_info(query \\ base()) do
from(
[detour_notification: dn] in query,
left_join: ad in assoc(dn, :detour),
as: :associated_detour,
select_merge: %{
route: ad.state["context"]["route"]["name"],
origin: ad.state["context"]["routePattern"]["name"],
headsign: ad.state["context"]["routePattern"]["headsign"],

# Ecto can't figure out how to index a JSON map via another JSON value
# because (in the ways it was tried) Ecto won't allow us to use the
# value from the associated detour, `ad`, as a value in the
# ["JSON path"](https://hexdocs.pm/ecto/Ecto.Query.API.html#json_extract_path/2).
#
# i.e., this
# ad.state["context"]["route"]["directionNames"][
# ad.state["context"]["routePattern"]["directionId"]
# ]
#
# But, Postgres _is_ able to do this, _if_ we get the types correct.
# A JSON value in Postgres is either of type JSON or JSONB, but
# - indexing a JSON array requires an `INTEGER`,
# - accessing a JSON map, requires Postgres's `TEXT` type.
#
# So because we know the `directionId` will correspond to the keys in
# `directionNames`, casting the `directionId` to `TEXT` allows us to
# access the `directionNames` JSON map
direction:
fragment(
"? -> CAST(? AS TEXT)",
ad.state["context"]["route"]["directionNames"],
ad.state["context"]["routePattern"]["directionId"]
)
}
)
end
end
end
53 changes: 53 additions & 0 deletions lib/notifications/db/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,59 @@ defmodule Notifications.Db.Notification do
}
)
end

@doc """
Joins associated `Notifications.Db.Detour`'s on
`Notifications.Db.Notification`'s and retrieves the Detour's
associated info.
## Example
The `query` parameter defaults to
`Notifications.Db.Notification.Queries.base()`, but can be specified
explicitly
iex> Notifications.Db.Notification.Queries.base()
...> |> Notifications.Db.Notification.Queries.select_detour_info()
...> |> Skate.Repo.all()
[
%Notifications.Db.Notification{
detour: %Notifications.Db.Detour{}
},
%Notifications.Db.Notification{
detour_id: nil,
detour: nil
},
_
]
iex> Notifications.Db.Notification.Queries.select_detour_info()
...> |> Skate.Repo.all()
[
%Notifications.Db.Notification{
detour: %Notifications.Db.Detour{ ... }
},
%Notifications.Db.Notification{
detour_id: nil,
detour: nil
},
_
]
"""
@spec select_detour_info(Ecto.Query.t()) :: Ecto.Query.t()
@spec select_detour_info() :: Ecto.Query.t()
def select_detour_info(query \\ base()) do
from([notification: n] in query,
left_join: detour in subquery(Notifications.Db.Detour.Queries.get_derived_info()),
on: detour.id == n.detour_id,
select_merge: %{
detour: detour
}
)
end

@doc """
Joins associated `Notifications.Db.BridgeMovement`'s on
`Notifications.Db.Notification`'s
Expand Down
24 changes: 24 additions & 0 deletions lib/notifications/detour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Notifications.Detour do
@moduledoc """
Context for working with Detour notifications
"""

@doc """
Creates a detour notification struct from a detour to insert into the database
"""
def detour_notification(%Skate.Detours.Db.Detour{} = detour) do
%Notifications.Db.Detour{
detour: detour
}
end

@doc """
Creates a activated detour notification struct to insert into the database
"""
def activated_detour(%Skate.Detours.Db.Detour{} = detour) do
%{
detour_notification(detour)
| status: :activated
}
end
end
67 changes: 67 additions & 0 deletions lib/notifications/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,66 @@ defmodule Notifications.Notification do
:content
]

@doc """
Inserts a new notification for an activated detour into the database
and returns the detour notification with notification info.
"""
def create_activated_detour_notification_from_detour(%Skate.Detours.Db.Detour{} = detour) do
import Notifications.Db.Notification.Queries

notification =
activated_detour_notification(detour)
|> unread_notifications_for_users(Skate.Settings.User.get_all())
|> Skate.Repo.insert!()

# We need the associated values in the Detour JSON, so query the DB with the
# id to load the extra data.
select_detour_info()
|> where([notification: n], n.id == ^notification.id)
|> Skate.Repo.one!()
|> from_db_notification()
end

# Creates a new notification set to the current time
defp new_notification_now() do
%Notifications.Db.Notification{
created_at: DateTime.to_unix(DateTime.utc_now())
}
end

# Adds a activated detour notification relation to a `Notifications.Db.Notification`
defp activated_detour_notification(%Skate.Detours.Db.Detour{} = detour) do
%Notifications.Db.Notification{
new_notification_now()
| detour: Notifications.Detour.activated_detour(detour)
}
end

defp notification_for_user(%Skate.Settings.Db.User{} = user) do
%Notifications.Db.NotificationUser{
user: user
}
end

defp unread_notification(%Notifications.Db.NotificationUser{} = user_notification) do
%{
user_notification
| state: :unread
}
end

defp unread_notifications_for_users(%Notifications.Db.Notification{} = notification, users) do
%{
notification
| notification_users:
for user <- users do
user
|> notification_for_user()
|> unread_notification()
end
}
end

@spec get_or_create_from_block_waiver(map()) :: t()
def get_or_create_from_block_waiver(block_waiver_values) do
changeset =
Expand Down Expand Up @@ -149,6 +209,7 @@ defmodule Notifications.Notification do
|> select_user_read_state(user_id)
|> select_bridge_movements()
|> select_block_waivers()
|> select_detour_info()
|> where([notification: n], n.created_at > ^cutoff_time)
|> order_by([notification: n], desc: n.created_at)
|> Skate.Repo.all()
Expand Down Expand Up @@ -220,4 +281,10 @@ defmodule Notifications.Notification do
}) do
bm
end

defp content_from_db_notification(%DbNotification{
detour: %Notifications.Db.Detour{} = detour
}) do
detour
end
end
73 changes: 73 additions & 0 deletions test/notifications/notification_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,77 @@ defmodule Notifications.NotificationTest do
|> Enum.sort_by(& &1.id)
end
end

describe "create_activated_detour_notification_from_detour/1" do
test "inserts new record into the database" do
count = 3

# create new notification
for _ <- 1..count do
:detour
|> insert()
|> Notifications.Notification.create_activated_detour_notification_from_detour()
end

# assert it is in the database
assert count == Skate.Repo.aggregate(Notifications.Db.Detour, :count)
end

test "creates a unread notification for all users" do
number_of_users = 5
[user | _] = insert_list(number_of_users, :user)

# create new notification
detour =
:detour
|> insert(
# don't create a new user and affect the user count
author: user
)
|> Notifications.Notification.create_activated_detour_notification_from_detour()

detour =
Notifications.Db.Notification
|> Skate.Repo.get!(detour.id)
|> Skate.Repo.preload(:users)

# assert all users have a notification that is unread
assert Kernel.length(detour.users) == number_of_users
end

test "returns detour information" do
# create new notification
%{
state: %{
"context" => %{
"route" => %{
"name" => route_name
},
"routePattern" => %{
"name" => route_pattern_name,
"headsign" => headsign
}
}
}
} =
detour =
:detour
|> build()
|> with_direction(:inbound)
|> insert()

detour_notification =
Notifications.Notification.create_activated_detour_notification_from_detour(detour)

# assert fields are set
assert %Notifications.Notification{
content: %Notifications.Db.Detour{
route: ^route_name,
origin: ^route_pattern_name,
headsign: ^headsign,
direction: "Inbound"
}
} = detour_notification
end
end
end
27 changes: 25 additions & 2 deletions test/support/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,31 @@ defmodule Skate.Factory do

def detour_factory do
%Skate.Detours.Db.Detour{
state: %{},
author: build(:user)
author: build(:user),
state: %{
"context" => %{
"route" => %{
"name" => sequence("detour_route_name:"),
"directionNames" => %{
"0" => "Outbound",
"1" => "Inbound"
}
},
"routePattern" => %{
"name" => sequence("detour_route_pattern_name:"),
"headsign" => sequence("detour_route_pattern_headsign:"),
"directionId" => sequence(:detour_route_pattern_direction, [0, 1])
}
}
}
}
end

def with_direction(%Skate.Detours.Db.Detour{} = detour, :inbound) do
put_in(detour.state["context"]["routePattern"]["directionId"], 1)
end

def with_direction(%Skate.Detours.Db.Detour{} = detour, :outbound) do
put_in(detour.state["context"]["routePattern"]["directionId"], 0)
end
end

0 comments on commit c231cfb

Please sign in to comment.