almost 4 years ago

本章的作業目標:

  • 可以在 Group 裡面 post 文章
  • 並且文章網址是使用 /group/1/post/2 這種網址表示。
  • 資料驗證: post 必須要有內容才能儲存 / 新增
  • 用 before_action 來整理重複的程式碼

前置作業 ( model, routes )

建立 posts controller

rails g controller posts
建立一個 Controller: posts (要加s)

rails g model post content:text group_id:integer
建立一個 Model: post (不加s)
並順便建立資料表 post 的二個欄位: content(text 文字屬性) 跟 group_id (integer 整數屬性)

rake db:migrate
將資料庫建立起來

建立 group 與 post 二資料表間的關聯性 (relationship)

routes 設定
config/routes.rb
Rails.application.routes.draw do
  root 'groups#index'
- resources :groups
+ resources :groups do
+   resources :posts
+ end
  
...
...

Models 設定

models/group.rb
app/models/group.rb
class Group < ActiveRecord::Base
  validates :title, presence: true

+ has_many :posts
end
models/post.rb
app/models/post.rb
class Post < ActiveRecord::Base
+ belongs_to :group
end

解說

routes 設定完以後,即可輸入 /groups/1/posts/1 這樣的網址來指向想前往的討論版與文章

Models 裡面 group 跟 post 設定好關聯性後

即可在 rails console 模式做資料表操作來模擬

rails console/logs
# 叫出 Group 第一筆的資料

 > a = Group.find(1)

 => #<Group id: 1, title: "rails101", description: "my first app", created_at: "2014-07-18 22:03:35", updated_at: "2014-07-18 22:03:35">

 
# 叫出該筆資料裡擁有 (has_many) 的全部 post 

 > b = a.posts

 => #<ActiveRecord::Associations::CollectionProxy [#<Post id: 1, content: "hello world", group_id: 1, created_at: "2014-07-18 22:06:23", updated_at: "2014-07-18 22:08:19">, #<Post id: 2, content: nil, group_id: 1, created_at: "2014-07-18 22:06:23", updated_at: "2014-07-18 22:06:23">]>

 
# 叫出某單筆 post 資料

 > c = a.posts.find(1)

 => #<Post id: 1, content: "hello world", group_id: 1, created_at: "2014-07-18 22:06:23", updated_at: "2014-07-18 22:08:19">

 
# 該筆 post 資料屬於 (belongs_to) 哪個 group

 > c.group

 => #<Group id: 1, title: "rails101", description: "my first app", created_at: "2014-07-18 22:03:35", updated_at: "2014-07

我們將用這樣的原理來設定 posts_controller


可以在 Group 裡面 post 文章 ( controllers % views 設定 )

要能在 group 的 show 頁面顯示所擁有的 post

controller

app/controllers/groups_controller.rb
...
...
  def show
    @group = Group.find(params[:id])
+   @posts = @group.posts
  end
...
...

views

app/views/groups/show.html.erb
<div class="col-md-12">
  <div class="group">
    <%= link_to("Edit", edit_group_path(@group), class: "btn btn-primary pull-right")%>
  </div>
  <h2><%= @group.title %></h2>
  <p><%= @group.description %></p>

+ <table class="table">
+   <thead>
+     <tr>
+       <th>文章</th>
+       <th colspan="2"></th>
+     </tr>
+   </thead>
+   <tbody>
+     <% @posts.each do |post| %>
+       <tr>
+         <td><%= post.content %></td>
+         <td>
+           <%= link_to("Edit", edit_group_post_path(post.group, post),
+                       class: "btn btn-default btn-xs")%>
+           <%= link_to("Delete", group_post_path(post.group, post),
+                       class: "btn btn-default btn-xs ", method: :delete, 
+                       data: { confirm: "Are you sure?" } )%>
+         </td>
+       </tr>
+     <% end %>
+   </tbody>
+ </table>
</div>

已上圖是『已經有一筆 post 資料』的模擬圖,我們需要做完後面的 controller 設定才能新增文章


post 的 CRUD 設定

前置

app/controllers/posts_controller.rb
class PostsController < ApplicationController

+ def new
+ end
+ 
+ def edit
+ end
+ 
+ def create
+ end
+ 
+ def update
+ end
+ 
+ def destroy
+ end
end

跟上一章的 groups_controller 比起來少了二個 action: index, show
因為我們全部的文章都直接在討論版的頁面出現,故不需要這二個功能

app/views/posts/ 資料夾新增二個檔案:

app/views/posts/new.html.erb
(空的)
app/views/posts/edit.html.erb
(空的)

new action

controller
app/controllers/posts_controller.rb
...
...
  def new
    @group = Group.find(params[:group_id])
+   @post = @group.posts.new
  end
 ...
 ...
view

要在討論版頁面(show) 有新增文章的按鈕

app/views/groups/show.html.erb
<div class="col-md-12">
  <div class="group">
