Створення движка для блогу з допомогою Phoenix і Elixir / Частина 4. Додаємо обробку ролей в контролерах



Від перекладача: «Elixir і Phoenix — прекрасний приклад того, куди рухається сучасна веб-розробка. Вже зараз ці інструменти надають якісний доступ до технологій реального часу для веб-додатків. Сайти з підвищеною інтерактивністю, багатокористувацькі браузерні ігри, микросервисы — ті напрямки, в яких дані технології послужать хорошу службу. Далі представлений переклад серії з 11 статей, докладно описують аспекти розробки на фреймворку Фенікс здавалося б такий тривіальної речі, як блоговый движок. Але не поспішайте кукситься, буде дійсно цікаво, особливо якщо статті спонукають вас звернути увагу на Еліксир або стати його послідовниками.

У цій частині ми закінчимо розмежування прав доступу з використанням ролей. Ключовий момент цієї серії статей — тут дуже багато уваги приділяється тестів, а тести — це здорово!


На даний момент наше додаток засноване на:

  • Elixir: v1.3.1
  • Phoenix: v1.2.0
  • Ecto: v2.0.2
  • Comeonin: v2.5.2
Де ми зупинилися
Минулого разу ми розлучилися з вами на додавання поняття ролі всередину моделей і створення допоміжних функцій для тестів, щоб трохи полегшити собі життя. Тепер нам потрібно додати всередину контролерів засновані на ролях обмеження. Почнемо з створення допоміжної функції, яку ми зможемо використовувати в будь-якому контроллері.

Створення допоміжної функції для перевірки ролей
Першим кроком на сьогодні стане створення простої перевірки користувача на наявність прав адміністратора. Для цього створіть файл
web/models/role_checker.ex
і заповніть його наступним кодом:

defmodule Pxblog.RoleChecker do
alias Pxblog.Repo
alias Pxblog.Role

def is_admin?(user) do
(role = Repo.get(Role, user.role_id)) && role.admin
end
end

Також давайте напишемо декілька тестів для покриття цієї функціональності. Відкрийте файл
test/models/role_checker_test.exs
:

defmodule Pxblog.RoleCheckerTest do
use Pxblog.ModelCase
alias Pxblog.TestHelper
alias Pxblog.RoleChecker

test "is_admin? is true when user has an admin role" do
{:ok, role} = TestHelper.create_role(%{name: "Admin", admin: true})
{:ok, user} = TestHelper.create_user(role, %{email: "test@test.com", username: "user", password: "test", password_confirmation: "test"})
assert RoleChecker.is_admin?(user)
end

test "is_admin? is false when user does not have an admin role" do
{:ok, role} = TestHelper.create_role(%{name: "User", admin: false})
{:ok, user} = TestHelper.create_user(role, %{email: "test@test.com", username: "user", password: "test", password_confirmation: "test"})
refute RoleChecker.is_admin?(user)
end
end

У першому тесті ми створюємо адміністратора, а в другому звичайного користувача. А в кінці перевіряємо, що функція
is_admin?
повертає
true
для першого і
false
для другого. Так як функція
is_admin?
з модуля
RoleChecker
вимагає наявність користувача, ми можемо написати дуже простий тест, щоб перевірити працездатність. Виходить код, в якому ми можемо бути впевнені! Запускаємо тести і переконуємося, що вони залишаються зеленими.

Дозволяємо додавати користувачів лише адміністратору
Раніше ми не додавали ніяких обмежень в
UserController
, так що зараз саме час підключити плаг
authorize_user
. Давайте швиденько сплануємо, що ж зараз будемо робити. Ми дозволимо користувачам редагувати, оновлювати і видаляти їх власні профілі, але додавати нових користувачів зможуть тільки адміністраторам.

Під рядком
scrub_params
у файлі
web/controllers/user_controller.ex
додамо наступне:

plug :authorize_admin when in action [:new, :create]
plug :authorize_user when in action [:edit, update, :delete]

І внизу файлу додамо кілька приватних функцій для обробки авторизації користувачів і авторизації адміністраторів:

defp authorize_user(conn, _) do
user = get_session(conn, :current_user)
if user && (Integer.to_string(user.id) == conn.params["id"] || Pxblog.RoleChecker.is_admin?(user)) do
conn
else
conn
|> put_flash(:error, "You are not authorized to modify that user!")
|> redirect(to: page_path(conn, :index))
|> halt()
end
end

