20: Letting users post updates

Having a database of cases with a beautiful interface for it is useful. However, it is more useful to let users post updates and attach various pieces of information to each case.

In order to implement the updates timeline functionality we first need to understand what kind of data is involved here. Users can post updates on many cases, therefore an update should be associated with a User who authored it as well as with the case to which it belongs.

We would like the update to have a slightly more complex structure, such as:

  • a title
  • a type or domain
  • a body
  • an external link referencing a source for more info

Given this information we can start off by creating the Update model.

When we introduced the Case model and terminology, we made a rather stupid mistake we can finally admit. case is a reserved word in Ruby - this means it has a pre-defined meaning in certain contexts. Choosing the case word to reflect an application concept was a bad thing and we had to be careful and find workarounds for some nasty issues (hence the c variable in the .html.erb files). To work with Update, I first checked if it's a reserved word or not. It seems safe.

The models

We know that updates should be stored in the database. To manipulate the database, we start with a migration. Type in the terminal:

$ rails generate migration CreateUpdates

This created a 123123123_create_updates.rb migration file in db/migrate, with the following content:

class CreateUpdates < ActiveRecord::Migration
  def change
    create_table :updates do |t|
    end
  end
end

Now specify the fields that should be created in the database by replacing the contents of the file with the following:

class CreateUpdates < ActiveRecord::Migration
  def change
    create_table :updates do |t|
      t.belongs_to :user
      t.belongs_to :case

      t.string :title
      t.string :domain
      t.text :body
      t.string :external_link

      t.timestamps null: false
    end
  end
end

In terminal, run

rake db:migrate

to apply the changes to the database schema.

Create a file app/models/update.rb and insert the following code into it:

class Update < ActiveRecord::Base
  belongs_to :user
  belongs_to :case
  validates :title, presence: true, allow_blank: false
  validates :domain, presence: true, allow_blank: false
  validates :body, presence: true, allow_blank: false
end

Notice that here we also specify that an Update belongs to a User and a Case. It also checks if the title, domain and body fields are present and not blank.

Now we also have to tell the User and the Case model that they can have multiple Updates associated with them.

In app/models/user.rb, add before the last end:

has_many :updates

In app/models/case.rb, also before the last end add:

has_many :updates

The models are set, let's move on to some visual stuff.

The case page - show action and view

At the moment we don't have a page where we can view a single post with all the data related to it.

This is called the show action and we can add it by providing a method in the CasesController and a respective view in app/views/cases.

Open the app/controllers/cases_controller.rb and add a method below the def index ... end:

def show
  @case = Case.find(params[:id])
end

Create a new file app/views/cases/show.html.erb and populate it with the following data:

<div class="container">
  <div class="col-lg-6 col-md-12 col-sm-12 col-xs-12">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title"><%= @case.title %></h3>
      </div>
      <div class="panel-body">
        <%= cl_image_tag(@case.case_image.path, width: 500, class: 'img-responsive img-thumbnail') %>
        <p>
          <%= simple_format @case.body %>
        </p>
      </div>
      <div class="panel-footer">
        <span class="label label-default"><%= "Created by: " + @case.user.email %></span>
        <% if current_user == @case.user %>
          <%= link_to(edit_case_path(@case), class: 'btn btn-default btn-xs pull-right') do %>
            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
          <% end %>
          <%= link_to(case_path(@case), method: :delete, class: 'btn btn-danger btn-xs pull-right') do %>
            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
          <% end %>
        <% end %>
      </div>
    </div>
    <div class="panel panel-default">
      <div class="panel-body">
        <h3> Post a new update </h3>
        <%= bootstrap_form_for([@case, Update.new], layout: :horizontal, label_col: "col-sm-3", control_col: "col-sm-8") do |form| %>
          <%= form.text_field :title, label: "Short title", required: true %>
          <%= form.text_field :domain, label: "Domain", required: true %>
          <%= form.text_area :body, label: "Full text", placeholder: "Enter the full text of your update", required: true %>
          <%= form.text_field :external_link, label: "External link"%>
          <%= form.submit %>
        <% end %>
      </div>
    </div>
  </div>

  <div class="col-lg-6 col-md-12 col-sm-12 col-xs-12">
    <% @case.updates.reverse_each do |u| %>
      <div class="panel panel-default">
        <div class="panel-heading">
          <h4 class="panel-title"> <%= u.title %> </h4>
        </div>
        <div class="panel-body">
          <%= simple_format u.body %>
        </div>
        <div class="panel-footer">
          <span class="label label-primary"><%= u.domain %></span>
          <span class="label label-info">External link: <%= link_to(u.external_link, u.external_link)%></span>
          <span class="label label-default">Posted by: <%= u.user.email %></span>
        </div>
      </div>
    <% end %>
  </div>
