Implementing the Ability to Edit a Post
In this section we'll walk through the steps necessary to give users the ability to edit the posts that they created.
Guide Tasks
  • Read Tutorial

Since users now have the ability to create posts and we have some clean data, it would make sense to implement the ability for a user to edit a post that they created. Let's build that feature out now.

We're create a new describe block in our post_spec and add in the specs that we'll need to get passing to consider this feature ready.

# spec/features/post_spec.rb

  describe 'editing' do
    before do
      user = FactoryGirl.create(:user)
      second_user = FactoryGirl.create(:second_user)
      login_as(user, :scope => :user)

      post = Post.create(title: "starter title", content: "starter content", topic_id: @topic.id, user_id: user.id)

      visit edit_topic_post_path(topic_id: @topic.id, id: post.id)
    end

    it 'allows a user to edit a post they created' do
      fill_in 'post[title]', with: "Baseball Stats"

      click_on "Save"

      expect(page).to have_content("Baseball Stats")
    end

    xit 'does not allow a user to edit a post they did not create' do
    end
  end

Let's walk through what we're doing here. In the before block we're creating multiple users, we're doing this because we need to test to ensure that a user is only able to edit posts that they created, so we need multiple users in order to test this functionality. Then we're logging in one of the users.

After this we're creating a new Post, we're going to use the create method in this case because I want to control the data and have it right in front of me, in the future I can move this into a factory with the appropriate data relationships, but having a few create calls in my test suite doesn't bother me. This follows the pattern I like when I'm building out features, where I integrate a working implementation in the easiest way possible and then refactor it afterwards. There's a practical reason for this. If I try to get too fancy with an implementation in the beginning and I run into a bug, I not only have to debug the feature, but I also need to debug the test suite, which is not a fun way to program, and coding should always be fun or we're doing it wrong :)

Lastly we're visiting the edit path, we want this action nested under a topic since all posts will have a topic that they're associated with, so we can keep the nesting structure at this point.

Inside the spec itself I'm simply filling out the title attribute with a new value, clicking save and then checking to see if the new title value shows up on the next page the user is redirected to.

Nothing too crazy with this feature, so let's get started by running rspec and see where the tests lead us. This gives us the output we'd hope for AbstractController::ActionNotFound: The action 'edit' could not be found for Topics::PostsController, this means that our route is working and we need to update the posts controller by adding in an edit method.

# app/controllers/topics/posts_controller.rb

def edit
end

Don't you like these small and easy steps? That's one of my favorite parts of using BDD to build applications. Let's also add in the template as well since we know that is going to be the next thing the test is going to ask for:

touch app/views/topics/posts/edit.html.erb