defp authorize_admin(conn, _) do
user = get_session(conn, :current_user)
if user && Pxblog.RoleChecker.is_admin?(user) do
conn
else
conn
|> put_flash(:error, "You are not authorized to create new users!")
|> redirect(to: page_path(conn, :index))
|> halt()
end
end

Виклик
authorize_user
по суті ідентичний тому, що у нас було в
PostController
за винятком перевірки
RoleChecker.is_admin?
.

Функція
authorize_admin
ще простіше. Ми перевіряємо лише, що поточний користувач є адміністратором.

Повернемося до файлу
test/controllers/user_controller_test.exs
і змінимо наші тести так, щоб вони враховували нові умови.

Почнемо з зміни блоку setup.

setup do
{:ok, user_role} = TestHelper.create_role(%{name: "user", admin: false})
{:ok, nonadmin_user} = TestHelper.create_user(user_role, %{email: "nonadmin@test.com", username: "nonadmin", password: "test", password_confirmation: "test"})

{:ok, admin_role} = TestHelper.create_role(%{name: "admin", admin: true})
{:ok, admin_user} = TestHelper.create_user(admin_role, %{email: "admin@test.com", username: "admin", password: "test", password_confirmation: "test"})

{:ok, conn: build_conn(), admin_role: admin_role, user_role: user_role, nonadmin_user: nonadmin_user, admin_user: admin_user}
end

Створіть всередині нього роль, роль адміністратора, звичайного користувача і адміністратора, після чого поверніть їх. Тим самим ми отримаємо можливість користуватися ними в тестах через зіставлення із зразком. Нам також знадобиться допоміжна функція для входу, так що скопіюйте функцію
login_user
PostController
.

defp login_user(conn, user) do
post conn, session_path(conn, :create), user: %{username: user.username, password: user.password}
end

Ми не додаємо жодних обмежень дії
index
, тому можемо пропустити цей тест. На наступний тест «renders form for new resources» (представляє дію
new
) обмеження накладається. Користувач повинен мати права адміністратора.

Змініть тест, щоб він відповідав наступним кодом:

@tag admin: true
test "renders form for new resources", %{conn: conn, admin_user: admin_user} do
conn = conn
|> login_user(admin_user)
|> get(user_path(conn, :new))
assert html_response(conn, 200) =~ "New user"
end

Додайте рядок
@tag admin: true
над цим тестом, щоб позначити його як адміністраторського. Таким чином ми зможемо запускати тільки подібні тести замість набору. Давайте спробуємо:

mix test --only admin

У висновку отримуємо помилку:

1) test renders form for new resources (Pxblog.UserControllerTest)
test/controllers/user_controller_test.exs:26
** (KeyError) key :role_id not found in: %{id: 348 username: "admin"}
stacktrace:
(pxblog) web/models/role_checker.ex:6: Pxblog.RoleChecker.is_admin?/1
(pxblog) web/controllers/user_controller.ex:84: Pxblog.UserController.authorize_admin/2
(pxblog) web/controllers/user_controller.ex:1: Pxblog.UserController.phoenix_controller_pipeline/2
(pxblog) lib/phoenix/router.ex:255: Pxblog.Router.dispatch/2
(pxblog) web/router.ex:1: Pxblog.Router.do_call/2
(pxblog) lib/pxblog/endpoint.ex:1: Pxblog.Endpoint.phoenix_pipeline/1
(pxblog) lib/phoenix/endpoint/render_errors.ex:34: Pxblog.Endpoint.call/2
(phoenix) lib/phoenix/test/conn_test.ex:193: Phoenix.ConnTest.dispatch/5
test/controllers/user_controller_test.exs:28

Проблема тут в тому, що ми не передаємо повну модель користувача функцію
RoleChecker.is_admin?
. А передаємо невелику підмножину даних, що отримується функцією
current_user
з функції
sign_in
модуля
SessionController
.

Давайте додамо до них також і
role_id
. Я внесла зміни в файл
web/controllers/session_controller.ex
, як показано нижче:

defp sign_in(user, password, conn) do
if checkpw(password, user.password_digest) do
conn
|> put_session(:current_user, %{id: user.id username: user.username, role_id: user.role_id})
|> put_flash(:info, "Sign in successful!")
|> redirect(to: page_path(conn, :index))
else
failed_login(conn)
end
end

Тепер ще раз спробуємо запустити тести з тегом
admin
.

$ mix test --only admin

