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

Brandon Richey
Oct 26, 2015 · 19 min read

Latest Update: 08/17/2016

Previous Post in this series

Current Versions

  • 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

Creating a Role Checker Helper

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

We’ll also write some tests to cover this functionality. Open up 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 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

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, _) 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 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) do
post 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: 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

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/5
test/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) 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

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: 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 we’ll do the same for the create action; creating one valid test and one invalid test.

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

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

Allowing Admins to Modify All 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

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

Our failing user creation error message

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

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 do
use Pxblog.Web, :view

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

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

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)

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

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

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

Where Are We Now?

Next post in this series

Check out my new book!

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!

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMI family. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

HackerNoon.com

#BlackLivesMatter

HackerNoon.com

Elijah McClain, George Floyd, Eric Garner, Breonna Taylor, Ahmaud Arbery, Michael Brown, Oscar Grant, Atatiana Jefferson, Tamir Rice, Bettie Jones, Botham Jean

Brandon Richey

Written by

I am a software engineer, and now, published author! Check out my new book at https://www.packtpub.com/web-development/phoenix-web-development

HackerNoon.com

Elijah McClain, George Floyd, Eric Garner, Breonna Taylor, Ahmaud Arbery, Michael Brown, Oscar Grant, Atatiana Jefferson, Tamir Rice, Bettie Jones, Botham Jean