+   <%= link_to("New Post", new_group_post_path(@group), class: "btn btn-warning pull-right") %>
    <%= link_to("Edit", edit_group_path(@group), class: "btn btn-primary pull-right") %>
  </div>
...
...

並顯示 新增文章 的頁面

app/views/posts/new.html.erb
<h1 class="text-center">新增文章</h1>

<div class="col-md-4 col-md-offset-4">
  <%= simple_form_for [@group,@post] do |f| %>
    <div class="form-group">
      <%= f.input :content, input_html: { class: "form-control"} %>
    </div>
    <div class="form-actions">
      <%= f.submit "Submit", disable_with: "Submiting...", class: "btn btn-primary"%>
    </div>
  <% end %>
</div>


create action

app/controllers/posts_controller.rb
...
...
  def create
+   @group = Group.find(params[:group_id])
+   @post = @group.posts.build(post_params)
+
+   if @post.save
+     redirect_to group_path(@group), notice: "新增文章成功!"
+   else
+     render :new
+   end
  end
...
...
#(放到最下面)
+ private
+
+ def post_params
+   params.require(:post).permit(:content)
+ end


edit & update action

controller
app/controllers/posts_controller.rb
...
...
  def edit
+   @group = Group.find(params[:group_id])
+   @post = @group.posts.find(params[:id])
  end
...
...
  def update
+   @group = Group.find(params[:group_id])
+   @post = @group.posts.find(params[:id])
+
+   if @post.update(post_params)
+     redirect_to group_path(@group), notice: "文章修改成功!"
+   else
+     render :edit
+   end
  end
...
...
view
app/views/posts/edit.html.erb
(跟 new.html.erb 一模一樣, 把『新增文章』改成『修改文章』即可)


destroy action

app/controllers/posts_controller.rb
...
...
  def destroy
+   @group = Group.find(params[:group_id])
+   @post = @group.posts.find(params[:id])
+
+   @post.destroy
+   redirect_to group_path(@group), alert: "文章已刪除"
  end
...
...


資料驗證: post 必須要有內容才能儲存 / 新增

當然,我們也要設下資料驗證,文章內容不能空白

app/models/post.rb
class Post < ActiveRecord::Base 
  belongs_to :group
+ validates :content, presence: true
end

用 before_action 來整理重複的程式碼

目前為止,我們的 posts_controller 整份程式碼如下:

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
    @group = Group.find(params[:group_id])
    @post = @group.posts.new
  end

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

  def create
    @group = Group.find(params[:group_id])
    @post = @group.posts.build(post_params)

    if @post.save
      redirect_to group_path(@group), notice: "新增文章成功!"
    else
      render :new
    end
  end

  def update
    @group = Group.find(params[:group_id])
    @post = @group.posts.find(params[:id])

    if @post.update(post_params)
      redirect_to group_path(@group), notice: "文章修改成功!"
    else
      render :edit
    end
  end

  def destroy
    @group = Group.find(params[:group_id])
    @post = @group.posts.find(params[:id])

    @post.destroy
    redirect_to group_path(@group), alert: "文章已刪除"
  end

  private

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

有沒有人發現每個 action 都有重複的地方? 不會覺得礙眼嗎 XD

可以看到每個 action 前面都有一行 『 @group = Group.find(params[:group_id]) 』

所以能使用 before_action 來宣告此 Controller 每個 action 在執行之前都會先做 before_action 的設定

來把每個 action 的那行簡化掉

在最下面增加一段 code

變成

app/controllers/posts_controller.rb
...
...
  private
...
...
  def find_group
      @group = Group.find(params[:group_id])
  end  

然後在最前面加一行 before_action :find_group

變成

app/controllers/posts_controller.rb
class PostsController < ApplicationController
    
  before_action :find_group
...
...  

最後再把每個 action 裡的 『 @group = Group.find(params[:group_id]) 』 刪掉
最後結果:

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :find_group

  def new
    @post = @group.posts.new
  end

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

  def create
    @post = @group.posts.build(post_params)

    if @post.save
      redirect_to group_path(@group), notice: "新增文章成功!"
    else
      render :new
    end
  end

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

    if @post.update(post_params)
      redirect_to group_path(@group), notice: "文章修改成功!"
    else
      render :edit
    end
  end

  def destroy
    @post = @group.posts.find(params[:id])

    @post.destroy
    redirect_to group_path(@group), alert: "文章已刪除"
  end

  private

  def find_group
    @group = Group.find(params[:group_id])
  end

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

延伸運用

before_action 是一個常見的 controller 技巧,用來收納重複的程式碼。

before_action 可以用 only,指定某些 action 執行:

before_action :find_group, only: [:edit, :update] 

或者使用 except,排除某些 action 不執行:

before_action :find_group, except: [:show, :index]

還有一個 after_action => 同 before_action, 用於執行完成 action 後的動作

← [ 2.0 ] 2-1. 手動實作出有 CRUD 功能的討論版 [ 2.0 ] 4. 建立使用者功能 →
 
comments powered by Disqus