Compare commits

...

5 Commits

Author SHA1 Message Date
Ryan Rix 123dc8f9d3 set up unit tests for :move and :do_move 2020-09-16 12:25:56 -07:00
Ryan Rix 21c4530553 add ThingController :do_move action 2020-09-16 11:41:27 -07:00
Ryan Rix 16ee55421a aesthetic changes 2020-09-16 11:37:29 -07:00
Ryan Rix 82ebdea86e ThingController.move will show a filter-form 2020-09-15 17:22:06 -07:00
Ryan Rix 1ea75dba55 add Thing.search_name 2020-09-15 17:20:36 -07:00
4 changed files with 213 additions and 19 deletions

View File

@ -25,11 +25,12 @@ This module contains the base HTML templates for the application, as well as the
</head>
<body>
<header class="top-bar">
<section class="top-bar-left">
<ul id="dropdown" class="dropdown menu" data-dropdown-menu>
<li class="menu-text"><%= assigns[:site_title] || "Poka Ijo" %></li>
</ul>
</section>
<div class="top-bar-left">
<h1><%= link assigns[:site_title] || "Poka Ijo", to: Routes.page_path(@conn, :index) %></h1>
</div>
<div class="top-bar-right">
<%= link "things", to: Routes.thing_path(@conn, :index) %>
</div>
</header>
<main role="main" class="grid-container">
@ -86,6 +87,7 @@ This is where all of my front end logic will come by loaded or stored. For now t
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import "../css/app.scss"
import "../../deps/phoenix_html/priv/static/phoenix_html.js"
// import "./live.js"

View File

@ -206,7 +206,8 @@ scope "/things", PokaIjoWeb do
pipe_through :browser
get "/:id/into", ThingController, :insert
# get "/:id/move", ThingController, :move
get "/:id/move", ThingController, :move
post "/:id/move", ThingController, :do_move
resources "/", ThingController
end
#+end_src

View File

