Software engineering notes

Rails Form

Introduction

Rails 原生表單

form

# method 預設是 post
<%= form_for @post, url: posts_path(@post) do |f| %>
    <%= f.label :title %>
    <%= f.text_field :title %>
    <%= f.label :content %>
    <%= f.text_field :content %>
    <%= f.button :submit, disable_with: 'Submiting' %>
<% end %>

form_for 與 form_tag 差別

使用 form_for 的話一定要在 controller 的先 new 好 (@post = Post.new)

所以要 create 必須先 new 好

view :

form_for @post, do |p|
    p.text_field :title
end

title 必須與 db 一致, 因為它會去抓 model 的欄位

使用 form_tag 的話欄位名稱就可以自己取, 只不過要自己手動抓欄位名稱 (aaa = params[:title])

顯示 validation 錯誤訊息

顯示在欄位後面

<%= f.label :title %> : <%= f.text_field :title, :placeholder => 'At least 5 characters' %><%= @post.errors.full_messages_for(:title).first %>

全部錯誤訊息

<% @post.errors.full_messages.each do |msg| %>
  <li><%= msg %></li>
<% end %>

全部錯誤訊息的第一個

<%= @post.errors.full_messages.first if @post.errors.any? %>

顯示某個欄位的錯誤

<%= @post.errors.full_messages_for(:title).first %>

當 form 的錯誤訊息發生, 造成跑版

因為 label 及 text_field 會被 <div class="field_with_errors"> 包起來, 所以造成跑版

在 config/application.rb 加上就會顯示原始的 html 了

config.action_view.field_error_proc = Proc.new { |html_tag, instance|
  html_tag
}

ActionController::InvalidAuthenticityToken

如果是自己寫 HTML 的 form 送出表單造成沒有一起把 token 送出去, 加入以下這行到 form 即可解決

<%= tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) %>

simple_form

Install

gem 'simple_form'

執行

rails generate simple_form:install

基本用法

<%= simple_form_for @user, defaults: { input_html: { class: 'default_class' } } do |f| %>
  <%= f.input :username, input_html: { class: 'special' }, wrapper_html: { class: 'username' }  %>          # wrapper_html 會在 label 及 input 外層包一個 div
  <%= f.input :password, input_html: { maxlength: 20 }, label_html: { class: 'my_class' } %>
  <%= f.input :role, as: :radio_buttons, collection: { t('.role_client') => 'client', t('.role_translator') => 'translator' }, checked: 'client' %>
  <%= f.input :remember_me, input_html: { value: '1' } %>
  <%= f.button :submit %>
<% end %>

f.input 包含了 label, input

bootstrap

執行

$ rails generate simple_form:install --bootstrap

   identical  config/initializers/simple_form.rb
      create  config/initializers/simple_form_bootstrap.rb
       exist  config/locales
   identical  config/locales/simple_form.en.yml
   identical  lib/templates/erb/scaffold/_form.html.erb

locales 只會產生出 simple_form EN 的, 自己再 copy 改成 zh-TW 版本的

基本用法

<div class="row">
  <div class="col-md-6">
    <div class="panel panel-primary">
      <div class="panel-heading">Simple Form: Basic Form</div>
      <div class="panel-body">
        <%= simple_form_for @post, url: posts_path(@post), html: { class: 'form-horizontal' } do |f| %>
          <%= f.error_notification %>

          <%= f.input :title, input_html: {class: 'form-control'} %>

          <%= f.input :content, input_html: {class: 'form-control'} %>

          <%= f.button :submit, disable_with: 'Submiting', input_html: {class: 'btn btn-success'} %>
        <% end %>
      </div>
    </div>
  </div>
</div>

連錯誤訊息都會自動顯示在欄位下, 非常方便

submit button 不能用 f.submit, 應該用 :

`<%= f.button :submit, t('form.submit'), class: 'btn btn-success' %>`

default value, fail validation 會填原本送出的值

