Создание движка для блога с помощью 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 action in [:new, :create] plug :authorize_user when action in [: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, чтобы вернуть список из более маленьких ключевых списков, где название роли — ключ, а идентификатор роли — значение.

Предположим, дано некое начальное значение для ролей:

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 in Role, 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.7ms queue=2.8ms [debug] BEGIN [] OK query=0.2ms [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.8ms [debug] COMMIT [] OK query=0.4ms [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.4ms [debug] BEGIN [] OK query=0.2ms [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.4ms [debug] COMMIT [] OK query=0.3ms [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.7ms [debug] BEGIN [] OK query=0.3ms [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.2ms [debug] COMMIT [] OK query=1.1ms

Когда мы загружаем их впервые, то видим пачку конструкций 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.8ms queue=3.6ms 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.6ms 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.8ms

Великолепно! Всё работает и работает довольно надёжно! Плюс никто не отменит того удовольствия, что мы получили от написания наших собственных полезных функций для 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. Скоро…

Успехов в изучении, оставайтесь с нами!
ссылка на оригинал статьи https://habrahabr.ru/post/316368/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *