スポンサーリンク

2015年8月13日

[Elixir+Phoenix]Web interface for the user who follows

Goal

フォロー/アンフォローのWebインターフェースを実装する。
また、フォロー/フォロワーのユーザ一覧ページを実装する。

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

Wait a minute

少し、Rails Tutorialと実施する順番が異なる。
最初に書いておきます。
ソースコードが汚い。本当に汚い。
最低のスパゲティコードです。
記事を見て下さっている方には大変申し訳ないのですが、
修正は後回しにさせて頂きます。
他人にお見せするソースコードではないことは、重々承知の上です。
笑われるのを承知の上で公開します。
コメントでm9(^Д^)してくれても構いません。
それでもTutorialを全部終わらせるのを優先させて下さい。
約束します。必ず修正します。
お目汚しのソースコードを公開して申し訳ない。
独り言にロードマップ(?)を書いてありますので、
詳しいところはそちらを見て頂ければと思います。m(_ _)m
(よし・・・言い訳完了)

Index

Web interface for the user who follows
|> Following/Followers User List
|> Display Follow/Unfollow Button
|> Relationship Controller
|> Extra

Following/Followers User List

フォローしているユーザ一覧とフォロワーユーザ一覧を表示させます。
ファイル: web/router.ex
ルーティングの追加。
get "user/:id/following", UserController, :following
get "user/:id/followers", UserController, :followers
ルーティングを取得。
>mix phoenix.routes
...
user_path  GET     /user/:id/following  SampleApp.UserController.following/2
user_path  GET     /user/:id/followers  SampleApp.UserController.followers/2
...
ユーザのプロファイルページにfollowingとfollowersの値とリンクを表示させる。
ファイル: web/controllers/user_controller.ex
ユーザデータの取得時にpreloadを追加。
def show(conn, %{"id" => id, "select_page" => select_page}) do
  user = Repo.get(SampleApp.User, id) |> Repo.preload(:relationships) |> Repo.preload(:reverse_relationships)
  ...
end
ファイル: web/templates/user/show.html.eex
以下のようにリンクを追加。
<div style="float: left; margin-top: 20px; margin-right: 20px;">
  <img src="<%= get_gravatar_url(@user) %>" class="gravatar">
  <h2><%= @user.name %></h2>
  <strong style="float: left; margin-right: 5px">
    <a href="<%= user_path(@conn, :following, @user) %>">Following (<%= Enum.count(@user.followed_users) %>)</a>
  </strong>
  <strong style="float: right;">
    <a href="<%= user_path(@conn, :followers, @user) %>">Followers (<%= Enum.count(@user.followers) %>)</a>
  </strong>
