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

Create people migration #162

Merged
merged 9 commits into from
Oct 5, 2022
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: 1 addition & 1 deletion .iex.exs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
alias App.{Repo, Item, Tag, ItemTag}
alias App.{Repo, Item, Tag, ItemTag, Person}
4 changes: 2 additions & 2 deletions lib/app/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ defmodule App.Item do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias App.{Repo, Tag, ItemTag}
alias App.{Repo, Tag, ItemTag, Person}
alias __MODULE__

schema "items" do
field :person_id, :integer
field :status, :integer
field :text, :string

belongs_to :people, Person, references: :person_id, foreign_key: :person_id
many_to_many(:tags, Tag, join_through: ItemTag, on_replace: :delete)

timestamps()
Expand Down
39 changes: 39 additions & 0 deletions lib/app/person.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule App.Person do
use Ecto.Schema
import Ecto.Changeset
alias App.{Item, Repo, Tag}
alias __MODULE__
# https://hexdocs.pm/phoenix/Phoenix.Param.html
Copy link
Member

Choose a reason for hiding this comment

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

I find the docs for Param pretty useless. Wish there was a standalone example. 💭

@derive {Phoenix.Param, key: :person_id}

@primary_key {:person_id, :id, autogenerate: false}
schema "people" do
field :name, :string

has_many :items, Item, foreign_key: :person_id
has_many :tags, Tag, foreign_key: :person_id
timestamps()
end

@doc false
def changeset(person, attrs \\ %{}) do
person
|> cast(attrs, [:person_id, :name])
|> validate_required([:person_id, :name])
|> unique_constraint(:name, name: :people_name_index)
Copy link
Member

Choose a reason for hiding this comment

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

Can't say I understand why the :name should be unique ... the :person_id, sure, but name ...? 🤷‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm thinking of using the name later on in the UI to select people to add to groups. If the name is not unique we won't be able to distinguish between users. Maybe it should be called username.

end

def create_person(attrs) do
%Person{}
|> changeset(attrs)
|> Repo.insert()
end

def get_person!(person_id), do: Repo.get!(Person, person_id)

def update_person(%Person{} = person, attrs) do
person
|> Person.changeset(attrs)
|> Repo.update()
end
end
4 changes: 2 additions & 2 deletions lib/app/tag.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ defmodule App.Tag do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias App.{Item, ItemTag, Repo}
alias App.{Item, ItemTag, Person, Repo}
alias __MODULE__

schema "tags" do
field :text, :string
field :person_id, :integer
field :color, :string

belongs_to :people, Person, references: :person_id, foreign_key: :person_id
many_to_many(:items, Item, join_through: ItemTag)
timestamps()
end
Expand Down
5 changes: 4 additions & 1 deletion lib/app_web/controllers/init_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ defmodule AppWeb.InitController do

conn
|> assign(:loggedin, true)
|> assign(:person, %{picture: "https://dwyl.com/img/favicon-32x32.png"})
|> assign(:person, %{
picture: "https://dwyl.com/img/favicon-32x32.png",
id: 0
})
|> render(:index,
env: check_env(@env_required),
api_key_set: api_key_set?()
Expand Down
41 changes: 41 additions & 0 deletions lib/app_web/controllers/profile_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule AppWeb.ProfileController do
use AppWeb, :controller
alias App.Person
plug :permission_profile when action in [:edit, :update]

def edit(conn, %{"person_id" => person_id}) do
profile = Person.get_person!(person_id)
changeset = Person.changeset(profile)
render(conn, "edit.html", profile: profile, changeset: changeset)
end

def update(
conn,
%{"person_id" => person_id, "person" => person_params}
) do
profile = Person.get_person!(person_id)

case Person.update_person(profile, person_params) do
{:ok, _person} ->
conn
|> put_flash(:info, "Person updated successfully.")
Copy link
Member

Choose a reason for hiding this comment

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

Guessing this "Person updated successfully." is auto-generated? 🤖
Maybe we could change it to "Your profile was updated." or something a bit more friendly? 💭

Copy link
Member Author

Choose a reason for hiding this comment

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

The flash messages are not displayed at the moment.
The function to display them might have been removed previously, I need to have a look at it but we can definitely update the message

|> redirect(to: "/")

{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", profile: profile, changeset: changeset)
end
end

defp permission_profile(conn, _opts) do
person_id = conn.assigns[:person][:id] || 0

if String.to_integer(conn.params["person_id"]) == person_id do
conn
else
conn
|> put_flash(:info, "You can't access that page")
|> redirect(to: "/")
|> halt()
end
end
end
9 changes: 0 additions & 9 deletions lib/app_web/controllers/tag_controller.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
defmodule AppWeb.TagController do
use AppWeb, :controller
alias App.Tag
plug :loggedin
plug :permission_tag when action in [:edit, :update, :delete]

def index(conn, _params) do
Expand Down Expand Up @@ -41,14 +40,6 @@ defmodule AppWeb.TagController do
|> redirect(to: Routes.tag_path(conn, :index))
end

defp loggedin(conn, _opts) do
if not is_nil(conn.assigns[:jwt]) do
assign(conn, :loggedin, true)
else
assign(conn, :loggedin, false)
end
end

defp permission_tag(conn, _opts) do
tag = Tag.get_tag!(conn.params["id"])

Expand Down
37 changes: 35 additions & 2 deletions lib/app_web/router.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule AppWeb.Router do
use AppWeb, :router
alias App.Person

pipeline :browser do
plug :accepts, ["html"]
Expand All @@ -17,13 +18,45 @@ defmodule AppWeb.Router do
end

pipeline :authOptional, do: plug(AuthPlugOptional)
pipeline :verify_loggedin, do: plug(:loggedin)
pipeline :check_profile_name, do: plug(:profile_name)

scope "/", AppWeb do
pipe_through [:browser, :authOptional]
pipe_through [:browser, :authOptional, :verify_loggedin]

resources "/profile", ProfileController,
only: [:edit, :update],
param: "person_id"

pipe_through [:check_profile_name]
live "/", AppLive
resources "/tags", TagController
resources "/tags", TagController, except: [:show]
get "/login", AuthController, :login
get "/logout", AuthController, :logout
end

# assign to conn the loggedin value used in templates
defp loggedin(conn, _opts) do
if not is_nil(conn.assigns[:jwt]) do
assign(conn, :loggedin, true)
else
assign(conn, :loggedin, false)
end
end

# Redirect to edit profile to force user to
# add name to their profile for sharing items feature
defp profile_name(conn, _opts) do
person_id = conn.assigns[:person][:id] || 0
person = Person.get_person!(person_id)

if is_nil(person.name) do
conn
|> put_flash(:info, "Add a name to your profile to allow sharing items")
|> redirect(to: "/profile/#{person.person_id}/edit")
|> halt()
else
conn
end
end
end
2 changes: 2 additions & 0 deletions lib/app_web/templates/layout/root.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
<div class="container flex flex-wrap justify-between items-center mx-auto">

<%= if @loggedin do %>
<a href={Routes.profile_path(@conn, :edit, @person.id)}>
Copy link
Member Author

Choose a reason for hiding this comment

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

Add link to the profile picture in the nav bar to the profile edit page

<img src={@person.picture} class="mr-3 h-6 sm:h-9 rounded-full" alt="avatar image">
</a>
<% else %>
<h1 class="text-white">Hi Friend!</h1>
<% end %>
Expand Down
19 changes: 19 additions & 0 deletions lib/app_web/templates/profile/edit.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<.container>
<.h2 class="text-center mt-3">Edit Profile</.h2>
<.p>Please make sure a name is defined as it is used to share items</.p>
<.form :let={f} for={@changeset} action={Routes.profile_path(@conn, :update, @profile)} method="patch" class="py-3">
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<.form_field
type="text_input"
form={f}
field={:name}
/>

<.button type="submit" label="Save" />
</.form>

</.container>
3 changes: 3 additions & 0 deletions lib/app_web/views/profile_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule AppWeb.ProfileView do
use AppWeb, :view
end
30 changes: 30 additions & 0 deletions priv/repo/migrations/20221004195054_add_person.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule App.Repo.Migrations.AddPerson do
use Ecto.Migration

def change do
execute("CREATE EXTENSION IF NOT EXISTS citext")

create table(:people, primary_key: false) do
add(:person_id, :integer, primary_key: true)
add(:name, :citext)

timestamps()
end

# insert new people rows by select unique person_id from items and tags
execute(
Copy link
Member Author

Choose a reason for hiding this comment

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

Create person from the existing person_id from the items and tags table

"INSERT INTO people(person_id, inserted_at, updated_at)
SELECT DISTINCT i.person_id, now(), now() FROM items as i UNION SELECT DISTINCT t.person_id, now(), now() from tags as t;"
)

create(unique_index(:people, [:name], name: :people_name_index))

alter table(:items) do
modify(:person_id, references(:people, column: :person_id))
end

alter table(:tags) do
modify(:person_id, references(:people, column: :person_id))
end
end
end
9 changes: 8 additions & 1 deletion test/app/item_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule App.ItemTest do
use App.DataCase
alias App.{Item, Timer}
alias App.{Item, Person, Timer}

setup [:create_person]

describe "items" do
@valid_attrs %{text: "some text", person_id: 1, status: 2}
Expand Down Expand Up @@ -151,4 +153,9 @@ defmodule App.ItemTest do
assert length(item_timers) > 0
end
end

defp create_person(_) do
person = Person.create_person(%{"person_id" => 1, "name" => "guest"})
%{person: person}
end
end
57 changes: 57 additions & 0 deletions test/app/person_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule App.PersonTest do
use App.DataCase
alias App.Person

describe "Test constraints and requirements for Person schema" do
test "valid person changeset" do
changeset = Person.changeset(%Person{}, %{person_id: 1, name: "person1"})

assert changeset.valid?
end

test "invalid person changeset when name value missing" do
changeset = Person.changeset(%Person{}, %{person_id: 1, name: ""})
refute changeset.valid?
end

test "invalid person changeset when person_id value missing" do
changeset = Person.changeset(%Person{}, %{name: "person1"})
refute changeset.valid?
end
end

describe "Save person in Postgres" do
@valid_attrs %{person_id: 1, name: "person 1"}
@invalid_attrs %{name: nil}

test "get_person!/1 returns the person with given id" do
{:ok, person} = Person.create_person(@valid_attrs)
assert Person.get_person!(person.person_id).name == person.name
end

test "create_person/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Person.create_person(@invalid_attrs)
end

test "create_person/1 returns invalid changeset when trying to insert a duplicate name" do
assert {:ok, _person} = Person.create_person(@valid_attrs)

assert {:error, _changeset} =
Person.create_person(%{person_id: 2, name: "person 1"})
end
end

describe "Update person in Postgres" do
@valid_attrs %{person_id: 1, name: "person 1"}
@valid_update_attrs %{person_id: 1, name: "person 1 updated"}

test "create_person/1 returns invalid changeset when trying to insert a duplicate name" do
assert {:ok, person} = Person.create_person(@valid_attrs)

assert {:ok, person_updated} =
Person.update_person(person, @valid_update_attrs)

assert person_updated.name == "person 1 updated"
end
end
end
9 changes: 8 additions & 1 deletion test/app/tag_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule App.TagTest do
use App.DataCase
alias App.Tag
alias App.{Person, Tag}

setup [:create_person]

describe "Test constraints and requirements for Tag schema" do
test "valid tag changeset" do
Expand Down Expand Up @@ -54,4 +56,9 @@ defmodule App.TagTest do
assert length(tags) == 0
end
end

defp create_person(_) do
person = Person.create_person(%{"person_id" => 1, "name" => "guest"})
%{person: person}
end
end
9 changes: 8 additions & 1 deletion test/app/timer_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule App.TimerTest do
use App.DataCase
alias App.{Item, Timer}
alias App.{Item, Person, Timer}

setup [:create_person]

describe "timers" do
@valid_item_attrs %{text: "some text", person_id: 1}
Expand Down Expand Up @@ -66,4 +68,9 @@ defmodule App.TimerTest do
assert "Keep on truckin'"
end
end

defp create_person(_) do
person = Person.create_person(%{"person_id" => 1, "name" => "guest"})
%{person: person}
end
end
Loading