スポンサーリンク

2015年7月22日

[Elixir+Phoenix]Updating users

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

Wait a minute

ここまでやってきた皆さんなら、
特に難しいことは何もない内容です。
既に、どういったことをすべきか、
頭の中で予想ができているのではないでしょうか?
それではやってきましょう~

Index

UserUpdate
|> Add edit action
|> Create input form template
|> Add settings link
|> Add update action
|> Sharing input form template
|> Extra

Add edit action

UserControllerへeditアクションを追加します。
ファイル: web/controllers/user_controller.ex
以下のアクション関数を追加して下さい。
def edit(conn, %{"id" => id}) do
  user = Repo.get(SampleApp.User, id)
  user = Map.put(user, :password, SampleApp.User.decrypt(user.password_digest))
  changeset = SampleApp.User.changeset(user)

  render(conn, "edit.html", user: user, changeset: changeset)
end
Description:
passwordにはvirtual属性を付けているのでDBに値が入っていません。
そのためpassword_digestからパスワードを復号化して値を入力しています。
テンプレートで入力されている値は、userの方になる。
なので、changesetの値を変更しても反映されない。
ファイル: web/models/user.ex
暗号化している部分を修正します。
defp encrypt(password) do
  Safetybox.encrypt(password, :default)
end
Caution:
パスワードの暗号化ですが、利用する関数を間違えていたようです。
なので、暗号化する関数に修正を入れます。
変更自体は、:defaultを加えただけです。
これは、デフォルトのSecretKeyとSaltKeyを利用して暗号化してくれます。
これを付けないと復号化の段階で:errorが返ってきてしまい復号化できませんでした。
ファイル: web/controllers/session_controller.ex
defp authentication(user, password) do
  case user do
    nil -> false
      _ ->
        password == Safetybox.decrypt(user.password_digest)
  end
end
Caution:
暗号化している関数を変えるとSafetybox.is_decrypted/2で評価できないため、
authentication/2の評価部分も変更します。
ファイル: web/models/user.ex
復号化する関数を追加します。
def decrypt(password) do
  Safetybox.decrypt(password)
end

Create input form template

さて、edit.html.eexがないため現状だと表示ができません。
作成をしましょう。
ファイル: web/templates/user/edit.html.eex
<h2>Edit UserProfile</h2>

<%= form_for @changeset, user_path(@conn, :update, @user), fn f -> %>
  <%= if f.errors != [] do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below:</p>
      <ul>
        <%= for {attr, message} <- f.errors do %>
          <li><%= humanize(attr) %> <%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="form-group">
    <label>Name</label>
    <%= text_input f, :name, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Email</label>
    <%= text_input f, :email, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Password</label>
    <%= text_input f, :password, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>
どこかで見たようなテンプレートですね・・・
えぇそうです。気のせいではありません。
new.html.eexとほぼ同一の内容になります。
(異なるのは指定しているアクションくらいですね)
そのため、後にeditとnewのテンプレートでフォーム部分を共通化させます。
さて後は、画面に表示したいのですが、
リンクがないのでURLを直接叩くしかないですね。
リンクを追加してしまいましょう。
ファイル: web/templates/user/show.html.eex
<h2>
  <div class="gravatar" style="float: left; margin-right: 10px;">
    <img src="<%= get_gravatar_url(@user) %>" class="gravatar">
  </div>
  <p><%= @user.name %></p>
  <%= link "Edit", to: user_path(@conn, :edit, @user), class: "btn btn-default btn-xs" %>
</h2>
現在のテンプレートだと、非常にダサい表示になっていますね。
自分の投稿を表示する段になった時、キチンと修正しますのでご安心を・・・
何にせよ、これで入力フォームが表示できましたね。

Add update action

