08/17/2016 Latest Update: Previous Post in this series _Latest Update: 07/21/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 3, Adding Roles to our Models Current Versions As of the time of writing this, the current versions of our applications are: : v1.3.1 Elixir v1.2.0 Phoenix: v2.0.2 Ecto: v2.5.2 Comeonin: 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 and we’ll populate it with the following module: web/models/role_checker.ex 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 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. is_admin? 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 line in , add the following: scrub_params web/controllers/user_controller.ex 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 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 call. authorize_user RoleChecker.is_admin? is even simpler; we’re just checking that the current user is an admin. authorize_admin To verify this all works, we’re going to go back to our file and modify our tests to work against these new assumptions. test/controllers/user_controller_test.exs 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: 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 ; 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 below: RoleChecker.is_admin? web/controllers/session_controller.ex 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 file looks like this: user_controller___test.exs 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 and modify the function to also use our 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. web/controllers/post_controller.ex authorize_user RoleChecker.is_admin? 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 and add some more tests at the bottom covering our authorization rules: test/controllers/post_controller_test.exs test "redirects when trying to delete a post for a different user", %{conn: conn, role: role, post: post} do{:ok, other_user} = TestHelper.create_user(role, %{email: "test2@test.com", username: "test2", password: "test", password_confirmation: "test"})conn = delete conn, user_post_path(conn, :delete, other_user, post)assert get_flash(conn, :error) == "You are not authorized to modify that post!"assert redirected_to(conn) == page_path(conn, :index)assert conn.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 ( ) by (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: https://github.com/Diamond/pxblog nolotus 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 actions. First, throw an to the top of your User Controller ( ) if it’s not already there. Then, we’ll modify the , , , and actions: new, create, edit, and update alias Pxblog.Role web/controllers/user_controller.ex new edit create update 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 and added those to the list of assigns we sent out to the view (including in our render statements in the error cases). Repo.all(Role) 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 argument, requires either a list or a keyword list, either in the form of or . 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: values [value, value, value] [displayed: value, displayed: value] defmodule Pxblog.UserView douse Pxblog.Web, :view def roles_for_select(roles) doroles|> Enum.map(&["#{&1.name}": &1.id])|> List.flattenendend We added a 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: roles_for_select 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, which compresses this down to a nice handy list instead of a list of lists. So our end result is: List.flatten ["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 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: web/templates/user/form.html.eex <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 anonymous functions: find_or_create 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 function from Ecto.Query to use the linq-style querying syntax. Next, we’ll look at the 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. does the same operations, but just looks for different criteria. from find_or_create_role find_or_create_user 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”, “ ”] 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” [“ ”, {{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 admin@test.com admin@test.com 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”, “ ”] OK query=0.8ms admin@test.com 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 and modify the function: test/support/test_helper.ex create_user 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 _Latest Update: 01/26/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 5, Adding ExMachina 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: _Learn to build a high-performance functional prototype of a voting web application from scratch using Elixir and…_www.packtpub.com Phoenix Web Development | PACKT Books 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!