Writing a Blog Engine in Phoenix and Elixir: Part 4, Adding Roles to our Controllers

Written by diamondgfx | Published 2015/10/15
Tech Story Tags: elixir | phoenix | phoenix-framework | web-development

TLDRvia the TL;DR App

Latest Update: 08/17/2016

Previous Post in this series

Writing a Blog Engine in Phoenix and Elixir: Part 3, Adding Roles to our Models_Latest Update: 07/21/2016_medium.com

Current Versions

As of the time of writing this, the current versions of our applications are:

  • Elixir: v1.3.1
  • Phoenix: v1.2.0
  • Ecto: v2.0.2
  • Comeonin: v2.5.2

If you are reading this and these are not the latest, let me know and I’ll update this tutorial accordingly.

Where We Left Off

When we left off, we had just finished implementing the concept of roles into our models and created some test helpers to make our lives easier, but now we need to get into the tricky bits of implementing restrictions based on roles into our controllers. We’ll start off by creating a helper to give us a function to use in each of our controllers.

Creating a Role Checker Helper

The next thing we need to do is create a simple way to verify if the user creating the other users is indeed an admin. Create web/models/role_checker.ex and we’ll populate it with the following module:

defmodule Pxblog.RoleChecker doalias Pxblog.Repoalias Pxblog.Role

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

We’ll also write some tests to cover this functionality. Open up test/models/role_checker_test.exs:

defmodule Pxblog.RoleCheckerTest douse Pxblog.ModelCasealias Pxblog.TestHelperalias 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)endend

In both tests we create a role and a user; in one we create an admin role and in the next we do not. Finally, we assert that the is_admin? function returns true for the admin user and false for the non-admin user. Because the RoleChecker’s is_admin? function requires you to supply the user, we can write very simple tests to guarantee our functionality. This is code that we can be confident about! Run these tests and verify your test suite is still green.

Restricting User Creation to Admins

In our User Controller, we never wrote any authorize_user plugs to restrict this, so we’re going to add them now. We will design this out quickly to make sure the actions we’re performing make sense. We’ll allow a user to edit, update, and delete their own accounts, but we’ll only allow admins to see the new user form or create new accounts.

Underneath the scrub_params line in 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 we’ll add a few private functions to handle authorizing users and authorizing admins.

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

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

The authorize_user call is basically identical to what we have in our Post Controller, with the exception of the if statement at the top also checking our new RoleChecker.is_admin? call.

authorize_admin is even simpler; we’re just checking that the current user is an admin.

To verify this all works, we’re going to go back to our test/controllers/user_controller_test.exs file and modify our tests to work against these new assumptions.

First, we’ll have to change our setup block to work with these new rules.

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

We create a user role, an admin role, a non-admin user, and an admin user, and then return all of that out to our tests to use in pattern matching. We also need a helper function to log in a user, so we’ll copy our login_user function from our Post Controller.

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

We didn’t attach any restrictions to index, so we can skip that test. The next test is our “renders form for new resources”, which is our new action and DOES have a restriction (must be an admin).

Change that test to the following code:

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

We’re adding a “@tag admin: true” line above our test to tag it as an “admin” test so that we can just run all of our admin tests instead of the full suite. We’ll run just this test with the following command:

mix test --only admin

And in our output we should see gre-uh oh! We’re getting a failure:

  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/5test/controllers/user_controller_test.exs:28

The trouble here is we’re not passing a full user model to RoleChecker.is_admin?; instead, we’re passing the small subset of data that we’re storing in current_user from our Session Controller’s sign_in function. We’ll update that to include the role_id as well. I’ve added the modification in web/controllers/session_controller.ex below:

defp sign_in(user, password, conn) doif checkpw(password, user.password_digest) doconn|> 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))elsefailed_login(conn)endend

And now we’ll run our mix test command targeting only the admin tagged tests.

$ mix test --only admin

Green again! Now, we need to create a negative test for when a user is not an admin but tries to visit the “new” action for Users. Back in test/controllers/user_controller_test.exs:

@tag admin: truetest "redirects from new form when not admin", %{conn: conn, nonadmin_user: nonadmin_user} doconn = 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.haltedend

And we’ll do the same for the create action; creating one valid test and one invalid test.