</div>
今のままではリンク先がありません。
followingから実装します。
ファイル: web/controllers/user_controller.ex
認可に追加。
plug SampleApp.Plugs.SignedInUser when action in [:index, :show, :edit, :update, :delete, :following, :followers]
アクションを追加。
def following(conn, params) do
  select_page = params["select_page"]
  id = params["id"]

  if is_nil(select_page) do
    select_page = "1"
  end

  user = Repo.get(SampleApp.User, id) |> Repo.preload(:relationships) |> Repo.preload(:reverse_relationships)
  ids_list = to_followed_user_ids_list(user.followed_users)

  page = SampleApp.Helpers.PaginationHelper.paginate(
    from(u in SampleApp.User, where: u.id in ^ids_list, order_by: [asc: :name]),
    select_page)

  if page do
    render(conn, "following.html",
           user: user,
           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("following.html", user: user, followed_users: [])
  end
end
関数を追加。
defp list_map_to_value_list(repo_result, key) do
  for map <- repo_result do Map.get(map, key) end
end
ファイル: web/templates/user/following.html.eex
テンプレートを作成。
<h2>Followed users</h2>

<%= render "show_follow.html", action: user_path(@conn, :following, @user), conn: @conn,
                        user: @user,
                        users: @users,
                        current_page: @current_page,
                        total_pages: @total_pages,
                        page_list: @page_list %>
続いて、followersを実装します。
ファイル: web/controllers/user_controller.ex
アクションを追加。
def followers(conn, params) do
  select_page = params["select_page"]
  id = params["id"]

  if is_nil(select_page) do
    select_page = "1"
  end

  user = Repo.get(SampleApp.User, id) |> Repo.preload(:relationships) |> Repo.preload(:reverse_relationships)
  ids_list = to_follower_user_ids_list(user.followers)

  page = SampleApp.Helpers.PaginationHelper.paginate(
    from(u in SampleApp.User, where: u.id in ^ids_list, order_by: [asc: :name]),
    select_page)

  if page do
    render(conn, "followers.html",
           user: user,
           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("followers.html", user: user, followed_users: [])
  end
end
ファイル: web/templates/user/followers.html.eex
テンプレートを作成。
<h2>Follower users</h2>

<%= render "show_follow.html", action: user_path(@conn, :followers, @user), conn: @conn,
                        user: @user,
                        users: @users,
                        current_page: @current_page,
                        total_pages: @total_pages,
                        page_list: @page_list %>
ファイル: web/templates/user/show_follow.html.eex
共通のテンプレートを作成。
<div style="float: left; margin-top: 20px; margin-right: 20px;">
      <img src="<%= get_gravatar_url(@user) %>" class="gravatar">
      <h1><%= @user.name %></h1>
      <div><%= link "view my profile", to: user_path(@conn, :show, @user), class: "btn btn-default btn-xs" %></div>
      <strong style="float: left; margin-right: 5px">
        Following (<%= Enum.count(@user.followed_users) %>)
      </strong>
      <strong style="float: right;">
        Followers (<%= Enum.count(@user.followers) %>)
      </strong>
</div>

<div style="clear: left; margin-top: 30px;">
  <%= if @users do %>
    <div class="user_avatars">
      <%= for follow_user <- @users do %>
        <img src="<%= get_gravatar_url(follow_user) %>" class="gravatar">
      <% end %>
    </div>
  <% end %>
</div>

<div style="clear: right; margin-left: 250px;">
  <%= if @users do %>
    <ul>
      <%= for follow_user <- @users do %>
        <li>
          <img src="<%= get_gravatar_url(follow_user) %>" class="gravatar">
          <h4><%= follow_user.name %></h4>
        </li>
      <% end %>
    </ul>

    <%= render "pagination.html",
             action: @action,
             current_page: @current_page,
             page_list: @page_list,
             total_pages: @total_pages %>
  <% end %>
</div>

Display Follow/Unfollow Button

フォロー/アンフォローのボタンを表示させます。
ファイル: web/templates/user/show.html.eex
以下を追加。(この時点では暫定)
<%= if !current_user?(@conn, @user) do %>
  <%= if !following?(@conn, @user.id) do %>
    <%= form_tag(user_path(@conn, :following, @conn.assigns[:current_user]), method: :post) %>
      <input type="hidden" name="follow_id" value="<%= @user.id %>">
      <%= submit "Follow", class: "btn btn-primary" %>
    </form>
  <% else %>
    <%= form_tag(user_path(@conn, :followers, @conn.assigns[:current_user]), method: :delete) %>
      <input type="hidden" name="unfollow_id" value="<%= @user.id %>">
      <%= submit "Unfollow", class: "btn btn-primary" %>
    </form>
  <% end %>
<% end %>
ファイル: web/views/user_view.ex
関数を追加。
def following?(conn, follow_user_id) do
  SampleApp.Relationship.following?(conn.assigns[:current_user].id, follow_user_id)
end
def current_user?(conn, user) do
  conn.assigns[:current_user].id == user.id
end
ファイル: web/controllers/user_controller.ex
別ユーザのプロファイルを参照できるように、showを削除。
plug :correct_user? when action in [:edit, :update, :delete]

Relationship Controller

フォローする、フォロー解除を画面から行えるようにします。
ファイル: web/router.ex
ルーティングを追加。
resources "/relationship", RelationshipController, only: [:create, :delete]
ファイル: web/controllers/relationship_controller.ex
コントローラを作成。
defmodule SampleApp.RelationshipController do
  use SampleApp.Web, :controller

  plug SampleApp.Plugs.CheckAuthentication
  plug SampleApp.Plugs.SignedInUser
  plug :action

  def create(conn, params) do
    if SampleApp.Relationship.follow!(params["id"], params["follow_id"]) do
      conn = put_flash(conn, :info, "Follow successfully!!")
    else
      conn = put_flash(conn, :error, "Follow failed!!")
    end

    redirect(conn, to: static_pages_path(conn, :home))
  end

  def delete(conn, params) do
    SampleApp.Relationship.unfollow!(params["id"], params["unfollow_id"])

    conn
    |> put_flash(:info, "Unfollow successfully!!")
    |> redirect(to: static_pages_path(conn, :home))
  end
end
ファイル: web/templates/user/show.html.eex
以下のように修正。
<%= if !current_user?(@conn, @user) do %>
  <%= if !following?(@conn, @user.id) do %>
    <%= form_tag(relationship_path(@conn, :create), method: :post) %>
      <input type="hidden" name="id" value="<%= @conn.assigns[:current_user].id %>">
      <input type="hidden" name="follow_id" value="<%= @user.id %>">
      <%= submit "Follow", class: "btn btn-primary" %>
    </form>
  <% else %>
    <%= form_tag(relationship_path(@conn, :delete, @conn.assigns[:current_user]), method: :delete) %>
      <input type="hidden" name="unfollow_id" value="<%= @user.id %>">
      <%= submit "Unfollow", class: "btn btn-primary" %>
    </form>
  <% end %>
<% end %>

Extra

リスト内のデータ数をカウントしたいことがあると思う。
以下のようにすれば、Ectoを使ってDBから取得したデータの数を取得できる。
iex> users = SampleApp.Repo.all(SampleApp.User)
iex> Enum.count(users)
Ectoにおける副問合せ。
以下のようにして副問合せができる。
iex> SampleApp.Repo.all(from u in SampleApp.User, where: u.id in [2,3] or u.id == 1)
Description:
副問合せのドキュメントは以下のリンク先にあります。
ドキュメント: hexdocs - v0.15.0 Ecto.Query.API left in right

Speaking to oneself

すいませんすいませんすいませんす
いませんすいませんすいませんすい
ませんすいませんすいませんすいま
せんすいませんすいませんすいませ
んすいませんすいませんすいません
ソースコードが汚いし整合性が取れてないですよね。
後で直すんで今はご勘弁を・・・
(ページネーションの部分も変わります)
説明も全然書いてない・・・
(いつものことな気がするが・・・)
今後の予定です。毎度のことですが暫定です。
かなりの気分屋なので変わるかもです。
(でもソースコードのリファクタリングは必ず実施します)
  • v0.1 ・・・ 一通りやり通す
  • v0.2 ・・・ ソースコードの大修正(リファクタリング)
  • v0.3 ・・・ 記事改稿、記事中のソースを移動/修正
  • v0.4 ・・・ v0.16.1(その時の最新)に対応(バージョンアップ)
  • v0.5 ・・・ ???
  • v?.? ・・・ 自作のページネーションライブラリを組込む
こういうのもロードマップと言うんだろうか・・・
当初、Ectoで副問合せするためには、
SQLを自分で作成して投げる必要があると勘違いしていました。
しかし、調査不足でした。
“Ecto.Query.API”の存在を知らず、自分で実装しようと思った。
見つかったのは完全に偶然の産物でした。
良かった・・・本当に良かった・・・
何と言うか・・・ちゃんと調べましょうってのが今回の教訓ですねorz
Twitterで喚いていた基地外は自分です・・・orz

Bibliography

人気の投稿