</div>

This page will show the main case info, the list of updates (in reverse order, with the most recent on top), and a form that allows users to submit a new update.

The display is not unique and definitely not a standard, I assembled it using default Boostrap components and moving things around until they looked good enough. Feel free to take only as reference and ajust it to best reflect your needs.

In order to reach this page we can make the titles (and images) of cases in the index action clickable. For implementing this, open the app/views/cases/index.html.erb and find the block:

<div class="panel-heading">
  <h3 class="panel-title"><%= c.title %></h3>
</div>
<div class="panel-body">
  <%= cl_image_tag(c.case_image.path, width: 500, class: 'img-responsive img-thumbnail') %>
  <p>
    <%= c.body %>
  </p>
</div>

And use the link_to helper before the title and the image, like this:

<div class="panel-heading">
  <h3 class="panel-title"><%= link_to(c.title, c) %></h3>
</div>
<div class="panel-body">
  <%= link_to(cl_image_tag(c.case_image.path, width: 500, class: 'img-responsive img-thumbnail'), c) %>
  <p>
    <%= c.body %>
  </p>
</div>

This will transform the title and image of every case c into a link to the respective case page.

Routes, routes

If you navigate now to a case you will see errors about routing. This is because we didn't define yet any routes and controllers that would handle the action of adding an update.

To define the necessary routes open the config/routes.rb file and locate the line:

resources :case

And replace it with this block of code:

resources :cases do
  resources :updates, only: :create
end

Controllers and actions

The last thing you have to do to make updates work is define the updates controller. Create the file app/controllers/updates_controller.rb and fill it with the following code:

class UpdatesController < ApplicationController
  before_filter :authenticate_user!

  def create
    update = Update.new(update_params)
    update.case = Case.find(params[:case_id])
    update.user = current_user
    update.save!

    redirect_to update.case
  end

  private

  def update_params
    params.require(:update).permit(:title, :domain, :body, :external_link)
  end
end

You can see above the action create that takes care of initializing an Update and linking it to a user and a case.
Now you can navigate to the page of a case and and use the form below the case in order to add an Update:

Important! Dependent destroy

We know our cases have many updates. However, the users can delete (destroy) cases, and we will have lingering updates in the database. This is why we have to add a new operation - a dependent destroy.

When a case is destroyed, we would like to delete all associated updates. Open the app/models/case.rb file and edit the has_many :updates line to add the dependent: :destroy operation:

class Case < ActiveRecord::Base
  has_attachment :case_image
  belongs_to :user
  has_many :updates, dependent: :destroy
end

Even if we don't use it right now, let's do the same for app/models/user.rb - when a user is deleted, the associated cases and updates must be deleted, too:

class User < ActiveRecord::Base
  has_many :cases, dependent: :destroy
  has_many :updates, dependent: :destroy

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable
end

Your turn

Feel free to explore the code above and adapt it to reflect the content of your application. Small changes in the labels, colors and order in which the content is displayed will make big changes in the percepetion and use of the system.

Use <%= render "shared/internal_navbar" %> on the top of the views/cases/show.html.erb file to display the internal navigation bar.

results matching ""

    No results matching ""