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.
caseis a reserved word in Ruby - this means it has a pre-defined meaning in certain contexts. Choosing the
caseword to reflect an application concept was a bad thing and we had to be careful and find workarounds for some nasty issues (hence the
cvariable in the .html.erb files). To work with Update, I first checked if it's a reserved word or not. It seems safe.
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
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.
app/models/user.rb, add before the last
app/models/case.rb, also before the last end add:
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/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.
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:
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
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
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.
<%= render "shared/internal_navbar" %>on the top of the
views/cases/show.html.erbfile to display the internal navigation bar.