Goal
マイクロポストの一覧を表示する。
Dev-Environment
OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.13.1
PostgreSQL: postgres (PostgreSQL) 9.4.4
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.13.1
PostgreSQL: postgres (PostgreSQL) 9.4.4
Wait a minute
ユーザのプロファイルページに手を入れて、
マイクロポストの一覧を表示できるようにしましょう。
マイクロポストの一覧を表示できるようにしましょう。
最初に謝罪しておきます。
ソースコードを修正に修正を繰り返していたら、
行動の順番が分からなくなってしまいました。
ソースコードを修正に修正を繰り返していたら、
行動の順番が分からなくなってしまいました。
説明は記述していますが、必ずしも順番にはなっていません。
そして、修正量が多いです。
今までの記事をやった方々には、お手数をお掛けして申し訳ないです。
そして、修正量が多いです。
今までの記事をやった方々には、お手数をお掛けして申し訳ないです。
それでも仕方ないなやってやるよと言う親切な方は、
どうぞお付き合い下さい。
どうぞお付き合い下さい。
Index
Microposts List
|> Microposts List
|> Pagination again
|> Share template of pagination
|> Extra
|> Microposts List
|> Pagination again
|> Share template of pagination
|> Extra
Microposts List
ユーザに紐づいている、マイクロポストの一覧を表示します。
ファイル: web/controllers/user_controller.ex
showアクションを修正。
showアクションを修正。
def show(conn, %{"id" => id}) do
user = Repo.get(SampleApp.User, id)
page = Repo.all(from(m in SampleApp.Micropost, where: m.user_id == ^user.id, order_by: [desc: m.inserted_at]))
render(conn, "show.html", user: user, posts: page)
end
ファイル: web/templates/user/show.html.eex
マイクロポストを表示させる。
マイクロポストを表示させる。
<h2>User profile</h2>
<div style="float: left; margin-top: 20px; margin-right: 20px;">
<img src="<%= get_gravatar_url(@user) %>" class="gravatar">
<h2><%= @user.name %></h2>
</div>
<%= if @posts do %>
<div style="float: right; margin-right: 350px;">
<h2>Microposts<h2>
<h2>
<%= for post <- @posts do %>
<li style="padding: 10px 0; border-top: 1px solid #e8e8e8;"><%= post.content %></li>
<% end %>
</h2>
</div>
<% end %>
<div style="clear: left; margin-top: 60px">
<%= link "Edit", to: user_path(@conn, :edit, @user), class: "btn btn-default btn-xs" %>
<%= link "Delete", to: user_path(@conn, :delete, @user), method: :delete, class: "btn btn-danger btn-xs" %>
</div>
う~ん、相変わらずの直書き・・・
私のデザイン力(CSS力)はこの程度だ。
私のデザイン力(CSS力)はこの程度だ。
Pagination again
ページネーション再び・・・
マイクロポストも数が増えてきたら、
ページネーションをしてあげないと見辛くなってしまいますね。
ページネーションをしてあげないと見辛くなってしまいますね。
うん、またなんだ。一緒に頑張りましょう・・・
ファイル: lib/helpers/pagination_helper.ex
ページネーションを行うのに補助するモジュールを作成。
上から順に以下のような機能。
ページネーションを行うのに補助するモジュールを作成。
上から順に以下のような機能。
- select_pageがnilかemptyか判定する関数
- select_pageが有効なページか判定する関数
- 判定をまとめている関数
- 各モデルでのページネーションを補助する関数
defmodule SampleApp.Helpers.PaginationHelper do
@page_size "1"
defp is_nil_or_empty?(select_page) do
is_nil(select_page) || select_page == ""
end
defp is_valid_value?(select_page) do
Regex.match?(~r/^[0-9]+$/, select_page)
end
defp is_able_to_paginate?(select_page) do
!is_nil_or_empty?(select_page) && is_valid_value?(select_page)
end
def paginate(query, select_page) do
if is_able_to_paginate?(select_page) do
query |> SampleApp.Repo.paginate(page: select_page, page_size: @page_size)
else
nil
end
end
end
ファイル: web/models/user.ex
定数と関数の削除。また、paginate/1関数の修正。
(ソースコード全文を記載しています)
定数と関数の削除。また、paginate/1関数の修正。
(ソースコード全文を記載しています)
defmodule SampleApp.User do
use SampleApp.Web, :model
use Ecto.Model.Callbacks
import Ecto.Query
before_insert :set_password_digest
before_update :set_password_digest
schema "users" do
field :name, :string
field :email, :string
field :password_digest, :string
field :password, :string, virtual: true
has_many :microposts, SampleApp.Micropost
timestamps
end
@required_fields ~w(name email password)
@optional_fields ~w()
@doc """
Creates a changeset based on the `model` and `params`.
If `params` are nil, an invalid changeset is returned
with no validation performed.
"""
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> validate_presence(:name)
|> validate_presence(:email)
|> validate_presence(:password)
|> validate_format(:email, ~r/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i)
|> validate_unique(:name, on: SampleApp.Repo)
|> validate_unique(:email, on: SampleApp.Repo)
|> validate_length(:name, min: 1)
|> validate_length(:name, max: 50)
|> validate_length(:password, min: 8)
|> validate_length(:password, max: 100)
end
def paginate(select_page) do
SampleApp.Helpers.PaginationHelper.paginate(
from(u in SampleApp.User, order_by: [asc: :name]),
select_page)
end
# before_insert - password to password_digest
def set_password_digest(changeset) do
password = Ecto.Changeset.get_field(changeset, :password)
change(changeset, %{password_digest: encrypt(password)})
end
# find user from email
def find_user_from_email(email) do
SampleApp.Repo.get_by(SampleApp.User, email: email)
end
# password decrypt
def decrypt(password) do
Safetybox.decrypt(password)
end
# my presence check validation
defp validate_presence(changeset, field_name) do
field_data = Ecto.Changeset.get_field(changeset, field_name)
cond do
field_data == nil ->
add_error changeset, field_name, "#{field_name} is nil"
field_data == "" ->
add_error changeset, field_name, "No #{field_name}"
true ->
changeset
end
end
# password encrypt
defp encrypt(password) do
Safetybox.encrypt(password, :default)
end
end
ファイル: web/models/micropost.ex
paginate/2関数を追加。
paginate/2関数を追加。
def paginate(user_id, select_page) do
SampleApp.Helpers.PaginationHelper.paginate(
from(m in SampleApp.Micropost, where: m.user_id == ^user_id, order_by: [desc: m.inserted_at]),
select_page)
end
ファイル: web/controllers/user_controller.ex
index、showアクションで特定のパラメータが存在しない場合エラーを出す。
plug :scrub_params, "select_page" when action in [:index, :show]
indexアクションを以下のように修正。
def index(conn, %{"select_page" => select_page}) do
page = SampleApp.User.paginate(select_page)
if page do
render(conn, "index.html",
users: page.entries,
current_page: page.page_number,
total_pages: page.total_pages,
page_list: Range.new(1, page.total_pages))
else
conn
|> put_flash(:error, "Invalid page number!!")
|> render("index.html", users: [])
end
end
showアクションを以下のように修正。
def show(conn, %{"id" => id, "select_page" => select_page}) do
user = Repo.get(SampleApp.User, id)
page = SampleApp.Micropost.paginate(user.id, select_page)
if page do
render(conn, "show.html",
user: user,
posts: page.entries,
current_page: page.page_number,
total_pages: page.total_pages,
page_list: Range.new(1, page.total_pages))
else
conn
|> put_flash(:error, "Invalid page number!!")
|> render("show.html", user: user, posts: [])
end
end
ファイル: web/views/layout_view.ex
Profileページでも使えるように関数を修正。
Profileページでも使えるように関数を修正。
defmodule SampleApp.LayoutView do
use SampleApp.Web, :view
def current_user(conn) do
conn.assigns[:current_user]
end
def add_first_page_param(action) do
"#{action}?select_page=1"
end
end
ファイル: web/templates/layout/header.html.eex
ProfileとAll Usersへのリンクを修正。
ProfileとAll Usersへのリンクを修正。
<%= if current_user(@conn) do %>
<li><%= link "Profile", to: add_first_page_param(user_path(@conn, :show, current_user(@conn).id)) %><li>
<li><%= link "All Users", to: add_first_page_param(user_path(@conn, :index)) %><li>
<li><%= link "Sign-out", to: session_path(@conn, :delete) %></li>
<% else %>
Share template of pagination
テンプレートに記述されているページネーション部分を
別テンプレートに移し、共有テンプレートとします。
別テンプレートに移し、共有テンプレートとします。
また、UserViewに実装している、
ページネーションのリンクを作成している関数を修正します。
ページネーションのリンクを作成している関数を修正します。
ファイル: web/views/user_view.ex
以下の関数を修正。
以下の関数を修正。
def get_previous_page_url(action, current_page) do
get_page_url(action, current_page - 1)
end
def get_next_page_url(action, current_page) do
get_page_url(action, current_page + 1)
end
def get_page_url(action, page_number) do
"#{action}?select_page=#{page_number}"
end
以下の関数を追加。
def is_empty_list?(list) when is_list(list) do
list == []
end
ファイル: web/templates/user/index.html.eex
以下のように修正する。
以下のように修正する。
<h2 class="text-center">All users</h2>
<%= if !is_empty_list?(@users) do %>
<%= render "pagination.html",
action: user_path(@conn, :index),
current_page: @current_page,
page_list: @page_list,
total_pages: @total_pages %>
<table class="table">
<thead>
<tr>
<th>Profile Image</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<%= for user <- @users do %>
<tr>
<td>
<div class="gravatar" style="float: left; margin-right: 10px;">
<img src="<%= get_gravatar_url(user) %>" class="gravatar">
</div>
</td>
<td><h4><%= user.name %><h4></td>
</tr>
<% end %>
</tbody>
</table>
<%= render "pagination.html",
action: user_path(@conn, :index),
current_page: @current_page,
page_list: @page_list,
total_pages: @total_pages %>
<% end %>
ファイル: web/templates/user/show.html.eex
以下のように修正する。
以下のように修正する。
<%= alias SampleApp.LayoutView %>
<h2>User profile</h2>
<div style="float: left; margin-top: 20px; margin-right: 20px;">
<img src="<%= get_gravatar_url(@user) %>" class="gravatar">
<h2><%= @user.name %></h2>
</div>
<div style="float: right; margin-right: 350px;">
<h2>Microposts</h2>
<%= if !is_empty_list?(@posts) do %>
<h2>
<%= for post <- @posts do %>
<li style="padding: 10px 0; border-top: 1px solid #e8e8e8;"><%= post.content %></li>
<% end %>
</h2>
<%= render "pagination.html",
action: user_path(@conn, :show, LayoutView.current_user(@conn).id),
current_page: @current_page,
page_list: @page_list,
total_pages: @total_pages %>
<% end %>
</div>
<div style="clear: left; margin-top: 60px">
<%= link "Edit", to: user_path(@conn, :edit, @user), class: "btn btn-default btn-xs" %>
<%= link "Delete", to: user_path(@conn, :delete, @user), method: :delete, class: "btn btn-danger btn-xs" %>
</div>
ファイル: web/templates/user/pagination.html.eex
共有のテンプレートを作成。
共有のテンプレートを作成。
<nav>
<ul class="pagination">
<!-- previous link -->
<%= if @current_page > 1 do %>
<li>
<a href="<%= get_previous_page_url(@action, @current_page) %>" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<% end %>
<!-- page link -->
<%= for page_number <- @page_list do %>
<%= if page_number == @current_page do %>
<li class="active">
<a href="<%= get_page_url(@action, page_number) %>">
<%= page_number %><span class="sr-only">(current)</span>
</a>
</li>
<% else %>
<li><a href="<%= get_page_url(@action, page_number) %>"><%= page_number %></a></li>
<% end %>
<% end %>
<!-- next link -->
<%= if @current_page < @total_pages do %>
<li>
<a href="<%= get_next_page_url(@action, @current_page) %>" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
<% end %>
</ul>
</nav>
Extra
利用した正規表現のテストを記載。
正規表現: /^[0-9]+$/
説明: 数字の繰り返しに対してマッチ
説明: 数字の繰り返しに対してマッチ
iexで簡単な確認を行う
iex(1)> Regex.match?(~r/^[0-9]+$/, "12345")
true
iex(2)> Regex.match?(~r/^[0-9]+$/, "hoge")
false
iex(3)> Regex.match?(~r/^[0-9]+$/, "1hoge")
false
iex(4)> Regex.match?(~r/^[0-9]+$/, "1hoge2")
false
iex(5)> Regex.match?(~r/^[0-9]+$/, "123hoge234")
false
iex(6)> Regex.match?(~r/^[0-9]+$/, "1")
true
iex(7)> Regex.match?(~r/^[0-9]+$/, "12")
true
iex(8)> Regex.match?(~r/^[0-9]+$/, "01")
true
iex(9)> Regex.match?(~r/^[0-9]+$/, "-1")
false
ついでの確認。
iex(1)> String.to_integer("01")
1
Speaking to oneself
更新が遅れてしまって申し訳ない。
また、修正に次ぐ修正を強いてしまって申し訳ない。
全ては見通しと設計の甘さが問題ですね。
また、修正に次ぐ修正を強いてしまって申し訳ない。
全ては見通しと設計の甘さが問題ですね。
とりあえず、最低限レベルだが記事にしてもいいソースコードになった。
根本的にページネーションを実装するときの設計を間違えた気がします。
ページネーションの実装を完全に修正する可能性があります。
もしかしたら、といったレベルの話なので頭の片隅にでも置いといて下さい。
ページネーションの実装を完全に修正する可能性があります。
もしかしたら、といったレベルの話なので頭の片隅にでも置いといて下さい。
hexdocs - v0.14.3 Ecto.Query.preload/3を使えば、
ユーザからの紐づけでページネーションのデータを取得することができる気がするんだよ。
ユーザからの紐づけでページネーションのデータを取得することができる気がするんだよ。
ちょっと調べきれてないので、
動作検証を行ったら修正するかもしれません。
動作検証を行ったら修正するかもしれません。
最初から一発OKのソースコードを出せればいいのですが、
今のやり方だと難しいですね・・・
今のやり方だと難しいですね・・・
うわぁーん(・_・。)グスン
修正ばっかだな・・・全ては自分のレベルが低いのがいけない( ;∀;) カナシイナー
修正ばっかだな・・・全ては自分のレベルが低いのがいけない( ;∀;) カナシイナー