Знову зелені! Тепер нам потрібно створити тести для зворотної ситуації, коли ви не є адміністратором, але при цьому намагається зайти на дію new контролера
UserController
. Повертаємося до файлу
test/controllers/user_controller_test.exs
:

@tag admin: true
test "redirects from new form when not admin", %{conn: conn, nonadmin_user: nonadmin_user} do
conn = login_user(conn, nonadmin_user)
conn = get conn, user_path(conn, :new)
assert get_flash(conn, :error) == "You are not authorized to create new users!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end

І зробимо те ж саме для дії
create
. Створимо по одному тесту для обох випадків.

@tag admin: true
test "creates resource and redirects when data is valid", %{conn: conn, user_role: user_role, admin_user: admin_user} do
conn = login_user(conn, admin_user)
conn = post conn, user_path(conn, :create), user: valid_create_attrs(user_role)
assert redirected_to(conn) == user_path(conn, :index)
assert Repo.get_by(User, @valid_attrs)
end

@tag admin: true
test "redirects from creating user when not admin", %{conn: conn, user_role: user_role, nonadmin_user: nonadmin_user} do
conn = login_user(conn, nonadmin_user)
conn = post conn, user_path(conn, :create), user: valid_create_attrs(user_role)
assert get_flash(conn, :error) == "You are not authorized to create new users!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end

@tag admin: true
test "does not create resource and renders errors when data is invalid", %{conn: conn, admin_user: admin_user} do
conn = login_user(conn, admin_user)
conn = post conn, user_path(conn, :create), user: @invalid_attrs
assert html_response(conn, 200) =~ "New user"
end

Ми можемо пропустити дію
show
, тому що ми не додали йому ніяких нових умов. Ми будемо діяти за таким же шаблоном до тих пір, поки файл
user_controller_test.exs
не стане схожий на:

defmodule Pxblog.UserControllerTest do
use Pxblog.ConnCase
alias Pxblog.User
alias Pxblog.TestHelper

@valid_create_attrs %{email: "test@test.com", username: "test", password: "test", password_confirmation: "test"}
@valid_attrs %{email: "test@test.com", username: "test"}
@invalid_attrs %{}

setup do
{:ok, user_role} = TestHelper.create_role(%{name: "user", admin: false})
{:ok, nonadmin_user} = TestHelper.create_user(user_role, %{email: "nonadmin@test.com", username: "nonadmin", password: "test", password_confirmation: "test"})

{:ok, admin_role} = TestHelper.create_role(%{name: "admin", admin: true})
{:ok, admin_user} = TestHelper.create_user(admin_role, %{email: "admin@test.com", username: "admin", password: "test", password_confirmation: "test"})

{:ok, conn: build_conn(), admin_role: admin_role, user_role: user_role, nonadmin_user: nonadmin_user, admin_user: admin_user}
end

defp valid_create_attrs(role) do
Map.put(@valid_create_attrs, :role_id, role.id)
end

defp login_user(conn, user) do
post conn, session_path(conn, :create), user: %{username: user.username, password: user.password}
end

test "lists all entries on index", %{conn: conn} do
conn = get conn, user_path(conn, :index)
assert html_response(conn, 200) =~ "Listing users"
end

@tag admin: true
test "renders form for new resources", %{conn: conn, admin_user: admin_user} do
conn = login_user(conn, admin_user)
conn = get conn, user_path(conn, :new)
assert html_response(conn, 200) =~ "New user"
end

@tag admin: true
test "redirects from new form when not admin", %{conn: conn, nonadmin_user: nonadmin_user} do
conn = login_user(conn, nonadmin_user)
conn = get conn, user_path(conn, :new)
assert get_flash(conn, :error) == "You are not authorized to create new users!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end

@tag admin: true
test "creates resource and redirects when data is valid", %{conn: conn, user_role: user_role, admin_user: admin_user} do
conn = login_user(conn, admin_user)
conn = post conn, user_path(conn, :create), user: valid_create_attrs(user_role)
assert redirected_to(conn) == user_path(conn, :index)
assert Repo.get_by(User, @valid_attrs)
end

@tag admin: true
test "redirects from creating user when not admin", %{conn: conn, user_role: user_role, nonadmin_user: nonadmin_user} do
conn = login_user(conn, nonadmin_user)
conn = post conn, user_path(conn, :create), user: valid_create_attrs(user_role)
assert get_flash(conn, :error) == "You are not authorized to create new users!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end