入力した内容で更新するようにupdateアクションを実装します。
ファイル: web/controllers/user_controller.ex
以下のアクション関数を追加。
def update(conn, %{"id" => id, "user" => user_params}) do
  user = Repo.get(SampleApp.User, id)
  changeset = SampleApp.User.changeset(user, user_params)

  if changeset.valid? do
    Repo.update(changeset)

    conn
    |> put_flash(:info, "User updated successfully!!")
    |> redirect(to: user_path(conn, :show, id))
  else
    conn
    |> put_flash(:error, "UserProfile updated is failed!! name or email or password is incorrect.")
    |> redirect(to: user_path(conn, :edit, id))
  end
end
ファイル: web/models/user.ex
before_updateの追加。
(これがないとパスワードが更新されない)
before_update :set_password_digest
Description:
before_insertと動作は同じ。
こちらはupdate時に動作する。

Sharing input form template

今回の最後は入力フォーム部分を共通テンプレートにすることです。
フォームテンプレートを作成します。
ファイル: web/templates/user/input_form.html.eex
<%= form_for @changeset, @action, fn f -> %>
  <%= if f.errors != [] do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below:</p>
      <ul>
        <%= for {attr, message} <- f.errors do %>
          <li><%= humanize(attr) %> <%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="form-group">
    <label>Name</label>
    <%= text_input f, :name, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Email</label>
    <%= text_input f, :email, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Password</label>
    <%= text_input f, :password, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>
ファイル: web/templates/user/new.html.eex
<h1>Sign up</h1>

<%= render "input_form.html", changeset: @changeset,
                        action: user_path(@conn, :create) %>
ファイル: web/templates/user/edit.html.eex
<h2>Edit UserProfile</h2>

<%= render "input_form.html", changeset: @changeset,
                        action: user_path(@conn, :update, @user) %>
大分すっきりしました。

Extra

構造体の値を変更する方法・・・
ちょっと詰まったので方法をまとめておく。
iexの起動。
>iex -S mix
iex(1)> alias SampleApp.User
nil
構造体を定義。
(今回はUserModelの構造体を利用)
iex(2)> user = %User{}
%SampleApp.User{__meta__: %Ecto.Schema.Metadata{source: "users", state: :built},
 email: nil, id: nil, inserted_at: nil, name: nil, password: nil,
 password_digest: nil, updated_at: nil}
構造体へのアクセス。
(値は入ってないですが・・・)
iex(3)> user.id
nil
失敗する値の変更。
iex(4)> user[:name] = "hoge"
** (CompileError) iex:5: cannot invoke remote function Access.get/2 inside match
    (elixir) src/elixir_translator.erl:234: :elixir_translator.translate/2
    (elixir) src/elixir_clauses.erl:26: :elixir_clauses.match/3
    (elixir) src/elixir_translator.erl:18: :elixir_translator.translate/2
成功する値の変更。
しかしこれだと、nameに値を入れて再定義しているのと変わらない・・・
(他に値がある状態でこれをやるとnameにしか値が入らない。他はnil)
iex(4)> user = %User{name: "hoge"}
%SampleApp.User{__meta__: %Ecto.Schema.Metadata{source: "users", state: :built},
 email: nil, id: nil, inserted_at: nil, name: "hoge", password: nil,
 password_digest: nil, updated_at: nil}
次の方法で行えば部分的に値を変更できる。
(Map.put/3を利用する)
iex(5)> user = Map.put(user, :password, "hogehoge")
%SampleApp.User{__meta__: %Ecto.Schema.Metadata{source: "users", state: :built},
 email: nil, id: nil, inserted_at: nil, name: "hoge", password: "hogehoge",
 password_digest: nil, updated_at: nil}
大した内容ではないですが・・・

Speaking to oneself

さて、私の経験値/知識不足で余計な変更を強いてしまいました。
本当にすいません。
次の記事では、今回実装した更新処理に対して、
edit、updateへの画面遷移前に認可処理を実行するようにします。
少々、面倒くさい内容ですが、
懲りずに付き合ってくだせー。

Bibliography

人気の投稿