From 187d60e84d2765d919d0b1a498b4b103e1c8210e Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 10 Mar 2024 15:09:43 +0100 Subject: [PATCH 1/4] WIP: notes where to log to database --- lib/cklist_web/live/cklist_run_live.ex | 42 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex index 02c528a..e079a24 100644 --- a/lib/cklist_web/live/cklist_run_live.ex +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -9,6 +9,9 @@ defmodule CklistWeb.CklistRunLive do def mount(%{"id" => id}, _session, socket) do checklist = Checklists.get_checklist!(id) + + # Log: cklist started + socket = socket |> assign(:checklist, checklist) |> assign(:steps, length(checklist.document["steps"])) @@ -21,21 +24,10 @@ defmodule CklistWeb.CklistRunLive do {:ok, socket} end - def handle_event("step_done", params, %{assigns: assigns} = socket) do - [step_name] = params["_target"] - updated_state = Map.put(assigns.step_state, step_name, Map.get(params, step_name) == "true") - updated_steps_done = Enum.reduce(updated_state, 0, &is_done/2) - - { - :noreply, - socket - |> assign(:step_state, updated_state) - |> assign(:steps_done, updated_steps_done) - |> assign(:completed, updated_steps_done === assigns.steps) - } - end - + # Handles aborting a checklist. def handle_event("abort", _params, %{assigns: assigns} = socket) do + # Log: cklist aborted + { :noreply, socket @@ -44,9 +36,11 @@ defmodule CklistWeb.CklistRunLive do } end + # Handles "next_step" event for sequential checklists. def handle_event("next_step", _params, %{assigns: assigns} = socket) do case assigns.completed do true -> + # Log: cklist completed { :noreply, socket @@ -54,19 +48,37 @@ defmodule CklistWeb.CklistRunLive do |> redirect(to: ~p"/checklists/#{assigns.checklist}") } false -> + # Log: cklist step completed updated_steps_done = assigns.steps_done + 1 completed = assigns.steps === updated_steps_done { :noreply, socket - |> assign(:steps_done, updated_steps_done) |> assign(:current_step, Enum.at(assigns.checklist.document["steps"], updated_steps_done)) |> assign(:next_step, Enum.at(assigns.checklist.document["steps"], updated_steps_done + 1)) + |> assign(:steps_done, updated_steps_done) |> assign(:completed, completed) } end end + # Handles "step_done" events for non-sequential checklists. + def handle_event("step_done", params, %{assigns: assigns} = socket) do + [step_name] = params["_target"] + updated_state = Map.put(assigns.step_state, step_name, Map.get(params, step_name) == "true") + updated_steps_done = Enum.reduce(updated_state, 0, &is_done/2) + + # Log cklist step completed + + { + :noreply, + socket + |> assign(:step_state, updated_state) + |> assign(:steps_done, updated_steps_done) + |> assign(:completed, updated_steps_done === assigns.steps) + } + end + defp is_done({_, true}, count), do: count + 1 defp is_done(_, count), do: count end From 8c4f0a6cea5f82eb497e98f51eb1255be8d0725f Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 10 Mar 2024 15:21:17 +0100 Subject: [PATCH 2/4] user has many cklists and cklists belong to a user --- lib/cklist/accounts/user.ex | 2 ++ lib/cklist/checklists/checklist.ex | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/cklist/accounts/user.ex b/lib/cklist/accounts/user.ex index 9447257..0c42bb7 100644 --- a/lib/cklist/accounts/user.ex +++ b/lib/cklist/accounts/user.ex @@ -8,6 +8,8 @@ defmodule Cklist.Accounts.User do field :hashed_password, :string, redact: true field :confirmed_at, :naive_datetime + has_many :checklists, Cklist.Checklists.Checklist + timestamps(type: :utc_datetime) end diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index 7f86ecc..174ccd3 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -6,9 +6,10 @@ defmodule Cklist.Checklists.Checklist do field :description, :string field :title, :string field :document, :map - field :user_id, :id field :access, Ecto.Enum, values: [:public, :personal] + belongs_to :user, Cklist.Accounts.User + timestamps(type: :utc_datetime) end From 8ad2cd996858a482de61b9f8b5a71753c7528c02 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 10 Mar 2024 16:48:28 +0100 Subject: [PATCH 3/4] Add tables for checklist runs and activities --- lib/cklist/checklists/activity.ex | 14 ++++++++++++++ lib/cklist/checklists/run.ex | 8 ++++++++ .../20240310152508_create_checklist_run.exs | 9 +++++++++ .../20240310153254_create_activity_log.exs | 13 +++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 lib/cklist/checklists/activity.ex create mode 100644 lib/cklist/checklists/run.ex create mode 100644 priv/repo/migrations/20240310152508_create_checklist_run.exs create mode 100644 priv/repo/migrations/20240310153254_create_activity_log.exs diff --git a/lib/cklist/checklists/activity.ex b/lib/cklist/checklists/activity.ex new file mode 100644 index 0000000..21419b0 --- /dev/null +++ b/lib/cklist/checklists/activity.ex @@ -0,0 +1,14 @@ +defmodule Cklist.Checklists.Activity do + use Ecto.Schema + + schema "activities" do + field :event, :string + field :payload, :map + + belongs_to :run, Cklist.Checklists.Run + belongs_to :user, Cklist.Accounts.User + + # We don't expect activities to be modified. + timestamps([type: :utc_datetime, updated_at: false]) + end +end diff --git a/lib/cklist/checklists/run.ex b/lib/cklist/checklists/run.ex new file mode 100644 index 0000000..c0088b0 --- /dev/null +++ b/lib/cklist/checklists/run.ex @@ -0,0 +1,8 @@ +defmodule Cklist.Checklists.Run do + use Ecto.Schema + + schema "runs" do + belongs_to :checklist, Cklist.Checklists.Checklist + has_many :activities, Cklist.Checklists.Activity + end +end diff --git a/priv/repo/migrations/20240310152508_create_checklist_run.exs b/priv/repo/migrations/20240310152508_create_checklist_run.exs new file mode 100644 index 0000000..55b40cb --- /dev/null +++ b/priv/repo/migrations/20240310152508_create_checklist_run.exs @@ -0,0 +1,9 @@ +defmodule Cklist.Repo.Migrations.CreateChecklistRun do + use Ecto.Migration + + def change do + create table(:runs) do + add :checklist_id, references(:checklists) + end + end +end diff --git a/priv/repo/migrations/20240310153254_create_activity_log.exs b/priv/repo/migrations/20240310153254_create_activity_log.exs new file mode 100644 index 0000000..de28f08 --- /dev/null +++ b/priv/repo/migrations/20240310153254_create_activity_log.exs @@ -0,0 +1,13 @@ +defmodule Cklist.Repo.Migrations.CreateActivityLog do + use Ecto.Migration + + def change do + create table(:activity) do + add :user_id, references(:users) + add :run_id, references(:runs) + + add :event, :string + add :payload, :map + end + end +end From 4d9ca3bef155d29e6e8df1fe90d6f80b070be412 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 10 Mar 2024 23:21:29 +0100 Subject: [PATCH 4/4] Working checklist runs & activity logs --- lib/cklist/checklists.ex | 36 +++++++++++++++++-- lib/cklist/checklists/activity.ex | 14 ++++++-- lib/cklist/checklists/checklist.ex | 1 + lib/cklist/checklists/run.ex | 8 +++++ lib/cklist_web/live/cklist_run_live.ex | 28 ++++++++++----- .../20240310153254_create_activity_log.exs | 5 +-- 6 files changed, 76 insertions(+), 16 deletions(-) diff --git a/lib/cklist/checklists.ex b/lib/cklist/checklists.ex index 12e315f..ca8207b 100644 --- a/lib/cklist/checklists.ex +++ b/lib/cklist/checklists.ex @@ -4,9 +4,10 @@ defmodule Cklist.Checklists do """ import Ecto.Query, warn: false - alias Cklist.Repo - + alias Cklist.Checklists.Run + alias Cklist.Checklists.Activity alias Cklist.Checklists.Checklist + alias Cklist.Repo @doc """ Returns the list of checklists. @@ -107,4 +108,35 @@ defmodule Cklist.Checklists do def change_checklist(%Checklist{} = checklist, attrs \\ %{}) do Checklist.changeset(checklist, attrs) end + + def log_run_start(checklist, user) do + {:ok, run} = %Run{} + |> Run.changeset(%{checklist_id: checklist.id}) + |> Repo.insert() + + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "checklist_start"}}) + |> Repo.insert() + + # return current run + run + end + + def log_run_abort(run, user) do + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "checklist_abort"}}) + |> Repo.insert() + end + + def log_run_complete(run, user) do + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "checklist_complete"}}) + |> Repo.insert() + end + + def log_step_complete(run, user, step_id, is_done \\ true) do + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "step_done", step_id: step_id, done: is_done}}) + |> Repo.insert() + end end diff --git a/lib/cklist/checklists/activity.ex b/lib/cklist/checklists/activity.ex index 21419b0..2923100 100644 --- a/lib/cklist/checklists/activity.ex +++ b/lib/cklist/checklists/activity.ex @@ -1,9 +1,10 @@ defmodule Cklist.Checklists.Activity do use Ecto.Schema + import Ecto.Changeset - schema "activities" do - field :event, :string - field :payload, :map + schema "activity" do + # We assume events have a type. Any other properties are optional and type-dependent. + field :event, :map belongs_to :run, Cklist.Checklists.Run belongs_to :user, Cklist.Accounts.User @@ -11,4 +12,11 @@ defmodule Cklist.Checklists.Activity do # We don't expect activities to be modified. timestamps([type: :utc_datetime, updated_at: false]) end + + @doc false + def changeset(activity, attrs) do + activity + |> cast(attrs, [:event, :run_id, :user_id]) + |> validate_required([:event, :run_id, :user_id]) + end end diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index 174ccd3..3b618d3 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -9,6 +9,7 @@ defmodule Cklist.Checklists.Checklist do field :access, Ecto.Enum, values: [:public, :personal] belongs_to :user, Cklist.Accounts.User + has_many :runs, Cklist.Checklists.Run timestamps(type: :utc_datetime) end diff --git a/lib/cklist/checklists/run.ex b/lib/cklist/checklists/run.ex index c0088b0..7b22ada 100644 --- a/lib/cklist/checklists/run.ex +++ b/lib/cklist/checklists/run.ex @@ -1,8 +1,16 @@ defmodule Cklist.Checklists.Run do use Ecto.Schema + import Ecto.Changeset schema "runs" do belongs_to :checklist, Cklist.Checklists.Checklist has_many :activities, Cklist.Checklists.Activity end + + @doc false + def changeset(run, attrs) do + run + |> cast(attrs, [:checklist_id]) + |> validate_required([:checklist_id]) + end end diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex index e079a24..2d10f18 100644 --- a/lib/cklist_web/live/cklist_run_live.ex +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -8,12 +8,14 @@ defmodule CklistWeb.CklistRunLive do end def mount(%{"id" => id}, _session, socket) do - checklist = Checklists.get_checklist!(id) + user = socket.assigns.current_user - # Log: cklist started + checklist = Checklists.get_checklist!(id) + run = Checklists.log_run_start(checklist, user) socket = socket |> assign(:checklist, checklist) + |> assign(:run, run) |> assign(:steps, length(checklist.document["steps"])) |> assign(:steps_done, 0) |> assign(:completed, false) @@ -26,8 +28,7 @@ defmodule CklistWeb.CklistRunLive do # Handles aborting a checklist. def handle_event("abort", _params, %{assigns: assigns} = socket) do - # Log: cklist aborted - + Checklists.log_run_abort(assigns.run, assigns.current_user) { :noreply, socket @@ -40,7 +41,6 @@ defmodule CklistWeb.CklistRunLive do def handle_event("next_step", _params, %{assigns: assigns} = socket) do case assigns.completed do true -> - # Log: cklist completed { :noreply, socket @@ -48,9 +48,12 @@ defmodule CklistWeb.CklistRunLive do |> redirect(to: ~p"/checklists/#{assigns.checklist}") } false -> - # Log: cklist step completed + Checklists.log_step_complete(assigns.run, assigns.current_user, assigns.steps_done) updated_steps_done = assigns.steps_done + 1 completed = assigns.steps === updated_steps_done + if completed do + Checklists.log_run_complete(assigns.run, assigns.current_user) + end { :noreply, socket @@ -65,17 +68,24 @@ defmodule CklistWeb.CklistRunLive do # Handles "step_done" events for non-sequential checklists. def handle_event("step_done", params, %{assigns: assigns} = socket) do [step_name] = params["_target"] - updated_state = Map.put(assigns.step_state, step_name, Map.get(params, step_name) == "true") + is_done = Map.get(params, step_name) == "true" + updated_state = Map.put(assigns.step_state, step_name, is_done) updated_steps_done = Enum.reduce(updated_state, 0, &is_done/2) - # Log cklist step completed + step_id = Enum.find_index(assigns.checklist.document["steps"], &(&1["name"] == step_name)) + Checklists.log_step_complete(assigns.run, assigns.current_user, step_id, is_done) + + completed = updated_steps_done === assigns.steps + if completed do + Checklists.log_run_complete(assigns.run, assigns.current_user) + end { :noreply, socket |> assign(:step_state, updated_state) |> assign(:steps_done, updated_steps_done) - |> assign(:completed, updated_steps_done === assigns.steps) + |> assign(:completed, completed) } end diff --git a/priv/repo/migrations/20240310153254_create_activity_log.exs b/priv/repo/migrations/20240310153254_create_activity_log.exs index de28f08..78af720 100644 --- a/priv/repo/migrations/20240310153254_create_activity_log.exs +++ b/priv/repo/migrations/20240310153254_create_activity_log.exs @@ -3,11 +3,12 @@ defmodule Cklist.Repo.Migrations.CreateActivityLog do def change do create table(:activity) do + add :event, :map + add :user_id, references(:users) add :run_id, references(:runs) - add :event, :string - add :payload, :map + timestamps([type: :utc_datetime, updated_at: false]) end end end