Running the specs now will tell us that it couldn't find the post[title] field. Let's copy the form fromnewtemplate (I know, I know, what happened to being DRY?! Don't worry, we'll take care of the duplication in the **refactor** stage, let's get the spec passing first). We'll only need to make a few modifications to get this working for theedit` action:

<!-- app/views/topics/posts/edit.html.erb -->

<%= form_tag topic_post_path(topic_id: @topic.id, id: @post.id), method: 'put' do %>

  <%= text_field_tag "post[title]" %>

  <%= submit_tag 'Save' %>

<% end %>

Some of the changes made here are:

  1. The route changed to the route dictated by the update action, you can find this by running rake routes | grep posts and looking at the line topics/posts#update, notice how it's listed under the method topic_post, that shows use that we'll be using the topic_post_path route helper method and passing in the trail and post id values.
MacBook-Pro-3:dailysmarty admin$ rake routes | grep posts
topic_posts     GET      /topics/:topic_id/posts(.:format)          topics/posts#index
edit_topic_post GET      /topics/:topic_id/posts/:id/edit(.:format) topics/posts#edit
topic_post      GET      /topics/:topic_id/posts/:id(.:format)      topics/posts#show
                PATCH    /topics/:topic_id/posts/:id(.:format)      topics/posts#update
                PUT      /topics/:topic_id/posts/:id(.:format)      topics/posts#update
                DELETE   /topics/:topic_id/posts/:id(.:format)      topics/posts#destroy
new_post        GET      /posts/new(.:format)                       topics/posts#new
create_post     POST     /posts(.:format)                           topics/posts#create
  1. Changed the method from post to put, referencing the rake routes list again, notice how the HTTP verb for the update action is put and patch? That's what tells me that we'll need to use one of those methods (I typically use put by default, but they're very similar).

  2. I removed the other form fields, we'll concern ourselves with those during the refactor step.

Running rspec now is going to give us a cryptic error ActionView::Template::Error: undefined methodid' for nil:NilClass, so what's going on? Well, we need to let theeditmethod so that it knows whatpost` is being edited, that makes sense:

# app/controllers/topics/posts_controller.rb

  def edit
    @post = @topic.posts.find(params[:id])
  end

Running the specs now will show that we're heading down the right path, saying that it can't find an update action in the controller. The update action is needed since it is what will be altering the database record, so let's update the controller, we can also add in some code into the action since we know the method isn't going to do anything magical by itself:

# app/controllers/topics/posts_controller.rb

  def update
    post = @topic.posts.find(params[:id])

    if post.update(post_params)
      redirect_to topic_post_path(topic_id: post.topic_id, id: post), notice: 'Your post was successfully updated.'
    else
      render :edit, notice: 'There was an error processing your request!'
    end
  end

Running rspec will now show that the tests are passing, nice work! Users can now edit their posts. Let's test this in the browser, starting up the rails server you can navigate to any post show page, such as http://localhost:3000/topics/my-title-99/posts/1, and even though we could simply append /edit, let's do it right and add a button to the show view:

<!-- app/views/topics/posts/show.html.erb -->

<h1><%= @post.title %></h1>
<p><%= @post.topic.title %></p>
<p><%= @post.user.full_name %></p>

<%= link_to "Edit Post", edit_topic_post_path(topic_id: @topic.id, id: @post.id) %>

Now if you refresh the page you'll see the Edit Post button. If you click on it you'll be taken to the edit view page where you can edit the value, so nice work, the feature is working properly. Now let's refactor, starting with the controller, do you see all of the duplicate calls of post = @topic.posts.find(params[:id]), right now the same code is in the show, edit, and update method. Let's move this into its own method and add it to a before_action method so these three methods will have access to the @post value automatically. The controller should now look like this:

# app/controllers/topics/posts_controller.rb

class Topics::PostsController < ApplicationController
  before_action :set_topic, except: [:new, :create]
  before_action :set_post, only: [:show, :edit, :update]

  def index
    @posts = @topic.posts
  end

  def new
  end

  def create
    post = Post.new(post_params)
    post.user_id = current_user.id

    if post.save
      redirect_to topic_post_path(topic_id: post.topic_id, id: post), notice: 'Your post was successfully published.'
    else
      render :new
    end
  end

  def show
  end

  def edit
  end

  def update
    @post = @topic.posts.find(params[:id])

    if @post.update(post_params)
      redirect_to topic_post_path(topic_id: @post.topic_id, id: @post), notice: 'Your post was successfully updated.'
    else
      render :edit, notice: 'There was an error processing your request!'
    end
  end

  private

    def set_topic
      @topic = Topic.friendly.find(params[:topic_id])
    end

    def set_post
      @post = @topic.posts.find(params[:id])
    end

    def post_params
        params.require(:post).permit(:title, :content, :topic_id)
    end
end

Let's run the tests again and they're all passing, so that refactor worked nicely and our controller looks much better now. The next refactor is a little tricky, mainly because we're using a form_tag for posts. However form_tag is pretty flexible, so let's create a new partial for the form that the edit and new templates can be share. We'll take it one at a time. First create a partial:

touch app/views/topics/posts/_form.html.erb

Now let's move the code from the posts/new.html.erb file into the form partial and update the new template to simply call the partial and let it know that it's coming from the new action:

<!-- app/views/topics/posts/new.html.erb -->

<%= render partial: 'form', locals: { source: 'new' } %>

Running the specs now will show that everything is still working properly, so that's good. However the behavior of the edit action is a little more complex. Let's go through differences:

  1. The form needs to know the topic and post that is being edited, so we can't use the same route helper.

  2. The form needs to pre-populate values that are already set, so the title, content, and topic need to show the current values, right now they'd show up blank.

We can start out by calling the partial from the edit template, updating the source local variable:

<!-- app/views/topics/posts/edit.html.erb -->

<%= render partial: 'form', locals: { source: 'edit' } %>

Now let's create a super ugly, super hideous implementation in the form partial that will work for both the edit and new actions:

<!-- app/views/topics/posts/_form.html.erb -->

<% if source == 'new' %>
  <%= form_tag create_post_path, method: 'post' do %>

    <%= text_field_tag "post[title]" %>
    <%= text_field_tag "post[content]" %>

    <%= collection_select(:post, 
                          :topic_id, 
                          Topic.all, 
                          :id,
                          :title,
                          { selected: (params[:topic_id] if params[:topic_id])}) 
    %>

    <%= submit_tag 'Save' %>

  <% end %>

<% else %>

  <%= form_tag topic_post_path(topic_id: @topic.id, id: @post.id), method: 'put' do %>

    <%= text_field_tag "post[title]" %>

    <%= submit_tag 'Save' %>

  <% end %>

<% end %>

Running this will still pass the tests, but... ugh, this is ugly. I put this in place to give us a base case, so we know everything up until this point is working, so now we can focus on the more complex refactor, which is to merge both forms into one, let's do that now:

<!-- app/views/topics/posts/_form.html.erb -->

<% if source == 'new' %>
  <% form_route = create_post_path %>
  <% form_action = 'post' %>
<% else %>
  <% form_route = topic_post_path(topic_id: @topic.id, id: @post.id) %>
  <% form_action = 'put' %>
<% end %>

<%= form_tag form_route, method: form_action do %>

  <%= text_field_tag "post[title]", (@post.title if @post) || "" %>
  <%= text_field_tag "post[content]", (@post.content if @post) || "" %>

  <%= collection_select(:post, 
                        :topic_id, 
                        Topic.all, 
                        :id,
                        :title,
                        { selected: (params[:topic_id] if params[:topic_id])}) 
  %>

  <%= submit_tag 'Save' %>

<% end %>

This works and we can test it by running rspec and by testing it in the browser for both the new and edit features, however we can still clean it up by implementing some view helpers, let's add the following methods to the helper file to get the route and the method:

# app/helpers/posts_helper.rb

module PostsHelper
  def form_route(source)
    return create_post_path if source == 'new'
    return topic_post_path(topic_id: @topic.id, id: @post.id) if source == 'edit'
  end

  def form_action(source)
    return 'post' if source == 'new'
    return 'put' if source == 'edit'
  end
end

Each of these methods take in a single argument of source and return different values based on if it's coming from the edit or new pages. Now we can update the form and simply call these methods, sending in the source variable:

<!-- app/views/topics/posts/_form.html.erb -->

<%= form_tag form_route(source), method: form_action(source) do %>

  <%= text_field_tag "post[title]", (@post.title if @post) || "" %>
  <%= text_field_tag "post[content]", (@post.content if @post) || "" %>

  <%= collection_select(:post, 
                        :topic_id, 
                        Topic.all, 
                        :id,
                        :title,
                        { selected: (params[:topic_id] if params[:topic_id])}) 
  %>

  <%= submit_tag 'Save' %>

<% end %>

Running the specs and everything is still working. If you think that this code still feels a little dirty your code senses are right, later on in the course we will take our refactor even further and implement a meta programming module to clean this up even more. But for right now this is working, even with pretty advanced behavior, using the same form for a nested and non-nested resource is not a trivial feature, however it gives our application much more flexibility, so very nice work.

In the next lesson we'll implement the next expectation that will ensure that only post creators can edit their posts.

Resources