: 08/02/2016 Latest Update Previous Post in this series _Latest Update: 08/02/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 6, Markdown Support Current Versions : v1.3.1 Elixir v1.2.0 Phoenix: v2.0.2 Ecto: Where We Left Off Our blogging application now supports using a nice little markdown editor to make our posts even prettier, so it’s really starting to look like a full-featured project! One thing we don’t have, though, is any way to receive any sort of feedback on any of the posts we write. The good news is this is a pretty simple thing to add and just builds on a lot of the work with associations that we’ve already built! We’re going to start very simple on this initial approach. We’re not going to worry about requiring users to register; instead we’re going to have new comments get added into a “pending” state. Comments are not displayed on the Post unless they’ve been approved out of that pending state unless the user visiting our blog checks a “Show Unapproved” button. Adding Our Comments Model Let’s start by adding a model for our comments. We’re expecting comments to have the following: An author (we’ll use string for this) A body (we’ll use text for this) An approved flag (we’ll use boolean for this, defaulting to false) The post the comment is for (we’ll use reference for this) We don’t want to create the full suite of templates and everything, so we’re going to stick to using for this. mix phoenix.gen.model mix phoenix.gen.model Comment comments author:string body:text approved:boolean post_id:references:posts And then we’ll migrate our DB: mix ecto.migrate Associating Comments With Posts If you open up you can see that Comments are now already associated with Posts, but we haven’t done anything to set that up in reverse. web/models/comment.ex In , add the following to your “posts” schema definition: web/models/post.ex has_many :comments, Pxblog.Comment And now just to make sure everything is working as we expect, run to verify that everything is still green (it should be at this point)! Now let’s modify our tests slightly so that we can test the association between a Post and a Comment. mix test First, open up and add a Comment factory. Add the following at the top with the rest of our alias statements: test/support/factory.ex alias Pxblog.Comment And add the following at the bottom of the project: def comment_factory do%Comment{author: "Test User",body: "This is a sample comment",approved: false,post: build(:post)}end Accordingly, we need to add some tests to take advantage of this new factory. Open up and add the following to the file: test/models/comment_test.exs import Pxblog.Factory # ... test "creates a comment associated with a post" docomment = insert(:comment)assert comment.post_idend Run our tests again and we should remain green! Adding Our Routes For Comments We’ll start by setting up our initial routes that we’ll be using for comments, which will help us create an initial technical design of sorts. Open up : web/router.ex resources "/posts", PostController, only: [] doresources "/comments", CommentController, only: [:create, :delete, :update]end Comments really only make sense in the context of posts, so we’re going to nest comments underneath posts. However, in the other routes, we’ve already defined the Posts resources nested under users! We don’t want to expose a bunch of post-only routes, so we use the code to lock out any root level post routes. Then we add the comments resources, but for right now, we’re only going to allow , , and . Create will be what anonymous users use to create comments (that are initially unapproved). Delete will be for the author of the post or admins to delete a post, and update will be used to allow an admin or author to update a comment to approved so it can be seen by the general public. only: [] :create :delete :update Adding Our Comments Controller and View Now that our models are set up, we need to build a controller with a couple of actions. Most of the showing/viewing comments is going to be via the Posts controller, but we’ll need to be able to delete/update/create comments from this controller in time. We’ll begin by creating : web/controllers/comment_controller.ex defmodule Pxblog.CommentController douse Pxblog.Web, :controllerend And we’ll create a view as well to keep Phoenix happy with us. Create : web/views/comment_view.ex defmodule Pxblog.CommentView douse Pxblog.Web, :viewend Now we’ll head back to the controller and start off with a very basic structure of our three actions, , , and . create update delete def create(conn, _), do: conndef update(conn, _), do: conndef delete(conn, _), do: conn Next, we’ll need a comment form template that we can use on the Show Post page. Create a new directory for us to work with: $ mkdir web/templates/comment Allowing Users To Post Comments We’ll start by creating : web/templates/comment/form.html.eex <%= form_for @changeset, @action, fn f -> %><%= if @changeset.action do %><div class="alert alert-danger"><p>Oops, something went wrong! Please check the errors below.</p></div><% end %> <div class="form-group"><%= label f, :author, class: "control-label" %><%= text_input f, :author, class: "form-control" %><%= error_tag f, :author %></div> <div class="form-group"><%= label f, :body, class: "control-label" %><%= textarea f, :body, class: "form-control", id: "body-editor" %><%= error_tag f, :body %></div> <div class="form-group"><%= submit "Submit", class: "btn btn-primary" %></div><% end %> Pretty standard form here, so not much we need to discuss. Now let’s head to the show post template , where we’ll add a reference to the comment form. Please note that in the template above, we define and , so we’ll need to cycle back to later. web/templates/post/show.html.eex @changeset @action web/controllers/post_controller.ex For right now, let’s stick with . After the list of attributes for the post, add the following line: web/templates/post/show.html.eex <%= render Pxblog.CommentView, "form.html", changeset: @comment_changeset, action: post_comment_path(@conn, :create, @post) %> We need to reference the “form.html” render inside of our View, so we specify that as the first argument to the “render” call. We need to pass it a (which we haven’t set yet, but will soon) and an action, which is where the comments should get posted to. CommentView @comment_changeset And now we’ll head to to make it all work. You’ll need to change the “show” function to resemble the following: web/controllers/post_controller.ex def show(conn, %{"id" => id}) dopost = Repo.get!(assoc(conn.assigns[:user], :posts), id)comment_changeset = post|> build_assoc(:comments)|> Pxblog.Comment.changeset()render(conn, "show.html", post: post, comment_changeset: comment_changeset)end Now let’s go back to our CommentController ( ) and flesh out the create function a little more. We’ll need some stuff at the top for a standard create function, so we’ll start by adding the following to the top (right before our functions): web/controllers/comment_controller.ex alias Pxblog.Commentalias Pxblog.Post plug :scrub_params, "comment" when action in [:create, :update] We’re including “update” in the scrub_params call because we’ll need that later anyways. Now, we’re going to hop down to the create function and replace it with the following: def create(conn, %{"comment" => comment_params, "post_id" => post_id}) dopost = Repo.get!(Post, post_id) |> Repo.preload([:user, :comments])changeset = post|> build_assoc(:comments)|> Comment.changeset(comment_params) case Repo.insert(changeset) do{:ok, _comment} ->conn|> put_flash(:info, "Comment created successfully!")|> redirect(to: user_post_path(conn, :show, post.user, post)){:error, changeset} ->render(conn, Pxblog.PostView, "show.html", post: post, user: post.user, comment_changeset: changeset)endend We first only want to try to create a comment when we’ve been passed comment params and the post_id, since without both of those we cannot create a new comment. Next, we fetch the associated post (remember to preload both the user and comments, since our template will start referencing both!) and start creating a new associated changeset. We start off with the post and pipe that into , which builds an associated schema by specifying the association via an atom. In this case, we want to build an associated . We then pipe that into the Comment.changeset function with the comment_params we pattern matched. The rest of this is business as usual with one exception: build_assoc comment The error condition is a little complicated because again, we’re trying to use another View’s render. We first pass the connection, then the appropriate View to use ( in our case), the template we want to render, and then since that template uses , , and , we need to supply the three. Now you can test it out: if it posts with errors, you’ll get the error listing for the comment right on the page. If it posts without errors, you’ll get a blue flash message at the top! Great progress! Pxblog.PostView @post @user @comment_changeset Adding A Comments Display To Our Posts Next, we need to make it so we can actually see the posts on the template. We’re going to create a shared template that can be used if we ever want to display comments in other places for different things, so we’re going to create and fill it with the following: web/templates/comment/comment.html.eex <div class="comment"><div class="row"><div class="col-xs-4"><strong><%= @comment.author %></strong></div><div class="col-xs-4"><em><%= @comment.inserted_at %></em></div><div class="col-xs-4 text-right"><%= unless @comment.approved do %><button class="btn btn-xs btn-primary approve">Approve</button><% end %><button class="btn btn-xs btn-danger delete">Delete</button></div></div><div class="row"><div class="col-xs-12"><%= @comment.body %></div></div></div> There’s not much to explain here. The approve/delete buttons aren’t hooked up yet; we’ll tackle that in the next tutorial in this series. We also need to modify the controller to preload the comments and modify the show template on Posts to include the listing of comments. First, we’ll tackle the controller updates. In , in the function, we’ll add one line to the line that fetches the post (I’ve bolded the new line): web/controllers/post_controller.ex show post = Repo.get!(assoc(conn.assigns[:user], :posts), id) |> Repo.preload(:comments) This just ensures that our comments are loaded as part of our post. Finally, we’ll open up and add the section of our template that displays comments: web/templates/post/show.html.eex <div class="comments"><h2>Comments</h2><%= for comment <- @post.comments do %><%= render Pxblog.CommentView, "comment.html", comment: comment %><% end %></div> Adding Controller Tests for Comments We can’t leave yet, since we still have some conditions not well-covered by tests! We should cover the controller’s create function and we’ll stick to one positive case and one negative case, since those are the paths that our code will travel through. Create and let’s begin: test/controllers/comment_controller_test.exs defmodule Pxblog.CommentControllerTest douse Pxblog.ConnCase import Pxblog.Factory @valid_attrs %{author: "Some Person", body: "This is a sample comment"}@invalid_attrs %{} setup douser = insert(:user)post = insert(:post, user: user) {:ok, conn: build\_conn(), user: user, post: post} end test "creates resource and redirects when data is valid", %{conn: conn, post: post} doconn = post conn, post_comment_path(conn, :create, post), comment: @valid_attrsassert redirected_to(conn) == user_post_path(conn, :show, post.user, post)assert Repo.get_by(assoc(post, :comments), @valid_attrs)end test "does not create resource and renders errors when data is invalid", %{conn: conn, post: post} doconn = post conn, post_comment_path(conn, :create, post), comment: @invalid_attrsassert html_response(conn, 200) =~ "Oops, something went wrong"endend We’ll continue using Pxblog.Factory since it is so handy. We’ll also set up two module variables, and , same as we do pretty much everywhere else. We also create a setup block that’ll set up a default user and post for us to use to create comments with. @valid_attrs @invalid_attrs Next, we’ll create our positive test case. We post to the nested post->comment path with valid attributes, assert we get redirected as expected and that the comment was created for that post! Finally, we’ll do the same, but with invalid data, and verify that we get the “Oops, something went wrong” message that appears in the HTML when something fails validations! Done! Our current UI! Next Steps We have a great foundation for our comments, but we can definitely improve this more. For example, we still cannot approve or delete comments, and we also will fetch every single comment for a post. In the next few posts, we’ll work on refining those a little more before moving on to changing our comment system into a live comment system powered by Phoenix’s channels! Apparently I completely left off the section where we add the comments to the actual display of posts! I’ve updated this post to now properly reflect those changes :) Thanks ! Update 02/29/2016: Andrew Benz Next Post In This Series _Latest Update: 08/02/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 8, finishing comments 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!