<%= f.input :contact_email, required: true, input_html: {value: (params[:user].nil?) ? f.object.email : params[:user]['contact_email']} %>

i18n

copy config/locales/simple_form.en.yml to config/locales/simple_form.zh-TW.yml, 替換第一行 en -> zh-TW

collection + i18n:

Radio

<%= f.input :sex, as: :radio_buttons, collection: [:male, :female] %>
<%= f.input :sex, as: :radio_buttons, collection: User.genders.keys.map { |x| x.to_sym } %>

collection 不能直接用 User.genders.keys, 因為它只是 array, 值一定要 symbol i18n 才會 work

simple_form.zh-TW.yml :

simple_form:
    options:
      user:
        gender:
          male: '男'
          female: '女'
    labels:
      user:
        gender: "性別"
    hints:
      user:
        gender: "請選擇性別"

Radio + ActiveRecord enum

<%= f.input :gender, as: :radio_buttons, collection: User.genders.keys %>

devise + simple_form + bootstrap - Sign_up

<div class="row">
  <div class="col-md-4 col-md-offset-4">
    <h2><%= t('.sign_up', :default => "Sign up") %></h2>

    <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
      <%= f.error_notification %>
      <%= f.input :email, required: true, autofocus: true %>
      <%= f.input :password, required: true, hint: (t('.characters_minimum', num: 8) if @minimum_password_length) %>
      <%= f.input :password_confirmation, required: true %>
      <%= f.button :submit, t('form.submit'), class: 'btn btn-success' %>
    <% end %>

    <%= render "devise/shared/links" %>
  </div>
</div>

bootstrap_form

gem 'bootstrap_form'

application.css 加上

/*
 *= require rails_bootstrap_forms
 */

基本用法

<%= bootstrap_form_for(@user) do |f| %>
  <%= f.email_field :email %>
  <%= f.password_field :password %>
  <%= f.check_box :remember_me %>
  <%= f.submit "Log In" %>
<% end %>

它會產生

<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
  <div class="form-group">
    <label for="user_email">Email</label>
    <input class="form-control" id="user_email" name="user[email]" type="email">
  </div>
  ...略...
</form>

Paperclip

paperclip, 上傳檔案或處理圖片, 對 avatar 可以處理的很乾淨

安裝 paperclip

gem "paperclip", "~> 4.3"

安裝 imagemagick

Mac

brew install imagemagick

Ubuntu

sudo apt-get install imagemagick -y

設定 ImageMagick utilities 的指令路徑

可用以下方法確認你的 convert 指令找的到, 有的話可以先不用設定

$ which convert
/usr/local/bin/convert

如果不能正常運作, 再設定 config/environments/development.rb :

Paperclip.options[:command_path] = "/usr/local/bin/"

上傳 avatar 圖片

1) models/user.rb

has_attached_file :avatar,
  styles: { medium: "300x300>", thumb: "100x100>" },
  # default_url: "/images/:style/missing.png",
  default_url: ->(attachment) { ActionController::Base.helpers.image_path('icons/avatar.png') },      # 在 production 才會正確顯示出來
  url: "/:class/:attachment/:id/:style.:extension",
  path: ":rails_root/public:url"
validates_attachment :avatar, presence: true,
  content_type: { content_type: /\Aimage\/.*\Z/ },                          # 或特定類型 content_type: "image/jpeg" or content_type: ['image/jpeg', 'image/png', 'image/gif']
  size: { in: 0..20.megabyte }                                             # 或 KB { in: 0..20.kilobytes }

2) 新增 avatar 所需的 DB 欄位

rails generate paperclip user avatar

產生 db/migrate/20150713155732_add_attachment_avatar_to_users.rb, 內容為

class AddAttachmentAvatarToUsers < ActiveRecord::Migration
  def self.up
    change_table :users do |t|
      t.attachment :avatar
    end
  end

  def self.down
    remove_attachment :users, :avatar
  end
end

執行 rake db:migrate

它會在你的 users 加上這些欄位

