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

Ecto changeset associations #98

Merged
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
36 changes: 21 additions & 15 deletions lib/app/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ defmodule App.Item do
alias __MODULE__

schema "items" do
field :person_id, :id
field :status_code, :integer
field :text, :string

belongs_to :status, App.Status, on_replace: :nilify
Copy link
Member

Choose a reason for hiding this comment

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

From a beginner's perspective, it doesn't make any sense that an item would "belongs_to" a Status.
An item can have a status but it doesn't "belong" to the status.
This is immediately confusing and adds no value to the MVP.

belongs_to :person, App.Person
timestamps()
end

@doc false
def changeset(item, attrs) do
item
|> cast(attrs, [:text, :person_id, :status_code])
|> validate_required([:text, :person_id])
|> cast(attrs, [:text])
|> validate_required([:text])
end

@doc """
Expand All @@ -32,9 +32,11 @@ defmodule App.Item do
{:error, %Ecto.Changeset{}}

"""
def create_item(attrs \\ %{}) do
def create_item(attrs \\ %{}, person, status) do
%Item{}
|> changeset(attrs)
|> put_assoc(:person, person)
|> put_assoc(:status, status)
|> Repo.insert()
end

Expand All @@ -52,10 +54,10 @@ defmodule App.Item do
** (Ecto.NoResultsError)

"""
def get_item!(id), do: Repo.get!(Item, id)
def get_item!(id), do: Repo.get!(Item, id) |> Repo.preload(:status)

@doc """
Returns the list of items.
Returns the list of items where the status is different to "deleted"

## Examples

Expand All @@ -64,10 +66,13 @@ defmodule App.Item do

"""
def list_items do
Item
|> order_by(desc: :inserted_at)
|> where([a], is_nil(a.status_code) or a.status_code != 6)
|> Repo.all()
query =
from i in Item,
join: s in assoc(i, :status),
where: s.text != :deleted,
preload: [status: s]

Repo.all(query)
end

@doc """
Expand All @@ -88,10 +93,11 @@ defmodule App.Item do
|> Repo.update()
end

# "soft" delete
def delete_item(id) do
get_item!(id)
|> Item.changeset(%{status_code: 6})
def update_status(%Item{} = item, status) do
item
|> Repo.preload(:status)
|> Ecto.Changeset.change()
|> put_assoc(:status, status)
|> Repo.update()
end
end
19 changes: 15 additions & 4 deletions lib/app/person.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,32 @@ defmodule App.Person do
field :key_id, :integer
field :locale, :string
field :picture, Fields.Encrypted
field :status_code, :integer

belongs_to :status, App.Status

timestamps()
end

@doc false
def changeset(person, attrs) do
person
|> cast(attrs, [:givenName, :auth_provider, :key_id, :picture, :locale, :status_code])
|> cast(attrs, [
:givenName,
:auth_provider,
:key_id,
:picture,
:locale
])
|> validate_required([:givenName, :auth_provider])
end
def create(attrs) do

def create(attrs, status) do
%Person{}
|> Repo.preload(:status)
|> changeset(attrs)
|> put_assoc(:status, status)
|> Repo.insert!()
end

def get_person!(id), do: Repo.get!(__MODULE__, id)
end
10 changes: 8 additions & 2 deletions lib/app/status.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ defmodule App.Status do

schema "status" do
field :text, App.EctoAtom
field :status_code, :integer
field :code, :integer

has_many :items, App.Item
has_many :person, App.Person

timestamps()
end

@doc false
def changeset(status, attrs) do
status
|> cast(attrs, [:text, :status_code])
|> cast(attrs, [:text, :code])
|> validate_required([:text])
|> unique_constraint(:text)
end

def create(attrs) do
Expand All @@ -24,6 +28,8 @@ defmodule App.Status do
|> Repo.insert()
end

def get_by_text!(text), do: Repo.get_by!(__MODULE__, text: text)

def upsert(attrs) do
case Repo.get_by(__MODULE__, text: attrs.text) do
# create status
Expand Down
66 changes: 49 additions & 17 deletions lib/app_web/live/app_live.ex
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
defmodule AppWeb.AppLive do
use AppWeb, :live_view
alias App.{Item, Timer}
alias App.{Item, Person, Status, Timer}

@topic "live"

@impl true
def mount(_params, _session, socket) do
# subscribe to the channel
if connected?(socket), do: AppWeb.Endpoint.subscribe(@topic)
{:ok, assign(socket, items: Item.list_items(), editing: nil, timer: %Timer{})}

{:ok,
assign(socket, items: Item.list_items(), editing: nil, timer: %Timer{})}
end

@impl true
def handle_event("create", %{"text" => text}, socket) do
Item.create_item(%{text: text, person_id: 1, status_code: 2})
socket = assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})
person = Person.get_person!(1)
status = Status.get_by_text!(:uncategorized)
Item.create_item(%{text: text}, person, status)

socket =
assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})

AppWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
{:noreply, socket}
end

@impl true
def handle_event("toggle", data, socket) do
# Toggle the status of the item between 3 (:active) and 4 (:done)
status = if Map.has_key?(data, "value"), do: 4, else: 3
status =
if Map.has_key?(data, "value") do
Status.get_by_text!(:done)
else
Status.get_by_text!(:active)
end

