Skip to content

Commit

Permalink
feat(ex/detours): add detour :status field, and populate from chang…
Browse files Browse the repository at this point in the history
…eset (#2952)

* feat(ex/detours): add detour :status field, and populate from changeset

This adds a new database column and starts populating it from the changeset function.
Once we deploy this and have all instances become aware of the field. We'll then
deploy a data migration which adds this to previously made detours, so that there
are no running instances without this change to the changeset function

* chore(ex/db/detour): add type to field

* refactor(ex/detours): move `detour_type` into `Skate.Detours.Db.Detour`

* refactor(ex/detours): replace `detour_type` with `Detour.status`

* test(ex/detours): add tests for `:status` column

* feat(ex/db/detour): set `:status` in changeset when `:state` changes`
  • Loading branch information
firestack authored Feb 6, 2025
1 parent 6c09b21 commit 4c180c1
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 13 deletions.
20 changes: 19 additions & 1 deletion lib/skate/detours/db/detour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ defmodule Skate.Detours.Db.Detour do

alias Skate.Settings.Db.User

@type status :: :active | :draft | :past

typed_schema "detours" do
field :state, :map

field(:status, Ecto.Enum, values: [:draft, :active, :past]) :: status()

belongs_to :author, User

# When this detour was activated
Expand All @@ -21,7 +26,20 @@ defmodule Skate.Detours.Db.Detour do
def changeset(detour, attrs) do
detour
|> cast(attrs, [:state, :activated_at])
|> validate_required([:state])
|> add_status()
|> validate_required([:state, :status])
|> foreign_key_constraint(:author_id)
end

defp add_status(changeset) do
case fetch_change(changeset, :state) do
{:ok, state} ->
# Once this column is added for all detours, `categorize_detour` logic
# should be moved here and should not be needed anymore
put_change(changeset, :status, Skate.Detours.Detours.categorize_detour(%{state: state}))

_ ->
changeset
end
end
end
16 changes: 7 additions & 9 deletions lib/skate/detours/detours.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ defmodule Skate.Detours.Detours do
end

@spec db_detour_to_detour(Detour.t()) :: DetailedDetour.t() | nil
@spec db_detour_to_detour(status :: detour_type(), Detour.t()) :: DetailedDetour.t() | nil
@spec db_detour_to_detour(status :: Detour.status(), Detour.t()) :: DetailedDetour.t() | nil
def db_detour_to_detour(%{} = db_detour) do
db_detour_to_detour(categorize_detour(db_detour), db_detour)
end
Expand Down Expand Up @@ -121,16 +121,14 @@ defmodule Skate.Detours.Detours do
nil
end

@type detour_type :: :active | :draft | :past

@doc """
Takes a `Skate.Detours.Db.Detour` struct and a `Skate.Settings.Db.User` id
and returns a `t:detour_type/0` based on the state of the detour.
and returns a `t:Detour.status/0` based on the state of the detour.
otherwise returns `nil` if it is a draft but does not belong to the provided
user
"""
@spec categorize_detour(detour :: map()) :: detour_type()
@spec categorize_detour(detour :: map()) :: Detour.status()
def categorize_detour(%{state: %{"value" => %{"Detour Drawing" => %{"Active" => _}}}}),
do: :active

Expand Down Expand Up @@ -284,7 +282,7 @@ defmodule Skate.Detours.Detours do
)
end

@spec broadcast_detour(detour_type(), Detour.t(), DbUser.id()) :: :ok
@spec broadcast_detour(Detour.status(), Detour.t(), DbUser.id()) :: :ok
defp broadcast_detour(:draft, detour, author_id) do
author_uuid =
author_id
Expand Down Expand Up @@ -351,7 +349,7 @@ defmodule Skate.Detours.Detours do
Retrieves a `Skate.Detours.Db.Detour` from the database by it's ID and then resolves the
detour's category via `categorize_detour/2`
"""
@spec categorize_detour_by_id(detour_id :: nil | integer()) :: detour_type() | nil
@spec categorize_detour_by_id(detour_id :: nil | integer()) :: Detour.status() | nil
def categorize_detour_by_id(nil = _detour_id), do: nil

def categorize_detour_by_id(detour_id) do
Expand All @@ -367,8 +365,8 @@ defmodule Skate.Detours.Detours do
) :: :ok | nil
@spec send_notification(%{
next_detour: Skate.Detours.Db.Detour.t() | nil,
next: detour_type() | nil,
previous: detour_type() | nil
next: Detour.status() | nil,
previous: Detour.status() | nil
}) :: :ok | nil
defp send_notification(
%Detour{} = new_record,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Skate.Repo.Migrations.AddDetourStatusField do
use Ecto.Migration

def change do
alter table(:detours) do
add :status, :string
end
end
end
10 changes: 10 additions & 0 deletions priv/repo/migrations/20250204034128_create_detour_status_index.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Skate.Repo.Migrations.CreateDetourStatusIndex do
use Ecto.Migration

@disable_ddl_transaction true
@disable_migration_lock true

def change do
create index(:detours, [:status], concurrently: true)
end
end
24 changes: 24 additions & 0 deletions test/skate/detours/db_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,29 @@ defmodule Skate.Detours.DbTest do
detour = detour_fixture()
assert %Ecto.Changeset{} = Detours.change_detour(detour)
end

test "change_detour/1 changes :status when :state updates" do
detour = build(:detour, status: nil)

assert nil ==
detour
|> Detours.change_detour(%{})
|> Ecto.Changeset.get_change(:status)

assert :draft ==
detour
|> Detours.change_detour(%{state: with_id(detour.state, 100)})
|> Ecto.Changeset.get_change(:status)

assert :active ==
detour
|> Detours.change_detour(%{state: activated(detour.state)})
|> Ecto.Changeset.get_change(:status)

assert :past ==
detour
|> Detours.change_detour(%{state: deactivated(detour.state)})
|> Ecto.Changeset.get_change(:status)
end
end
end
25 changes: 25 additions & 0 deletions test/skate_web/controllers/detours_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,31 @@ defmodule SkateWeb.DetoursControllerTest do
} = Detours.get_detour!(8)
end

@tag :authenticated
test "updates :status to match snapshot", %{conn: conn} do
setup_notification_server()

draft_id = 1
activated_id = 2
past_id = 3

conn
|> put(~p"/api/detours/update_snapshot", %{
"snapshot" => :detour_snapshot |> build() |> with_id(draft_id)
})
|> put(~p"/api/detours/update_snapshot", %{
"snapshot" => :detour_snapshot |> build() |> activated |> with_id(activated_id)
})
|> put(~p"/api/detours/update_snapshot", %{
"snapshot" => :detour_snapshot |> build() |> deactivated |> with_id(past_id)
})

Process.sleep(10)
assert Skate.Detours.Detours.get_detour!(draft_id).status === :draft
assert Skate.Detours.Detours.get_detour!(activated_id).status === :active
assert Skate.Detours.Detours.get_detour!(past_id).status === :past
end

@tag :authenticated
test "creates a new notification when detour is activated", %{conn: conn} do
setup_notification_server()
Expand Down
8 changes: 5 additions & 3 deletions test/support/factories/detour_factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ defmodule Skate.DetourFactory do
def detour_factory do
%Skate.Detours.Db.Detour{
author: build(:user),
state: build(:detour_snapshot)
state: build(:detour_snapshot),
status: :draft
}
end

Expand Down Expand Up @@ -80,7 +81,8 @@ defmodule Skate.DetourFactory do
%{
detour
| state: activated(detour.state, activated_at, estimated_duration),
activated_at: activated_at
activated_at: activated_at,
status: :active
}
end

Expand All @@ -103,7 +105,7 @@ defmodule Skate.DetourFactory do
end

def deactivated(%Skate.Detours.Db.Detour{} = detour) do
%{detour | state: deactivated(detour.state)}
%{detour | state: deactivated(detour.state), status: :past}
end

def deactivated(%{"value" => %{}} = state) do
Expand Down

0 comments on commit 4c180c1

Please sign in to comment.