Skip to content
Merged
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: 2 additions & 0 deletions lib/cklist/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 34 additions & 2 deletions lib/cklist/checklists.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
22 changes: 22 additions & 0 deletions lib/cklist/checklists/activity.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Cklist.Checklists.Activity do
use Ecto.Schema
import Ecto.Changeset

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

# 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
4 changes: 3 additions & 1 deletion lib/cklist/checklists/checklist.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ 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
has_many :runs, Cklist.Checklists.Run

timestamps(type: :utc_datetime)
end

Expand Down
16 changes: 16 additions & 0 deletions lib/cklist/checklists/run.ex
Original file line number Diff line number Diff line change
@@ -0,0 +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
52 changes: 37 additions & 15 deletions lib/cklist_web/live/cklist_run_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ defmodule CklistWeb.CklistRunLive do
end

def mount(%{"id" => id}, _session, socket) do
user = socket.assigns.current_user

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)
Expand All @@ -21,21 +26,9 @@ 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
Checklists.log_run_abort(assigns.run, assigns.current_user)
{
:noreply,
socket
Expand All @@ -44,6 +37,7 @@ 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 ->
Expand All @@ -54,19 +48,47 @@ defmodule CklistWeb.CklistRunLive do
|> redirect(to: ~p"/checklists/#{assigns.checklist}")
}
false ->
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
|> 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"]
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)

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, completed)
}
end

defp is_done({_, true}, count), do: count + 1
defp is_done(_, count), do: count
end
9 changes: 9 additions & 0 deletions priv/repo/migrations/20240310152508_create_checklist_run.exs
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions priv/repo/migrations/20240310153254_create_activity_log.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule Cklist.Repo.Migrations.CreateActivityLog do
use Ecto.Migration

def change do
create table(:activity) do
add :event, :map

add :user_id, references(:users)
add :run_id, references(:runs)

timestamps([type: :utc_datetime, updated_at: false])
end
end
end