item = Item.get_item!(Map.get(data, "id"))
Item.update_item(item, %{id: item.id, status_code: status})
socket = assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})
Item.update_status(item, status)

socket =
assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})

AppWeb.Endpoint.broadcast_from(self(), @topic, "update", socket.assigns)
{:noreply, socket}
end

@impl true
def handle_event("delete", data, socket) do
Item.delete_item(Map.get(data, "id"))
socket = assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})
def handle_event("delete", %{"id" => item_id}, socket) do
deleted_status = Status.get_by_text!(:deleted)
item = Item.get_item!(item_id)
Item.update_status(item, deleted_status)

socket =
assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})

AppWeb.Endpoint.broadcast_from(self(), @topic, "delete", socket.assigns)
{:noreply, socket}
end
Expand All @@ -44,10 +65,18 @@ defmodule AppWeb.AppLive do
# status = if Map.has_key?(data, "value"), do: 4, else: 3
item = Item.get_item!(Map.get(data, "id"))
# Item.update_item(item, %{id: item.id, status_code: status})
{:ok, timer} = Timer.start(%{item_id: item.id, person_id: 1, start: NaiveDateTime.utc_now})
{:ok, timer} =
Timer.start(%{
item_id: item.id,
person_id: 1,
start: NaiveDateTime.utc_now()
})

# IO.inspect(timer)

socket = assign(socket, items: Item.list_items(), active: %Item{}, timer: timer)
socket =
assign(socket, items: Item.list_items(), active: %Item{}, timer: timer)

AppWeb.Endpoint.broadcast_from(self(), @topic, "start|stop", socket.assigns)
{:noreply, socket}
end
Expand All @@ -64,20 +93,23 @@ defmodule AppWeb.AppLive do
{:ok, _timer} = Timer.stop(%{id: timer_id})
# IO.inspect(timer)

socket = assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})
socket =
assign(socket, items: Item.list_items(), active: %Item{}, timer: %Timer{})

AppWeb.Endpoint.broadcast_from(self(), @topic, "start|stop", socket.assigns)
{:noreply, socket}
end

@impl true
def handle_info(%{event: "start|stop", payload: %{items: items, timer: timer}}, socket) do
def handle_info(
%{event: "start|stop", payload: %{items: items, timer: timer}},
socket
) do
{:noreply, assign(socket, items: items, timer: timer)}
end

# helper function that checks for status 4 (:done)
def done?(item) do
not is_nil(item.status_code) and item.status_code == 4
end
def done?(item), do: item.status.text == :done

def started?(item, timer) do
# IO.inspect(item, label: "item")
Expand Down
4 changes: 2 additions & 2 deletions lib/app_web/live/app_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</label>
<button class="bg-orange-600 text-white px-2 py-2 mr-2 mt-2 h-10"
phx-click="delete" phx-value-id={item.id}>
Delete
Delete!
</button>
<% else %>
<input type="checkbox" phx-value-id={item.id} phx-click="toggle"
Expand Down Expand Up @@ -108,4 +108,4 @@

return h + ':' + m + ':' + s;
}
</script>
</script>
6 changes: 4 additions & 2 deletions priv/repo/migrations/20220627162141_create_status.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ defmodule App.Repo.Migrations.CreateStatus do

def change do
create table(:status) do
add :text, :string
add :status_code, :integer
add(:text, :string)
add(:code, :integer)

timestamps()
end

create(unique_index(:status, [:text]))
end
end
14 changes: 6 additions & 8 deletions priv/repo/migrations/20220627162148_create_people.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ defmodule App.Repo.Migrations.CreatePeople do

def change do
create table(:people) do
add :givenName, :binary
add :auth_provider, :string
add :key_id, :integer
add :picture, :binary
add :locale, :string
add :status_code, :integer
add(:givenName, :binary)
add(:auth_provider, :string)
add(:key_id, :integer)
add(:picture, :binary)
add(:locale, :string)
add(:status_id, references(:status, on_delete: :nothing))

timestamps()
end

create index(:people, [:status_code])
end
end
9 changes: 4 additions & 5 deletions priv/repo/migrations/20220627162154_create_items.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ defmodule App.Repo.Migrations.CreateItems do

def change do
create table(:items) do
add :text, :string
add :person_id, references(:people, on_delete: :nothing)
add :status_code, :integer
add(:text, :string)
add(:person_id, references(:people, on_delete: :nothing))
add(:status_id, references(:status, on_delete: :nothing))

timestamps()
end

create index(:items, [:person_id])
create index(:items, [:status_code])
create(index(:items, [:person_id]))
end
end
21 changes: 11 additions & 10 deletions priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ alias App.{Person, Status}

# Statuses: github.com/dwyl/statuses
Statuses.get_statuses()
|> Enum.each(fn s ->
Status.upsert(%{
text: s.text,
status_code: s.code
})
|> Enum.each(fn status ->
Status.upsert(Map.from_struct(status))
end)

Person.create(%{
givenName: "Beyoncé",
auth_provider: "Google",
status_code: 1
})
verified_status = Status.get_by_text!(:verified)

Person.create(
%{
givenName: "Beyoncé",
auth_provider: "Google"
},
verified_status
)
Loading