@tag admin: true
test "does not create resource and renders errors when data is invalid", %{conn: conn, admin_user: admin_user} do
conn = login_user(conn, admin_user)
conn = post conn, user_path(conn, :create), user: @invalid_attrs
assert html_response(conn, 200) =~ "New user"
end

test "shows chosen resource", %{conn: conn} do
user = Repo.insert! %User{}
conn = get conn, user_path(conn, :show user)
assert html_response(conn, 200) =~ "Show user"
end

test "renders page not found when id is nonexistent", %{conn: conn} do
assert_error_sent 404, fn ->
get conn, user_path(conn, :show, -1)
end
end

@tag admin: true
test "renders form for editing chosen resource when logged in as that user", %{conn: conn, nonadmin_user: nonadmin_user} do
conn = login_user(conn, nonadmin_user)
conn = get conn, user_path(conn, :edit, nonadmin_user)
assert html_response(conn, 200) =~ "Edit user"
end

@tag admin: true
test "renders form for editing chosen resource when logged in as an admin", %{conn: conn, admin_user: admin_user, nonadmin_user: nonadmin_user} do
conn = login_user(conn, admin_user)
conn = get conn, user_path(conn, :edit, nonadmin_user)
assert html_response(conn, 200) =~ "Edit user"
end

@tag admin: true
test "redirects away from editing when logged in as a different user", %{conn: conn, nonadmin_user: nonadmin_user, admin_user: admin_user} do
conn = login_user(conn, nonadmin_user)
conn = get conn, user_path(conn, :edit, admin_user)
assert get_flash(conn, :error) == "You are not authorized to modify that user!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end

@tag admin: true
test "updates chosen resource and redirects when data is valid when logged in as that user", %{conn: conn, nonadmin_user: nonadmin_user} do
conn = login_user(conn, nonadmin_user)
conn = put conn, user_path(conn, update, nonadmin_user), user: @valid_create_attrs
assert redirected_to(conn) == user_path(conn, :show, nonadmin_user)
assert Repo.get_by(User, @valid_attrs)
end

@tag admin: true
test "updates chosen resource and redirects when data is valid when logged in as an admin", %{conn: conn, admin_user: admin_user} do
conn = login_user(conn, admin_user)
conn = put conn, user_path(conn, update, admin_user), user: @valid_create_attrs
assert redirected_to(conn) == user_path(conn, :show, admin_user)
assert Repo.get_by(User, @valid_attrs)
end

@tag admin: true
test "does not update chosen resource when logged in as different user", %{conn: conn, nonadmin_user: nonadmin_user, admin_user: admin_user} do
conn = login_user(conn, nonadmin_user)
conn = put conn, user_path(conn, update, admin_user), user: @valid_create_attrs
assert get_flash(conn, :error) == "You are not authorized to modify that user!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end

@tag admin: true
test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, nonadmin_user: nonadmin_user} do
conn = login_user(conn, nonadmin_user)
conn = put conn, user_path(conn, update, nonadmin_user), user: @invalid_attrs
assert html_response(conn, 200) =~ "Edit user"
end

@tag admin: true
test "deletes chosen resource when logged in as that user", %{conn: conn, user_role: user_role} do
{:ok, user} = TestHelper.create_user(user_role, @valid_create_attrs)
conn =
login_user(conn, user)
|> delete(user_path(conn, :delete user))
assert redirected_to(conn) == user_path(conn, :index)
refute Repo.get(User user.id)
end

@tag admin: true
test "deletes chosen resource when logged in as an admin", %{conn: conn, user_role: user_role, admin_user: admin_user} do
{:ok, user} = TestHelper.create_user(user_role, @valid_create_attrs)
conn =
login_user(conn, admin_user)
|> delete(user_path(conn, :delete user))
assert redirected_to(conn) == user_path(conn, :index)
refute Repo.get(User user.id)
end

@tag admin: true
test "redirects away from deleting chosen resource when logged in as a different user", %{conn: conn, user_role: user_role, nonadmin_user: nonadmin_user} do
{:ok, user} = TestHelper.create_user(user_role, @valid_create_attrs)
conn =
login_user(conn, nonadmin_user)
|> delete(user_path(conn, :delete user))
assert get_flash(conn, :error) == "You are not authorized to modify that user!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end
end

Запускаємо весь набір тестів. Всі вони знову проходять!