@tag admin: truetest "creates resource and redirects when data is valid", %{conn: conn, user_role: user_role, admin_user: admin_user} doconn = 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: truetest "redirects from creating user when not admin", %{conn: conn, user_role: user_role, nonadmin_user: nonadmin_user} doconn = 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.haltedend

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

We can skip show, since we didn’t attach any new conditions to it. We’re going to follow this pattern over and over until our finished user_controller___test.exs file looks like this:

defmodule Pxblog.UserControllerTest douse Pxblog.ConnCasealias Pxblog.Useralias 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) doMap.put(@valid_create_attrs, :role_id, role.id)end

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

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

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

@tag admin: truetest "redirects from new form when not admin", %{conn: conn, nonadmin_user: nonadmin_user} doconn = 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.haltedend

@tag admin: truetest "creates resource and redirects when data is valid", %{conn: conn, user_role: user_role, admin_user: admin_user} doconn = 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: truetest "redirects from creating user when not admin", %{conn: conn, user_role: user_role, nonadmin_user: nonadmin_user} doconn = 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.haltedend

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

test "shows chosen resource", %{conn: conn} douser = 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} doassert_error_sent 404, fn ->get conn, user_path(conn, :show, -1)endend

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

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

@tag admin: truetest "redirects away from editing when logged in as a different user", %{conn: conn, nonadmin_user: nonadmin_user, admin_user: admin_user} doconn = 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.haltedend

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

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

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

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

@tag admin: truetest "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: truetest "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: truetest "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.haltedendend

Now, we run our full test suite, and we are all back to green!

Allowing Admins to Modify All Posts

Thankfully, we’ve already done almost all of the work to make this last piece of admin functionality work as expected. We’re going to open up web/controllers/post_controller.ex and modify the authorize_user function to also use our RoleChecker.is_admin? helper function to see if the user is an admin. If they are, then we’re going to give them full control over modifying any user’s posts.

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

Finally, we’ll open up test/controllers/post_controller_test.exs and add some more tests at the bottom covering our 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.haltedend

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 blog engine is humming along but there are a few bugs, whether due to omissions or things I missed along the way, so let’s identify and address a few bugs. We’ll also upgrade the versions of dependencies to make sure that this is running on the latest and greatest of everything that it can!

Adding new users throws an error about missing roles

This was pointed out to me as an issue on the Pxblog github page (https://github.com/Diamond/pxblog) by nolotus (Thank you!). As of the part_3 branch, if you attempt to create a new user, it will fail due to the lack of a specified role (since we did make role_id required for new user creation). Let’s explore the problem, first, and then we’ll start implementing a fix for it. When we log in as an admin, and go to /users/new, after filling everything out we’ll get the following error:

Which makes sense. We require the user to enter username, email, password, and password_confirmation, but nothing about the role. Knowing this is the case, we’ll start by passing the list of possible roles to choose from to our controller.

We’ll start by passing in a list of roles to each of the actions that will need to be able to select them, which in our case means the new, create, edit, and update actions. First, throw an alias Pxblog.Role to the top of your User Controller (web/controllers/user_controller.ex) if it’s not already there. Then, we’ll modify the new, edit, create, and update actions:

def new(conn, _params) doroles = Repo.all(Role)changeset = User.changeset(%User{})render(conn, "new.html", changeset: changeset, roles: roles)end

def edit(conn, %{"id" => id}) doroles = 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}) doroles = 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)endend

def update(conn, %{"id" => id, "user" => user_params}) doroles = 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)endend

Notice for all of these selected all of the roles via Repo.all(Role) and added those to the list of assigns we sent out to the view (including in our render statements in the error cases).

We will also need to implement a select box, so let’s take a look at the documentation for selects using the Phoenix.Html form helpers (taken from https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#select/4):

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

select boxes, for the values argument, requires either a list or a keyword list, either in the form of [value, value, value] or [displayed: value, displayed: value]. In our case, we want to display the role name but have it carry the id value in our form submit. We can’t just blindly throw @roles in there because it doesn’t adhere to either format, so let’s write a function in our view that will make this simpler:

defmodule Pxblog.UserView douse Pxblog.Web, :view

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

We added a roles_for_select function that just takes in a collection of roles. Let’s explore what this function does line-by-line. We start off with our collection and then pipe it into the next line:

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

Again, remember that &/&1 is shorthand syntax for an anonymous function, so if we threw away the pipe operation and the shorthand, we would see this function rewritten as:

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

We’re running the map operation to return us a list of smaller keyword lists where the name of the role is the key and the id of the role is the value.

Given a particular starting value for roles of:

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

This map call would return:

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

Which we then pipe into the last call, List.flatten which compresses this down to a nice handy list instead of a list of lists. So our end result is:

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

Which just happens to be the format the select form helper is expecting! We can’t pat ourselves on the backs just yet; we still need to modify the templates for 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 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) %>