t.string   "avatar_file_name"
t.string   "avatar_content_type"
t.integer  "avatar_file_size"
t.datetime "avatar_updated_at"

如果只是要增加欄位

add_attachment :users, :avatar

3) 加上 avatar 相關程式

views/dashboard/welcome/index.html.erb

<%= form_for @user, :url => users_path, :html => { :multipart => true } do |form| %>
  <%= form.file_field :avatar %>
<% end %>

或 simple_form 版

<%= simple_form_for @user, url: dashboard_welcome_update_avatar_path do |form| %>
  <%= form.input :avatar, as: :file %>
  <%= form.submit '上傳' %>
<% end %>

顯示原圖, medium and thumb 大小圖片

<%= image_tag @user.avatar.url %>
<%= image_tag @user.avatar.url(:medium) %>
<%= image_tag @user.avatar.url(:thumb) %>

controllers/dashboard/welcome_controller.rb

class Dashboard::WelcomeController < ApplicationController
  def index
    @user = current_user
  end

  def update_avatar
    if User.update(current_user, avatar_params)
      redirect_to action: :index
    end
  end

  private

  def avatar_params
    params.require(:user).permit(:avatar)
  end
end

config/routes.rb

namespace :dashboard do
  root 'welcome#index'
  patch 'welcome/update_avatar'
end

參數說明

hash

has_attached_file :avatar, {
    :url => "/system/:hash.:extension",
    :hash_secret => "longSecretString"
}

image

has_attached_file :avatar, :styles => {:thumb => 'x100', :croppable => '600x600>', :big => '1000x1000>'}
has_attached_file :cover, :styles => {:small => 'x100', :large => '1000x1000>'}
has_attached_file :sample, :styles => {:thumb => 'x100'}

Dynamic Processor

has_attached_file :avatar, :styles => lambda { |attachment| { :thumb => (attachment.instance.boss? ? "300x300>" : "100x100>") } }

has_attached_file :avatar, :processors => lambda { |instance| instance.processors }
attr_accessor :processors

不檢查檔案格式一定要加上

do_not_validate_attachment_file_type :client_uploading

尺寸的符號

:styles => { :medium => "300x300>", :thumb => "100x100>" }

儲存路徑參數

不同大小的 avatar

url: "/:class/:id/avatar/:style.:extension",
path: ":rails_root/public:url",

/users/4/avatars/original.jpeg?1436890118
/users/4/avatars/medium.jpeg?1436890118
/users/4/avatars/thumb.jpeg?1436890118

檔案不公開, 在根目錄建立 private (與 public 同層)

url: "/:class/:id/:basename.:extension",
path: ":rails_root/private:url"

/users/3/profile.png

Custom path

url: "/:class/:uuid/avatar/:style.:extension"

Paperclip.interpolates :uuid do |attachment, style|
  attachment.instance.uuid              # uuid 是欄位名稱
end

Default url

預設

default_url: "/images/:style/missing.png",

如果要使用 assets/images 下的圖片,必須這樣設置,在 production 才會正確顯示出來

default_url: ->(attachment) { ActionController::Base.helpers.image_path('icons/avatar.png') },

其他

刪除檔案

u.avatar = nil
u.save(validate: false)

Private 檔案下載

if @case && @case.original_file && File.exist?(@case.original_file.path)
    send_file @case.original_file.path
end

判斷是否已上傳檔案

@users.avatar.exists?

必須上傳圖片

validates :photo, presence: true

Paperclip + Crop

簡易 crop

加上 convert_options

has_attached_file :avatar,
styles: { medium: "300x300>", thumb: "100x100>" },
url: "/:class/:id/avatar/:style.:extension",
path: ":rails_root/public:url",
convert_options: {
  #the gravity parameter takes "Center" and directions like "northeast, nort, west"
  :thumb => "-gravity northwest -crop 100x100+0+0 +repage", #crop to 100x100 starting at upper left corner (northwest)
  :medium => "-gravity northwest -crop 101x100+100+0 +repage", #crop to 100x100 starting 100 pixels to the right of the upper left corner
}