Дозволяємо адміністратору змінювати будь-які пости
На щастя, ми вже зробили більшу частину роботи і залишився тільки цей останній шматочок. Після того, як ми закінчимо з ним, функціональність адміністратора буде повністю готова. Давайте відкриємо файл
web/controllers/post_controller.ex
і змінимо функцію
authorize_user
, щоб вона теж використовувала допоміжну функцію
RoleChecker.is_admin?
. Якщо користувач є адміністратором, то дамо йому повний контроль над зміною постів користувачів.

defp authorize_user(conn, _) do
user = get_session(conn, :current_user)
if user && (Integer.to_string(user.id) == conn.params["user_id"] || Pxblog.RoleChecker.is_admin?(user)) do
conn
else
conn
|> put_flash(:error, "You are not authorized to modify that post!")
|> redirect(to: page_path(conn, :index))
|> halt()
end
end

На завершення відкриємо файл
test/controllers/post_controller_test.exs
і додамо ще декілька тестів для покриття правил авторизації:

test "redirects when trying to delete a post for a different user", %{conn: conn, role: role, post: post} do
{:ok, other_user} = TestHelper.create_user(role, %{email: "test2@test.com", username: "test2", password: "test", password_confirmation: "test"})
conn = delete conn, user_post_path(conn, :delete, other_user, post)
assert get_flash(conn, :error) == "You are not authorized to modify that post!"
assert redirected_to(conn) == page_path(conn, :index)
assert conn.halted
end

test "renders form for editing chosen resource when logged in as admin", %{conn: conn, user: user, post: post} do
{:ok, role} = TestHelper.create_role(%{name: "Admin", admin: true})
{:ok admin} = TestHelper.create_user(role, %{username: "admin", email: "admin@test.com", password: "test", password_confirmation: "test"})
conn =
login_user(conn, admin)
|> get(user_post_path(conn, :edit user, post))
assert html_response(conn, 200) =~ "Edit post"
end

test "updates chosen resource and redirects when data is valid when logged in as admin", %{conn: conn, user: user, post: post} do
{:ok, role} = TestHelper.create_role(%{name: "Admin", admin: true})
{:ok admin} = TestHelper.create_user(role, %{username: "admin", email: "admin@test.com", password: "test", password_confirmation: "test"})
conn =
login_user(conn, admin)
|> put(user_post_path(conn, update, user, post), post: @valid_attrs)
assert redirected_to(conn) == user_post_path(conn, :show user, post)
assert Repo.get_by(Post @valid_attrs)
end

test "does not update chosen resource and renders errors when data is invalid when logged in as admin", %{conn: conn, user: user, post: post} do
{:ok, role} = TestHelper.create_role(%{name: "Admin", admin: true})
{:ok admin} = TestHelper.create_user(role, %{username: "admin", email: "admin@test.com", password: "test", password_confirmation: "test"})
conn =
login_user(conn, admin)
|> put(user_post_path(conn, update, user, post), post: %{"body" => nil})
assert html_response(conn, 200) =~ "Edit post"
end

test "deletes chosen resource when logged in as admin", %{conn: conn, user: user, post: post} do
{:ok, role} = TestHelper.create_role(%{name: "Admin", admin: true})
{:ok admin} = TestHelper.create_user(role, %{username: "admin", email: "admin@test.com", password: "test", password_confirmation: "test"})
conn =
login_user(conn, admin)
|> delete(user_post_path(conn, :delete user, post))
assert redirected_to(conn) == user_post_path(conn, :index, user)
refute Repo.get(Post, post.id)
end

Прямо зараз наш блоговый движок в цілому працює без збоїв, але є кілька багів. Може бути вони з'явилися за помилки. або ми щось забули по дорозі. Тому давайте виявимо і усунемо їх. Також давайте поновимо всі залежності, щоб програму було запущено на самій останній версії всього чого можливо.

Додавання нового користувача видає помилку про відсутніх ролях
На це звернув увагу nolotus на сторінці Pxblog https://github.com/Diamond/pxblog). Спасибі тобі!

В гілці
part_3
спроби створити нового користувача будуть призводити до помилки через відсутність ролі (так як ми зробили обов'язковою наявність
role_id
при створенні користувача). Давайте для початку вивчимо проблему, а тільки потім почнемо її виправляти. Коли ми зайшовши в якості адміністратора, переходимо за адресою
/users/new
, заповнюємо всі поля і натискаємо на кнопку, то отримуємо наступну помилку:


Яка відбувається тому, що ми вимагаємо від користувача введення імені, електронної пошти, пароль, підтвердження пароля. Але нічого не говоримо з приводу ролі. Тепер, знаючи про це, приступимо до виконання. Почнемо з передачі списку можливих ролей для вибору у формі.