Finally, in web/templates/user/form.html.eex you’ll want to add in our new select box using our helper and roles assignment. We’ll want to add a select box that contains each of the roles that a user can get moved into. Add the following before our 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>

And now, if you try to add a new user or edit an existing user, you’ll be able to assign a role to that person! That’s one bug off our list!

Running our seeds multiple times duplicates data

Right now, if we run our seeds multiple times we’ll end up erroneously duplicating data, which is no good. Let’s implement a few helper find_or_create anonymous functions:

alias Pxblog.Repoalias Pxblog.Rolealias Pxblog.Userimport 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"endend

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

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

The first thing to note is that we’re aliasing our Repo, Role, and User, and we’re also importing the from function from Ecto.Query to use the linq-style querying syntax. Next, we’ll look at the find_or_create_role anonymous function. The function itself just takes a role name and an admin flag as its arguments. Based on that, we then query with Repo.all for those criteria (note those ^ next to each variable in our where clause; we do not want to do any pattern matching or anything here) and toss that into a case statement. If we cannot find anything with Repo.all, we’ll get an empty list back, so if we get an empty list back, we’ll insert the role. Otherwise, we assume we’ve gotten some matching criteria back and we’ll acknowledge it already exists and move on with the rest of the seeds file. find_or_create_user does the same operations, but just looks for different criteria.

Finally, we call out each of these functions (note the . in between the function name and the arguments; this is required for anonymous function calls!). We need to reuse the admin role to create the admin user, so that’s why we’re not prefacing admin_role with an underscore. We may later decide to keep user_role or the admin user for later seeds, so I’ll leave that code in place but preface those with underscores. It keeps the seeds file looking nice and clean. Now that’s done and we’re ready to run our seeds:

$ 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

The first time we run it, see a bunch of insert statements! Fantastic! Just to be totally sure it’s all working, let’s run it one more time and verify that we don’t see any inserts:

$ mix run priv/repo/seeds.exsRole: 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.6msRole: 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.6msUser: 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

Great! Everything is working and much safer! Plus, we got to have a bit of fun with writing our own utility functions for Ecto!

Errors about duplicate admin user on tests

Since we modified the seeds to create a new user, if you reset your test DB at any point you’ll start running into issues since you cannot create a user that already exists. There’s a simple (and temporary) way to fix this. Open up test/support/test_helper.ex and modify the create_user function:

def create_user(role, %{email: email, username: username, password: password, password_confirmation: password_confirmation}) doif user = Repo.get_by(User, username: username) doRepo.delete(user)endrole|> build_assoc(:users)|> User.changeset(%{email: email, username: username, password: password, password_confirmation: password_confirmation})|> Repo.insertend

Where Are We Now?

Now, we have green specs, we have users, posts, and roles. We’ve implemented sane functionality for restricting user sign-ups, modifying users and posts, and implemented some helpers to make our lives easier when writing code. In our next few posts, we’ll take some time out to add some cool new features to our blog engine!

Next post in this series

Writing a Blog Engine in Phoenix and Elixir: Part 5, Adding ExMachina_Latest Update: 01/26/2016_medium.com

Check out my new book!

Hey everyone! If you liked what you read here and want to learn more with me, check out my new book on Elixir and Phoenix web development:

Phoenix Web Development | PACKT Books_Learn to build a high-performance functional prototype of a voting web application from scratch using Elixir and…_www.packtpub.com

I’m really excited to finally be bringing this project to the world! It’s written in the same style as my other tutorials where we will be building the scaffold of a full project from start to finish, even covering some of the trickier topics like file uploads, Twitter/Google OAuth logins, and APIs!


Published by HackerNoon on 2015/10/15