📜 ⬆️ ⬇️

Creating a blog engine with Phoenix and Elixir / Part 4. Add processing roles in controllers



From the translator: “ Elixir and Phoenix are a great example of where modern web development is heading. Already, these tools provide high-quality access to real-time technologies for web applications. Sites with increased interactivity, multiplayer browser games, microservices - those areas in which these technologies will serve a good service. The following is a translation of a series of 11 articles that describe in detail the aspects of development on the Phoenix framework that would seem such a trivial thing as a blog engine. But do not hurry to sulk, it will be really interesting, especially if the articles encourage you to pay attention to the Elixir or become its followers.

In this section, we will end the distinction of access rights using roles. The key point of this series of articles is that a lot of attention is paid to tests, and tests are great!
')
At the moment, our application is based on:


Where we stayed


Last time, we parted with you by adding the concept of a role into models and creating support functions for tests to make your life a little easier. Now we need to add role-based constraints inside the controllers. Let's start by creating an auxiliary function that we can use in any controller.

Creating an auxiliary function for checking roles


The first step for today is to create a simple user check for administrator rights. To do this, create a file web/models/role_checker.ex and fill it with the following code:

 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 

Also let's write some tests to cover this functionality. Open the file 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 

In the first test we create an administrator, and in the second we create a regular user. And at the end, check that the function is_admin? returns true for the first and false for the second. So how is the is_admin? function is_admin? from the RoleChecker module requires a user, we can write a very simple test to check the performance. It turns out the code in which we can be sure! We run the tests and make sure they stay green.

We allow adding users only to the administrator.


Previously, we did not add any restrictions to the UserController , so now is the time to connect the authorize_user . Let's quickly plan what we will do now. We will allow users to edit, update and delete their own profiles, but only administrators can add new users.

Under the scrub_params line in the web/controllers/user_controller.ex add the following:

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

And at the bottom of the file, add some private functions for processing user authorization and administrator authorization:

 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 

The authorize_user call is essentially identical to what we had in PostController , except for checking RoleChecker.is_admin? .

The authorize_admin function is even simpler. We only verify that the current user is an administrator.

Let's test/controllers/user_controller_test.exs back to the file test/controllers/user_controller_test.exs and change our tests so that they take into account the new conditions.

Let's start by changing the setup block.

 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 

Create a user role, an administrator, a regular user and an administrator within it, and then return them. Thereby we will be able to use them in tests through pattern matching. We will also need a helper function to log in, so copy the login_user function from PostController .

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

We do not add any restrictions to the index action, so we can skip this test. The next test, the "renders form for new resources" (representing the action of new ), the restriction is imposed. The user must have administrator rights.

Modify the test to match the following code:

 @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 

Add a line @tag admin: true over this test to mark it as an admin. Thus, we can run only similar tests instead of the entire set. Let's try:

 mix test --only admin 

In the output we get the error:

 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 

The problem here is that we are not passing the full user model to the RoleChecker.is_admin? function RoleChecker.is_admin? . And we transfer a small subset of data received by the current_user function from the sign_in function of the sign_in module.

Let's add the role_id to them as well. I made changes to the web/controllers/session_controller.ex file as shown below:

 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 

Now once again try running tests with the admin tag.

 $ mix test --only admin 

Green again! Now we need to create tests for the reverse situation, when the user is not an administrator, but at the same time tries to enter the action of the new UserController controller. Go back to the file 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 

And do the same for the create action. Create one test for both cases.

 @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 

We can skip the show action, because we have not added any new conditions to it. We will act on the same template until the user_controller_test.exs file looks like:

 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 

Run the entire test suite. They all pass again!

We allow the administrator to change any posts.


Fortunately, we have already done most of the work and only this last piece remains. After we finish with it, the administrator's functionality will be completely ready. Let's open the web/controllers/post_controller.ex and change the authorize_user function to use the RoleChecker.is_admin? auxiliary function RoleChecker.is_admin? . If the user is an administrator, then we will give him full control over the change of user posts.

 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 open the file test/controllers/post_controller_test.exs and add a few more tests to cover the authorization rules:

 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 

Right now, our blogging engine as a whole works without a glitch, but there are a few bugs. Maybe they appeared by mistake. or we forgot something along the way. So let's identify and eliminate them. Also let's update all dependencies so that the application is launched on the latest version of all that is possible.

Adding a new user gives an error about missing roles