Він знадобиться нам в кожному з дій:
new, create, edit або update
. Додайте
alias Pxblog.Role
наверх UserController (файл
web/controllers/user_controller.ex
) якщо цього поки що ще немає. Потім внесемо зміни у всі раніше перераховані дії:

def new(conn, _params) do
roles = Repo.all(Role)
changeset = User.changeset(%User{})
render(conn, "new.html", changeset: changeset, roles: roles)
end
def edit(conn, %{"id" => id}) do
roles = Repo.all(Role)
user = Repo.get!(User id)
changeset = User.changeset(user)
render(conn, "edit.html", user: user, changeset: changeset, roles: roles)
end
def create(conn, %{"user" => user_params}) do
roles = Repo.all(Role)
changeset = User.changeset(%User{}, user_params)

case Repo.insert(changeset) do
{:ok, _user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: user_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset, roles: roles)
end
end
def update(conn, %{"id" => id, "user" => user_params}) do
roles = Repo.all(Role)
user = Repo.get!(User id)
changeset = User.changeset(user, user_params)

case Repo.update(changeset) do
{:ok, user} ->
conn
|> put_flash(:info, "User updated successfully.")
|> redirect(to: user_path(conn, :show user))
{:error, changeset} ->
render(conn, "edit.html", user: user, changeset: changeset, roles: roles)
end
end

Зверніть увагу, що для кожного з них ми вибрали всі ролі з допомогою
Repo.all(Role)
і додали їх в список
assigns
, який передаємо поданням (в тому числі і у разі помилки).

Нам також потрібно реалізувати випадаючий список, використовуючи допоміжні функції для форм з
Phoenix.Html
. Так що давайте подивимося, як це робиться у документації:

select(form field, values, opts \\ [])
Generates a select tag with the given values.

Випадаючі списки очікують в якості аргументу
values
або звичайний список (у форматі
[value, value, value]
), або список ключових слів (у форматі
[displayed: value, displayed: value]
). В нашому випадку нам потрібно відображати назви ролей і разом з цим передавати значення ідентифікатора обраної ролі при відправці форми. Ми не можемо просто сліпо кидати змінну
@roles
в допоміжну функцію, тому що вона не підходить ні під одну з перерахованих форматів. Так що давайте писати функцію у
View
, яка спростить завдання.

defmodule Pxblog.UserView do
use Pxblog.Web, :view

def roles_for_select(roles) do
roles
|> Enum.map(&["#{&1.name}": &1.id])
|> List.flatten
end
end

Ми додали функцію
roles_for_select
, просто приймаючу колекцію ролей. Давайте порядково розглянемо що ж робить ця функція. Почнемо з колекції ролей, яку передаємо наступної функції по ланцюжку:

Enum.map(&["#{&1.name}": &1.id])

Знову нагадаю, що
&/&1
— скорочення для анонімних функцій, яке можна переписати у повному варіанті так:

Enum.map(roles, fn role -> ["#{role.name}": role.id] end)

Ми запустили операцію
map
, щоб повернути список з більш маленьких ключових списків, де назва ролі — ключ, а роль id — значення.

Припустимо, дано якесь початкове значення для ролей:

roles = [%Role{name: "Admin Role", id: 1}, %Role{name: "User Role", id: 2}]

У цьому випадку, виклик функції map повернув би такий список:

[["Admin Role": 1], ["User Role": 2]]

Потім ми передамо в останню функцію
List.flatten
, убирающую зайву вкладеність. Таким чином наш остаточний результат:

["Admin Role": 1, "User Role": 2]

Так сталося, що це і є необхідний формат для допоміжної функції випадаючого списку! Поки ми не можемо поплескати себе по плечу, адже нам все ще потрібно змінити шаблони у файлі
web/templates/user/new.html.eex
:

<h2>New user</h2>

<%= render "form.html", changeset: @changeset,
action: user_path(@conn, :create),
roles: @roles %>

<%= link "Back", to: user_path(@conn, :index) %>

І у файлі
web/templates/user/edit.html.eex
:

<h2>Edit user</h2>
<%= render "form.html", changeset: @changeset,
action: user_path(@conn, update, @user),
roles: @roles %>
<%= link "Back", to: user_path(@conn, :index) %>

Ну і нарешті, я думаю ви не відмовитеся додати нашу нову допоміжну функцію в файл
web/templates/user/form.html.eex
. Як підсумок, у формі з'явиться випадаючий список, що включає всі можливі для перекладу користувача ролі. Додайте наступний код кнопки Submit:

<div class="form-group">
<%= label f, :role_id, "Role", class: "control-label" %>
<%= select f, :role_id, roles_for_select(@roles), class: "form-control" %>
<%= error_tag f, :role_id %>
</div>

Тепер, якщо ви спробуєте додати нового користувача або відредагувати існуючого, то отримаєте можливість привласнити роль цієї людини! Залишився останній баг!

Завантаження початкових даних кілька разів поспіль дублює їх
Прямо зараз, якщо ми завантажимо наші початкові дані в кілька разів, то отримаємо дублі, що є помилкою. Давайте напишемо декілька допоміжних анонімних функцій
find_or_create
:

alias Pxblog.Repo
alias Pxblog.Role
alias Pxblog.User
import Ecto.Query, only: [from: 2]

find_or_create_role = fn role_name, admin ->
case Repo.all(from r Role in, where: r.name == ^role_name and r.admin == ^admin) do
[] ->
%Role{}
|> Role.changeset(%{name: role_name admin: admin})
|> Repo.insert!()
_ ->
IO.puts "Role: #{role_name} already exists, skipping"
end
end

find_or_create_user = fn username, email, role ->
case Repo.all(from u in User, where: u.username == ^username and u.email == ^email) do
[] ->
%User{}
|> User.changeset(%{username: username, email: email password: "test", password_confirmation: "test", role_id: role.id})
|> Repo.insert!()
_ ->
IO.puts "User: #{username} already exists, skipping"
end
end

_user_role = find_or_create_role.("User Role", false)
admin_role = find_or_create_role.("Admin Role", true)
_admin_user = find_or_create_user.("admin", "admin@test.com", admin_role)

Зверніть увагу на додавання псевдоніма
Repo
,
Role
та
User
. Ми також імпортуємо функцію
from
з модуля
Ecto.Query
, щоб використовувати зручний синтаксис запитів. Потім погляньте на анонімну функцію
find_or_create_role
. Функція сама по собі просто приймає назву ролі і прапор адміністратора в якості аргументів.

Ґрунтуючись на цих критеріях ми виконуємо запит за допомогою
Repo.all
(зверніть увагу на знак ^, наступний за кожної змінної всередині умови
where
, тому що ми хочемо порівняти значення, замість зіставлення зі зразком). І кидаємо результат оператор case. Якщо
Repo.all
нічого не знайшов, ми отримаємо назад порожній список, отже, нам потрібно додамо роль. В іншому випадку ми припускаємо, що роль вже існує і переходимо до завантаження іншого файлу. Функція
find_or_create_user
робить те ж саме, але використовують інші критерії.

Нарешті, ми викликаємо кожну з цих функцій (зверніть увагу на обов'язкову для анонімних функцій точку між їх назвою та аргументами!). Для створення адміністратора, нам потрібно повторно використовувати його роль. Саме тому ми не предваряем назва
admin_role
знаком підкреслення. Пізніше ми можливо захочемо пустити в хід
user_role
або
admin_user
для подальшого використання у файлі початкових даних, але поки залишимо цей код у спокої, звернувшись до знаку підкреслення. Це дозволить файлу початкових даних виглядати охайним і чистим. Тепер все готово до завантаження початкових даних:

$ mix run priv/repo/seeds.exs
[debug] SELECT r0."id", r0."name", r0."admin", r0."inserted_at", r0."updated_at" FROM "roles" AS r0 WHERE ((r0."name" = $1) AND (r0."admin" = $2)) ["User Role", false] OK query=81.7 ms queue=2.8 ms
[debug] BEGIN [] OK query=0.2 ms
[debug] INSERT INTO "roles" ("admin", "inserted_at", "name", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [false, {{2015, 11, 6}, {19, 35, 49, 0}}, "User Role", {{2015, 11, 6}, {19, 35, 49, 0}}] OK query=0.8 ms
[debug] COMMIT [] OK query=0.4 ms
[debug] SELECT r0."id", r0."name", r0."admin", r0."inserted_at", r0."updated_at" FROM "roles" AS r0 WHERE ((r0."name" = $1) AND (r0."admin" = $2)) [Admin Role", true] OK query=0.4 ms
[debug] BEGIN [] OK query=0.2 ms
[debug] INSERT INTO "roles" ("admin", "inserted_at", "name", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [true, {{2015, 11, 6}, {19, 35, 49, 0}}, "Admin Role", {{2015, 11, 6}, {19, 35, 49, 0}}] OK query=0.4 ms
[debug] COMMIT [] OK query=0.3 ms
[debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."role_id", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE ((u0."username" = $1) AND (u0."email" = $2)) ["admin", "admin@test.com"] OK query=0.7 ms
[debug] BEGIN [] OK query=0.3 ms
[debug] INSERT INTO "users" ("email", "inserted_at", "password_digest", "role_id", "updated_at", "username") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" ["admin@test.com", {{2015, 11, 6}, {19, 35, 49, 0}}, "$2b$12$.MuPBUVe/7/9HSOsccJYUOAD5IKEB77Pgz2oTJ/UvTvWYwAGn/L. i", 2, {{2015, 11, 6}, {19, 35, 49, 0}}, "admin"] OK query=1.2 ms
[debug] COMMIT [] OK query=1.1 ms

Коли ми завантажуємо їх вперше, то бачимо пачку конструкцій
INSERT
. Приголомшливо! Щоб бути повністю впевненими, що все працює як треба, давайте спробуємо завантажити їх ще раз переконаємося, що не відбувається ніяких операцій вставок:

$ mix run priv/repo/seeds.exs
Role: User Role already exists, skipping
[debug] SELECT r0."id", r0."name", r0."admin", r0."inserted_at", r0."updated_at" FROM "roles" AS r0 WHERE ((r0."name" = $1) AND (r0."admin" = $2)) ["User Role", false] OK query=104.8 ms queue=3.6 ms
Role: Admin Role already exists, skipping
[debug] SELECT r0."id", r0."name", r0."admin", r0."inserted_at", r0."updated_at" FROM "roles" AS r0 WHERE ((r0."name" = $1) AND (r0."admin" = $2)) [Admin Role", true] OK query=0.6 ms
User: admin already exists, skipping
[debug] SELECT u0."id", u0."username", u0."email", u0."password_digest", u0."role_id", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE ((u0."username" = $1) AND (u0."email" = $2)) ["admin", "admin@test.com"] OK query=0.8 ms

Чудово! Все працює і працює досить надійно! Плюс ніхто не скасує того задоволення, що ми отримали від написання наших власних корисних функцій для
Ecto
!

Помилки про дублювання адміністраторів у тестах
Тепер, якщо в якийсь момент, ви скинете тестову базу даних, то отримаєте помилку, свідчить «Користувач вже існує». Пропоную простий (і тимчасовий) спосіб це виправити. Відкрийте файл
test/support/test_helper.ex
та змініть функцію
create_user
:

def create_user(role, %{email: email, username: username, password: password, password_confirmation: password_confirmation}) do
if user = Repo.get_by(User username: username) do
Repo.delete(user)
end
role
|> build_assoc(:users)
|> User.changeset(%{email: email, username: username, password: password, password_confirmation: password_confirmation})
|> Repo.insert
end


До чого ми прийшли?
Зараз у нас є повністю зелені тести, а також користувачі, посади і ролі. Ми реалізували працездатні обмеження користувальницьких реєстрацій, зміни користувачів і постів. І додали кілька корисних допоміжних функцій. У подальших постах ми присвятимо деякий час додавання нових можливостей класних до нашого блоговому движку!

Висновок від Вуншей
З кожним тижнем стає все більше, а це не може не радувати! Друзі, велике спасибі за інтерес до спільноти і виражене довіру. Намагаємося його виправдовувати і кілька разів на тиждень викладати новий цікавий матеріал. Тому, якщо ви все ще не підписалися на російськомовну розсилку про Elixir, то не втрачайте часу. Підписуйтесь зараз і вже завтра отримаєте нову ексклюзивну статтю! Працюємо буквально ночі безперервно, спеціально для вас.

Ще нагадую, що ми проводимо конкурс з новенькою хрусткою книгою Programming Elixir від Дейва Томаса в якості призу. Приймайте участь, виграти не так складно!

Також не забувайте ставити плюси і пересилати статтю друзям, якщо вона вам сподобалася. Або якщо вам подобається наша діяльність. Адже чим швидше ми з вами зберемо критичну масу користувачів, тим швидше зможемо запустити повноцінну версію сайта, що покриває більшість питань про чарівному мовою Еліксир.

Інші частини:

  1. Вступ
  2. Авторизація
  3. Додавання ролей
  4. Додаємо обробку ролей в контролерах
  5. Скоро...
Успіхів у вивченні, залишайтеся з нами!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.