@ -11,15 +11,18 @@ mix phx.routes | grep thing_path
#+end_src
#+results:
: thing_path GET /things/:id/into PokaIjoWeb.ThingController :insert
: thing_path GET /things/:id/move PokaIjoWeb.ThingController :move
: thing_path GET /things PokaIjoWeb.ThingController :index
: thing_path GET /things/:id/edit PokaIjoWeb.ThingController :edit
: thing_path GET /things/new PokaIjoWeb.ThingController :new
: thing_path GET /things/:id PokaIjoWeb.ThingController :show
: thing_path POST /things PokaIjoWeb.ThingController :create
: thing_path PATCH /things/:id PokaIjoWeb.ThingController :update
: thing_path DELETE /things/:id PokaIjoWeb.ThingController :delete
#+begin_example
thing_path GET /things/:id/into PokaIjoWeb.ThingController :insert
thing_path GET /things/:id/move PokaIjoWeb.ThingController :move
thing_path POST /things/:id/move PokaIjoWeb.ThingController :do_move
thing_path GET /things PokaIjoWeb.ThingController :index
thing_path GET /things/:id/edit PokaIjoWeb.ThingController :edit
thing_path GET /things/new PokaIjoWeb.ThingController :new
thing_path GET /things/:id PokaIjoWeb.ThingController :show
thing_path POST /things PokaIjoWeb.ThingController :create
thing_path PATCH /things/:id PokaIjoWeb.ThingController :update
thing_path DELETE /things/:id PokaIjoWeb.ThingController :delete
#+end_example
** =:index= lists all Things
@ -187,7 +190,7 @@ end
def insert(conn, %{"id" => id}) do
container = Thing.get_by(id: id)
changeset = Thing.new(%{"container_id": container.id})
changeset = Thing.new(%{"container_id" => container.id})
conn
|> put_view(PokaIjoWeb.ThingView)
@ -454,7 +457,7 @@ end
*** Controller Tests
#+begin_src elixir :tangle test/poka_ijo_web/controllers/thing_controller_test.exs
defmodule WebControllersThingShowTest do
defmodule WebControllersThingUpdateTest do
use PokaIjoWeb.ConnCase
test "assigns are assigned", %{conn: conn} do
@ -477,8 +480,184 @@ I'm not sure how to do this yet. I am thinking that this will be the arbitrary p
The =move= action is maybe misnamed -- it's not doing the movement just rendering a form where the user will be able to.
#+begin_src elixir :noweb-ref controller-move
def move(conn, _) do
conn |> redirect(to: Routes.thing_path(conn, :index))
def move(conn, %{"id" => id, "filter_str" => filter_str}) do
_move(conn, id, filter_str)
end
def move(conn, %{"id" => id}) do
_move(conn, id)
end
defp _move(conn, id, filter_str \\ nil) do
{int_id, _} = Integer.parse(id)
things =
cond do
is_binary(filter_str) -> Thing.search_name("%#{filter_str}%")
true -> Thing.all()
end |> Enum.filter(&(&1.id != int_id))
thing_to_move = Thing.get_by(id: id)
conn
|> put_view(PokaIjoWeb.ThingView)
|> render(:move,
things: things,
subject: thing_to_move,
filter: filter_str,
page_title: "Poka Ijo: Moving #{thing_to_move.name}"
)
end
def do_move(conn, %{"id" => id, "new_parent" => new_parent}) do
t = Thing.get_by(id: id)
parent = Thing.get_by(id: new_parent)
changeset = Thing.changeset(t, %{container_id: parent.id})
case Thing.update(changeset) do
{:error, changeset} ->
Logger.info("Changeset save failed: #{inspect(changeset.errors)}")
conn
|> put_flash(:alert, "Changeset save failed: #{inspect(changeset.errors)}")
|> put_view(PokaIjoWeb.ThingView)
_ ->
conn
|> put_flash(:success, "#{t.name} moved!")
end
|> redirect(to: Routes.thing_path(conn, :show, t))
end
#+end_src
*** Templates
This relies on [[file:../javascript.org][§JavaScript]] to work; =Phoenix.HTML.Link.button= requires javascript for the =move here= actions to work. Eventually I'll move that to be a Phoenix LiveView module, which requires javascript in different ways. I don't really care about that here; this is probably going to be running on my phone, in Firefox. Now, for the "regular user" frontend, it will be key that the thing can behave without JavaScript but this code is fine, I think.
#+begin_src web :tangle lib/poka_ijo_web/templates/thing/move.html.eex :mkdirp yes
<div class="grid-x grid-padding-x">
<div class="small-12 large-8 cell">
<h2>Move <%= @subject.name %> to:</h2>
<%= form_for @conn, Routes.thing_path(@conn, :move, @subject), [method: "get"], fn f -> %>
<div class="input-group">
<span class="input-group-label">Name Filter:</span>
<%= text_input f, :filter_str, value: assigns[:filter], class: "input-group-field" %>
<div class="input-field-button">
<%= submit "filter", class: "button secondary" %>
</div>
</div>
<% end %>
<%= for thing <- @things do %>
<div class="callout grid-x">
<div class="small-8 medium-10 cell">
<%= thing.name %>
</div>
<div class="small-4 medium-2 cell">
<%= button "move here", method: :post,
to: Routes.thing_path(@conn, :do_move, @subject, new_parent: thing.id),
class: "button primary expanded" %>
</div>
</div>
<% end %>
</div>
<div class="small-12 large-4 cell">
<iframe src="https://www.youtube.com/embed/VFZNvj-HfBU" frameborder="0" allow="picture-in-picture" allowfullscreen></iframe>
</div>
</div>
#+end_src
*** Controller Tests
Tests for the =:move= action are pretty simple, we're mostly just setting up the string filtering. The list of places to move to must of course not include the object itself. This also does *nothing* to prevent cycles in the object graph! Probably want to care about that at some point.
I have a helper which I embed that'll create the objects:
#+begin_src elixir :noweb-ref move-test-harnesses
defp insert do
{:ok, container} =
%{name: "container", description: "hi"}
|> PokaIjo.Thing.new()
|> PokaIjo.Repo.insert()
{:ok, cur_container} =
%{name: "current container", description: "hi"}
|> PokaIjo.Thing.new()
|> PokaIjo.Repo.insert()
{:ok, t} =
%{name: "test", description: "hi", container_id: cur_container.id}
|> PokaIjo.Thing.new()
|> PokaIjo.Repo.insert()
{:ok, candidate_1} =
%{name: "candidate 1", description: "hi"}
|> PokaIjo.Thing.new()
|> PokaIjo.Repo.insert()
{:ok, candidate_2} =
%{name: "candidate 2", description: "hi"}
|> PokaIjo.Thing.new()
|> PokaIjo.Repo.insert()
%{subject: t, container: container, all: [t, container, candidate_1, candidate_2, cur_container]}
end
#+end_src
#+begin_src elixir :tangle test/poka_ijo_web/controllers/thing_controller_test.exs :noweb yes
defmodule WebControllersThingMoveTest do
use PokaIjoWeb.ConnCase
alias WebControllersThingMoveTest
<<move-test-harnesses>>
test ":move @things doesn't include subject", %{conn: conn} do
%{subject: t, all: all} = insert()
conn = get(conn, "/things/#{t.id}/move")
assert conn.assigns[:things] != nil
assert Enum.count(conn.assigns[:things]) == Enum.count(all) - 1
end
test ":move filter_str works", %{conn: conn} do
%{subject: t} = insert()
conn = get(conn, "/things/#{t.id}/move?filter_str=ca")
assert conn.assigns[:things] != nil
assert Enum.count(conn.assigns[:things]) == 2
end
test ":move assigns are assigned (no filter)", %{conn: conn} do
%{subject: t} = insert()
conn = get(conn, "/things/#{t.id}/move")
assert conn.assigns != nil
assert conn.assigns[:subject] != nil
assert conn.assigns[:subject].id == t.id
assert conn.assigns[:page_title] =~ "Moving"
end
end
#+end_src
the =:do_move= behavior is also pretty easy to test.
#+begin_src elixir :tangle test/poka_ijo_web/controllers/thing_controller_test.exs :noweb yes
defmodule WebControllersThingDoMoveTest do
use PokaIjoWeb.ConnCase
alias PokaIjo.Thing
alias WebControllersThingMoveTest
<<move-test-harnesses>>
test ":do_move happy path works", %{conn: conn} do
%{subject: t, container: container} = insert()
conn = post(conn, "/things/#{t.id}/move?new_parent=#{container.id}")
assert conn.status == 302
assert Thing.get_by(id: t.id).container_id == container.id
assert get_flash(conn, :success) =~ "moved!"
end
end
#+end_src

View File

@ -24,6 +24,7 @@ defmodule PokaIjo.Thing do
<<thing-get_parent>>
<<thing-is_container>>
<<thing-search_name>>
end
#+end_src
@ -187,6 +188,17 @@ I have some simple "getters" now:
- Some search functions
#+begin_src elixir :noweb-ref thing-search_name
def searchq filter do
from t in PokaIjo.Thing,
where: ilike(t.name, ^filter)
end
def search_name filter do
Repo.all(searchq(filter))
end
#+end_src
=Thing.parent_q= and =Thing.children_q= return an =Ecto.Query= which can have other things attached to it before the query. Any use of the query outside this module should be scrutinized, if it's being used a lot in similar patterns, add a retriever here.
* Working with Things