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 thecase
word to reflect an application concept was a bad thing and we had to be careful and find workarounds for some nasty issues (hence thec
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 theviews/cases/show.html.erb
file to display the internal navigation bar.