This was noticed by nolotus on the Pxblog page ( https://github.com/Diamond/pxblog). Thank you!

In the part_3 branch part_3 attempts to create a new user will result in an error due to the lack of a role (since we made the presence of role_id when creating the user). Let's first examine the problem, and only then begin to fix it. When we log in as an administrator, go to the address /users/new , fill in all the fields and click on the button, we get the following error:


Which happens because we require the user to enter a name, email, password, password confirmation. But do not say anything about the role. Now, knowing this, let's proceed to the decision. We begin by submitting a list of possible roles to choose from on the form.

We will need it in each of the actions: new, create, edit update . Add alias Pxblog.Role top of UserController ( web/controllers/user_controller.ex ) if this is not yet there. Then we will make changes to all previously listed actions:

 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 

Please note that for each of them we have selected all the roles with the help of Repo.all(Role) and added them to the assigns list, which we pass to the view (including in case of an error).

We also need to implement a drop-down list using auxiliary functions for forms from Phoenix.Html . So let's see how this is done in the documentation :

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

Drop-down lists expect as an argument values either a regular list (in the format of [value, value, value] ) or a list of keywords (in the format of [displayed: value, displayed: value] ). In our case, we need to display the names of the roles and at the same time pass the value of the identifier of the selected role when sending the form. We cannot just blindly throw the @roles variable into an auxiliary function, because it does not fit into any of the above formats. So let's write a function in View that will simplify our task.

 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 

We added the roles_for_select function, which simply accepts a collection of roles. Let's look at line by line what this function does. Let's start with the collection of roles, which we pass to the following function along the chain:

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

Again I remind you that &/&1 is an abbreviation for anonymous functions, which can be rewritten in its full version as follows:

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

We launched the map operation to return a list of smaller key lists, where the name of the role is the key and the identifier of the role is the value.

Suppose given some initial value for roles:

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

In this case, a call to the map function would return the following list:

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

Which we then pass to the last function List.flatten , which removes the extra nesting. So our final result:

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

It so happens that this is the required format for the auxiliary function of the drop-down list! So far, we can’t pat ourselves, because we still need to change the templates in the 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) %> 

And in the 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) %> 

And finally, I think you will not refuse to add our new helper function to the web/templates/user/form.html.eex . As a result, a drop-down list will appear in the form, including all possible roles for the user to transfer. Add the following code to the Submit button:

 <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> 

Now, if you try to add a new user or edit an existing user, you will be able to assign the role to this person! The last bug left!

Loading initial data duplicates them several times


Right now, if we load our initial data several times, we get a duplicate, which is an error. Let's write a couple of helper anonymous functions 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) 

Note the addition of the alias Repo , Role and User . We also import the from function from the Ecto.Query module to use the convenient query syntax. Then take a look at the anonymous function find_or_create_role . The function itself simply accepts the name of the role and the admin flag as arguments.

Based on these criteria, we perform a query using Repo.all (note the ^ sign following each variable inside the where clause, because we want to compare values ​​instead of matching to the sample). And throw the result in the case statement. If Repo.all did not find anything, we will get back an empty list, therefore, we need to add a role. Otherwise, we assume that the role already exists and proceed to load the rest of the file. The find_or_create_user function does the same thing but uses different criteria.

Finally, we call each of these functions (note the mandatory for anonymous functions point between their name and arguments!). To create an administrator, we need to reuse his role. That is why we do not admin_role name admin_role with an underscore. Later, we may want to use user_role or admin_user for further use in the initial data file, but for the time being we will leave this code alone by referring to the underscore. This will make the raw data file look tidy and clean. Now everything is ready to load the initial data:

 $ 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/Li”, 2, {{2015, 11, 6}, {19, 35, 49, 0}}, “admin”] OK query=1.2ms [debug] COMMIT [] OK query=1.1ms 

When we load them for the first time, we see a stack of structures INSERT. Awesome! To be completely sure that everything works as it should, let's try to load them again and make sure that there are no insert operations:

 $ 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 

Sumptuously!Everything works and works quite reliably! Plus, no one will cancel the pleasure that we got from writing our own useful features for Ecto!

Errors about duplicate administrators in tests


Now, if at some point, you drop the test database, you get an error saying "User already exists . " I suggest a simple (and temporary) way to fix this. Open the file test/support/test_helper.exand change the function 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 


What have we come to?


Now we have completely green tests, as well as users, posts and roles. We implemented workable restrictions on user registrations, changes to users and posts. And added some useful helper functions. In further posts, we will devote some time to adding new cool features to our blog engine!

Conclusion from the Wuns


Every week we are getting more and more, and this is good news! Friends, thank you very much for the interest in the community and the expressed trust. We try to justify it and post new interesting material several times a week. Therefore, if you still have not subscribed to the Russian-language newsletter about Elixir , then do not waste time. Subscribe now and tomorrow you will receive a new exclusive article! We work literally all night long, especially for you.

I also remind you that we are holding a contest with a new crisp book Programming Elixir from Dave Thomas as a prize. Take part, win is not so difficult!

Also, do not forget to put the pros and send the article to your friends if you like it. Or if you like our activities. After all, the sooner we collect the critical mass of users, the sooner we can launch a full-fledged version of the site, covering most of the questions about the beautiful language of Elixir.

Other series articles


  1. Introduction
  2. Authorization
  3. Adding Roles
  4. Process roles in controllers
  5. We connect ExMachina
  6. Markdown support
  7. Add comments
  8. We finish with comments
  9. Channels
  10. Channel testing
  11. Conclusion


Good luck in learning, stay with us!

Source: https://habr.com/ru/post/316368/


All Articles