使用 papercrop

Gemfile

gem 'papercrop', '~> 0.3.0'

application.js

//= require jquery.jcrop
//= require papercrop

application.scss

*= require jquery.jcrop

controller

def upload_avatar
  current_user.update(avatar_params)
  redirect_to action: :edit
end

def crop_avatar
  current_user.update(crop_params)
  redirect_to action: :edit
end

def avatar_params
  params[:user].permit(:avatar)
end

def crop_params
  params[:user].permit(:avatar_original_w, :avatar_original_h, :avatar_aspect, :avatar_box_w, :avatar_crop_x, :avatar_crop_y, :avatar_crop_w, :avatar_crop_h)
end

model

has_attached_file :avatar,
  styles: { medium: "300x300>", thumb: "100x100>" },
  url: "/:class/:id/avatar/:style.:extension",
  path: ":rails_root/public:url",
  processors: [:papercrop]              # 加上它
validates_attachment :avatar, content_type: { content_type: ['image/jpeg', 'image/png', 'image/gif'] }, size: { in: 0..10.megabyte }
crop_attached_file :avatar              # 加上它

view

上傳的 view
<%= form_for @user, url: upload_avatar_info_user_path(@user), method: :PATCH do |f| %>
  <%= image_tag @user.avatar.url %>
  <%= image_tag @user.avatar.url(:thumb) %>
  <%= image_tag @user.avatar.url(:medium) %>
  <%= f.file_field :avatar, as: :file %>
  <%= f.submit 'Save' %>
<% end %>

crop 的 view
<%= form_for @user, url: crop_avatar_info_user_path(@user), method: :PATCH do |f| %>
  <%= f.cropbox :avatar, width: 500 %>
  <%= f.crop_preview :avatar, width: 100 %>
  <%= f.submit 'Save' %>
<% end %>

crop 不會修改 original 的圖片只會改 thumb 及 medium 的圖

Paperclip 上傳到 S3

安裝 & 設定

Install aws-sdk

gem "paperclip", "~> 5.0.0.beta2"           # 注意! 5 版以上才支援 aws-sdk 2 版
gem 'aws-sdk', '~> 2.2.37'
  1. 先到 IAM 建立一個有 S3 權限的 User, 並記下 Access Key ID Secret Access Key

  2. 到 S3 -> Create Bucket -> 設定 Policy, 設定後簡單使用 command 上傳測試是否 work, 請參考此篇 - AWS-SDK / AWS-CLI 上傳及下載

  3. 設定給 paperclip 讀取 s3 的 config。可以設定在 config/application.rb

    config.paperclip_defaults = { storage: :s3, s3_region: ‘ap-northeast-1’, s3_credentials: { bucket: ‘my-bucket’, access_key_id: ‘Access Key ID’, secret_access_key: ‘Secret Access Key’, } }

  4. Model 的 Paperclip 設定

    s3_host_name: “s3-ap-northeast-1.amazonaws.com”, url: “/:class/:attachment/:id/:style.:extension”, path: “:url” # path 跟 url 一樣就好了

  5. 輸出 @product.photo.url(:thumb) 它就會自動組出正確的 url 了

    http://s3-ap-northeast-1.amazonaws.com/my-bucket/products/photos/1/thumb.jpeg?1461739010

  6. 完成

區分 Development 使用本機空間, Production 使用 S3

  1. 加上環境判斷, config/application.rb

    if Rails.env.production? config.paperclip_defaults = { … } end

  2. (選項)區分 path

    如果是 development 上傳到網站根目錄的 /public

    path: (Rails.env.production?) ? “:url” : “:rails_root/public:url”

    s3_host_name 不需處理它, 即使 development 存在這個參數也無妨

    s3_host_name: “s3-ap-northeast-1.amazonaws.com”,

  3. 完成! 記得要重啟動 Rails