Rails-Treasure chest3 嵌套表单; Ransack(3900✨)用于模糊查询, ranked-model(800🌟)自订列表顺序; PaperTrail(5000✨)跟踪model's data,auditing and versioning.

  •  自订列表顺序, gem 'ranked-model'

  • 多步骤表单

  • 显示资料验证错误讯息

  • 资料筛选和搜寻, gem 'ransack' (3900✨);

  • 软删除和版本控制

  • 数据汇出(csv),


 

自订列表顺序:ranked-model( 800✨)     https://github.com/mixonic/ranked-model

gem 'ranked-model'

简单使用:

为Event增加一个column, :row_order,type是integer,加上index。

  1. 在model层加上include RankedModel换行ranks: row_order
  2. 在controller层使用: Event.rank(:row_order).all
  3. 更新一条记录的顺序  @event.update(:row_order_position, 0)
    • 第二个参数,可以是数字,或者:first, :last, :up, :down方法。

如果使用一个普通的json controller, @event.attributes = params[:event]; @duck.save。

$.ajax({
  type: 'PUT',
  url: '/ducks',
  dataType: 'json',
  data: { duck: { row_order_position: 0 } },  
});

在routes.rb中定义一个路径:member {post :reorder}

link_to "上移", reorder_admin_event_path(event, :position => :up), method: :post

link_to "下移", reorder_admin_event_path(event, :position => :down), method: :post

在路径中加上请求参数"position": "up"

 

复杂使用:

ranks接受几种参数:

class Duck < ActiveRecord::Base

  include RankedModel

  ranks :row_order,           # 使用rank(),来定义一个ranker
    :column => :sort_order    # 加载这个默认列, which defaults to the name
  
  belongs_to :pond
  ranks :swimming_order,
    :with_same => :pond_id    # Ducks belong_to Ponds, 让ranker围绕一个pond
  
  scope :walking, where(:walking => true )
  ranks :walking_order,
    :scope => :walking        # Narrow this ranker to a scope

end

当你查询时,使用rank():
Duck.rank(:row_order)

Pond.first.ducks.rank(:swimming_order)

Duck.walking.rank(:walking)
 

 


Ajax UI

所谓的 Ajax 拖拉 UI,就是直接用鼠标进行拖拉排序,这种方式对用户来说操作速度更快。
拖拉的 UI 需要额外的前端套件,这里介绍 jQuery UI 的 Sortable Plugin,并直接使用 jquery-ui-rails 这个 gem 来安装.
 
知识点:
cursor property指定了当鼠标的光标在一个元素上的时候,显示什么样式

案例(博客地址):https://www.cnblogs.com/chentianwei/p/9443664.html

 



  

多步骤表单

(Multi Step Form,又叫做 Wizards)

什么时候会用到呢? 当表单很复杂的时候,我们不希望一次就把所有字段显示出来,这样会吓跑用户。而是会拆成步骤一、步骤二、步骤三.... 一步一步让用户掉入这个坑完成表单,以增加表单完成的成功率。

要制作的 UI 将拆分成三个表单:

  • 第一个表单: 选票种
  • 第二个表单: 填姓名、E-mail、电话
  • 第三个表单: 填个人网站URL、填自我介绍

其中第二个表单和第三个表单,除了有下一步之外,也可以回到上一步进行修改。如果用户中途离开,下次再进来也可以继续编辑。

另一种纯前端的做法,例如 jQuery Steps,则是只用特效的方式拆成不同步骤,而没有将过程储存进到数据库,如果中离就毫无纪录。本章的做法是中间过程都会存进数据库。

 

实做简介:

第一步:拆路径;原本的controller是new和create, 现在拆成三部分。

resources :registrations do
# 增加1-3步的url指向contoller#action,并对url使用别名:
 member do
  get 'steps/1' => "registrations#step1", as: :step1
  patch "steps/1/update" => "registrations#step1_update", as: :update_step1
  get "steps/2" => "registrations#step2", as: :step2
  patch "steps/2/update" => "registrations#step2_update", as: :update_step2
  get "steps/3" => "registrations#step3", as: :step3
  patch "steps/3/update" => "registrations#step3_update", as: :update_step3
 end
end

注意: step1是step2返回到step1的路径,因为第一步是new和create,已经创建了记录就无需再创建了。

 

第二步:写各个步骤的controller和view。

controller:

def step2
 @registration = @event.registrations.find_by_uuid(params[:id])
end

def step2_update
 @registration = @event.registrations.find_by_uuid(params[:id])

 if @registration.update(registration_params)
  redirect_to step3_event_registration_path(@event, @registration)
 else
  render "step2"
 end
end

view:

拆原来的form,注意几点:

  1. step2的form_for需要加上url参数, url指向的是#step2_update, HTML动作是patch
  2. 实例变量是form_for的参数

<%= form_for @registration, :url => update_step2_event_registration_path(@event, @registration) do |f| %>

 

第三步: 写各个步骤的返回上一步功能:

  1. view的表格底部加上连接link_to,url的HTML动作是get.
  2. 第2步返回第一步,使用的是自定义的路径step1_event_registration_path(), 这是因为返回到第一步是修改而不是新建,已经创建了记录就无需再创建了,所以需要从新自定义一个返回step1的路径。

第四步: validates,因为拆成几步,所以每步的验证也就不一样了。对每一步编号,然后根据编号来进行验证。

Model层:

  1. 加上实例变量attr_assessor :current_step
  2. 对validations_presence_of :name,...,后面加上:if进行判断,if返回true或false,需要定义一个方法。

validates_presence_of :name, :email, :cellphone, if: :should_validate_basic_data?

def should_validate_basic_data?
 current_step == 2
end

 

controller层:

create, step1_update, step2_update, step3_update, 加上@registration.current_step =  1|2|3


 

 

 显示资料验证错误讯息

服务器验证, 和前端验证两种:

 

服务器验证:

<div class="form-group <%= (f.object.errors[:name].any?)? "has-error" : "" %>">
  <%= f.label :name %>
  <%= f.text_field :name, :class => "form-control" %>

  <% if f.object.errors[:name] %>
  <span class="help-block"><%= safe_join(f.object.errors[:name], ',')%></span>
  <% end %>
</div>

has-error和help-block是bootstrap3的类方法。

可以手动实现这个效果, 先对text_filed加上border-color,然后,给<span>加上style: "color: red;"

f.object 指的是这个 form_for 表单的 model 物件,也就是 @registration

f.object.errors[字段名称] 是个数组储存了这个字段的错误讯息

 

safe_join(arrary, sep=$,)

和Array#join(Sep=$)功能类似,先flatten,然后遍历map每个item,再使用分隔符号join每一个item,然后使用html_tag,让内含的html tag脱逸(escape check),并返回最后的结果:一个大string。


自订义资料验证的错误显示

Model层使用:

valdate :check_something, on: :create

def check_something

 if 条件

  errors.add(:base, "messages")

 end

end

如果错误不是发生在attribute上,则使用:base。

 

controller上:

可以使用flash.now[:alert] = @registration.errors[:base].join(",")

now代表只在当前页面显示flash

 

view上:

<% if notice %>
 <p class="alert alert-success"><%= notice %></p>
<% end %>

notice是flash的方法,是flash[:notice]的简写。


  

前端资料验证:

input中添加required特性。即可。

上述的 HTML5 验证是浏览器内建的,如果想要更漂亮的特效,我们可以考虑安装其他前端的套件。

参考 10 jQuery Form Validation Plugins 我们挑一套 Bootstrap Validator 来试试看。

这个前端套件没有包好的 Gem 可以安装,请手动下载 validator.min.js 这个 javascript 档案,放在 vendor/assets/javascripts/ 目录下。

然后修改 app/assets/javascripts/application.js 加载它

app/assets/javascripts/application.js
+  //= require validator.min
   //= require_tree .

view中:

字段上添加: <div class="help-block with-errors"></div>
javascript代码:
+ <script> + $("form").validator(); + </script>
 

前端验证是不可靠的,用户只要关闭浏览器的 JavaScript 就可以跳过前端验证。

以防万一,还是需要后端验证的,如果前端验证失效时,至少还可以看到错误讯息。

 


 


 

资料筛选和搜寻

  •  资料筛选,单选/多选
  •  时间区间筛选
  •  资料对比筛选
  •  关键字search
  •  前台活动状态筛选

资料筛选,单选

需求:点选按钮,根据状态或票种(单选)来从数据库中查询报名人资料。

view:

增加一排按钮,2组按钮,1组是根据status来查询,2组是根据ticket_id来查询。

重点是按钮传递的参数:

  • 根据status来传递参数admin_event_registrations_path(status: s)
  • 根据ticket_id来传递参数admin_event_registrations_path(ticket_id: t.id)

controller:

根据request参数的不同,来从数据库查询不同的资料。

if params[:status] && Registration::STATUS.include?(params[:status])
    return @registrations = @event.registrations.includes(:ticket).where(status: params[:status])
end

if params[:ticket_id]
 return @registrations = @event.registrations.includes(:ticket).where(ticket_id: params[:ticket_id])
end

 

然后进行重构:

第一,把query查询语法放到Model中。scope :by_status, ->(s){ where( status: s )}

第二,在页面的按钮组上,每个按钮上显示查询记录的数量。

  • link_to "全部#{@event.registrations.size}", admin_event_registrations_path(@event)
  • link_to t(s, scope:"registration.status") + "#{@event.registrations.by_status(s).size}", admin_event_registrations_path(status: s)

第三,凡事点击的按钮,要凹陷下去,通过增加css属性给<a>tag。或者使用bootstrap的active类。

  • 在class中添加条件判断:#{(params[:status].blank?)? "active" : "" }
  • 或者 #{(params[:status] == s)? "active" : ""}

 

这个功能目前不管用:也非单选需求下的功能:

目的在于建构按钮超连结的参数。当点了状态再点票种,或是点了票种再点状态时,要同时套用两个参数。

app/helpers/admin/event_registrations_helper.rb

   module Admin::EventRegistrationsHelper
+
+   def registration_filters(options)
+     params.permit(:status, :ticket_id).merge(options)
+   end
+
  end

 

筛选资料(多选)

使用核选方框。

实务上,单选和多选的作法不太会混用,所以这里会注解掉上一节的单选接口(用if false去掉)

 

使用的HTML tag:

1.   form_tag(url, method)

2.  check_box_tag(name, value, checked="false")

 

controller中因为是多项条件的筛选。应当是where内用and, or对参数进行整合的运用。

@registrations = @event.registrations.includes(:ticket).order("id DESC").page(params[:page]).per(20)

if Array(params[:statuses]).any?
   @registrations = @registrations.where(:status => params[:statuses])
end

if Array(params[:ticket_ids]).any?
   @registrations = @registrations.where(:ticket_id => params[:ticket_ids])
end

见⬆️, 经过2次if的条件筛选得到一个查询语法,如:

SELECT * FROM "registrations" WHERE "registrations"."event_id" = 32 AND "registrations"."status" = 'pending' AND "registrations"."ticket_id" IN (7, 9) ORDER BY id DESC LIMIT 20 OFFSET 0  

 

不理解的是Array(params[..])?

答案:

因为,请求参数中包括"statuses"=>["pending"], "ticket_ids"=>["7", "9"],  它们的值是2个Array,它们是在check_box_tag中定义的name="statuses[]"和name="ticket_ids[]"。

所以,在controller中需要用判断这2个Array值是否存在,使用Array(params[...]), 进一步确保传进来的参数值是数组。

另外,直接写params[...]作为if的条件,来判断是否存在也可以。


 


 

 

时间区间筛选

date_field(object_name, method, options={})

返回一个text_field 类型是date。

options包括 value:"1984-05-11",  min: Date.today, max: "2099-10-11"属性。

 

date_filed_tag(name, value=nil, options={})

options包括,max, min, 和text_field_tag相同的参数如:disabled,

 

view:

<p>
  报名日期:<%= date_field_tag :start_on, params[:start_on] %>~<%= date_field_tag :end_on, params[:end_on] %>
</p>

controller:

if params[:start_on].present?
   @registrations = @registrations.where("created_at >= ?", Date.parse(params[:start_on]).beginning_of_day)
end

传递进来的参数是"2018-01-01"的字符串。需要使用parse()转化为日期,然后用beginning_of_day转化为当前设置的时区的时间。

present?看一个对象是否存在。!blank?


 

 

资料比对筛选

需求:可以输入报名编号搜寻报名人资料

controller:

if params[:registration_id].present?
 @registrations = @registrations.where(:id => params[:registration_id].split(","))
end

view:

text_field_tag :registration_id, params[:registration_id], :placeholder => "报名编号,可用,号区隔", :class => "form-control"


 


 

关键字搜寻,使用 Ransack (3900✨)

to search a place very thoroughly, ofen making it untidy.

使用Ransack创建既简单又先进的搜索表格form。支持Rails5.0以上。

ransack 会用数据库的 LIKE 语法来做搜寻,虽然用起来方便,但它会逐笔检查资料是否符合,而不会使用数据库的索引。如果数据量非常多有上万笔以上,搜寻效能就会不满足我们的需要。这时候会改安装专门的全文搜寻引擎,例如 Elasticsearch,这是大数据等级的。


 

安装即用gem 'ransack',

 

简单使用(复杂使用没有看)

注意:

  • 搜索参数的默认params key是  :q   , 
  • form_for ->  search_form_for, 验证一个Ransack::Search object 会被传给search_form_for
  • ActiveRecord::Relation methods不再delegated通过搜索对象。你可以通过使用Ransack#result 搜索结果

 在controller中:

@q = Person.ransack(params[:q])

@people = @q.result(distinct: true)    #使用distinct:true去掉重复的查询结果。

如果使用关联的表格列:

@q = Person.ransack(params[:q])

@people = @q.result.includes(:articles).page(params[:page]).to_a.uniq

 #使用to_a.uniq移除重复,也可以在view中实现。

 

 在view中:

又2个helper方法sort_link,search_form_for

Ransack's search_form_for helper replaces form_for for creating the view search form

<%= search_form_for @q, url: admin_event_registrations_path(@event) do |f| %>
 <P><%= f.search_field :name_cont, :placeholder => "姓名", class: "form-control"%></p>
 <P><%= f.search_field :email_cont, :placeholder => "E-mail", class: "form-control"%></p>

 

:name_cont中的cont是contains包括,这是search predicates搜索谓语。详见:list  , wiki


 

 

什么是搜索谓语?

在Ransack搜索中, Predicates用于决定匹配什么信息。例如:cont predicate 会核查是否一个属性包含一个值,通过使用一个wildcard query(通配符查询)。

例子:

> User.ransack(email_cont: "candy@")
=> Ransack::Search<class: User, base: Grouping <conditions: [Condition <attributes: ["email"], predicate: cont, values: ["candy@"]>], combinator: and>>
> User.ransack(email_cont: "candy@").result
User Load (0.2ms) SELECT "users".* FROM "users" WHERE ("users"."email" LIKE '%candy@%')
=> #<ActiveRecord::Relation []>
> User.ransack(email_cont: "candy@").result.to_sql
=> "SELECT \"users\".* FROM \"users\" WHERE (\"users\".\"email\" LIKE '%candy@%')"

 

可以和or, and  连用:

>> User.ransack(first_name_or_last_name_cont: 'Rya').result.to_sql
=> SELECT "users".* FROM "users"  WHERE ("users"."first_name" LIKE '%Rya%' 
   OR "users"."last_name" LIKE '%Rya%')

也可使用关联,假设User has_one Account,  Account 有属性foo, bar:

>> User.ransack(account_foo_or_account_bar: "var").result.to_sql

=> SELECT  * FROM "users" INNER JOIN accounts ON account.user_id = users.id WHERE( "accounts.foo LIKE '%var%' OR accounts.bar LIKE '%var%')

 

注意⚠️:对一个不存在的属性使用a predicate 会失败,where子句相当于不存在。

 

eq(equals) 

eq predicate returns all records where a field is exactly equal to a given value; 相反的有not_eq

 

matches

匹配查询所有记录并返回, 相反的有does_not_match

使用LIKE 'xxx',精确匹配, 而contain使用 LIKE '%xxx%'

 

lt (less than) 

gt(greater than)

gteq(greater than or equal to)

Iteq(less than or equal to)

  in  

>> User.ransack(age_in: 20..25)

>> User.ransack(age_in: [20, 21, 22, 23])

上面都是和数字相关的predicate

 

cont_all(contains all) 

city_cont_all: %w(Grand Rapids)必须包括所有关键字才满足条件,生成
WHERE (("users"."city" LIKE '%Grand%' AND "users"."city" LIKE '%Rapids%'))

not_cont_all 

cont_any( contains any) 

first_name_cont_any: %w(Rya Lis)) 包括任意关键字即可 ,生成
WHERE (("users"."first_name" LIKE '%Rya%' OR "users"."first_name" LIKE '%Lis%'))

not_cont_any 

 

start(starts with)

LIKE "%xx"  开头是xxx, 类似正则表达式/^xxx/

end(ends with)

LIKe "xx%"  结尾是xxx, 类似正则表达式/xxx$/

 

true ,  false

 

The false predicate returns all records where a field is false.

 

>> User.ransack(awesome_false: '1').result.to_sql
=> SELECT "users".* FROM "users"  WHERE ("users"."awesome" = 'f')

present , blank

>> User.ransack(first_name_present: '1').result.to_sql
=> SELECT "users".* FROM "users"  WHERE (("users"."first_name" IS NOT NULL AND "users"."first_name" != ''))

null

>> User.ransack(first_name_null: 1).result.to_sql
=> SELECT "users".* FROM "users"  WHERE "users"."first_name" IS NULL

URL parameter structure

  Parameters: {"utf8"=>"✓", "q"=>{"name_cont"=>"", "email_cont"=>"cand"}, "registration_id"=>"", "statuses"=>["pending"], "start_on"=>"", "end_on"=>"", "commit"=>"送出筛选", "event_id"=>"hahaha-meetup"}

User.ransack(params[:q]) , 搜索参数被传入到ransack内是一个hash结构。q[:email_count]= "cand"

 

如果使用JavaScript来创建一个URL, 一个匹配的查询:

$.ajax({
  url: "/users.json",
  data: {
    q: {
      first_name_cont: "pete",
      last_name_cont: "jack",
      s: "created_at desc"
    }
  },
  success: function (data){
    console.log(data);
  }
});


 

 

软删除和版本控制

  • 在实际运作的网站中,用户可能会不小心删除资料, 用户可能会透过客服请求管理员进行复原。
  • 针对重要的资料,建立追踪和稽核的机制。

软删除(Soft Deletion):不真的删除这一笔资料,常见的作法是增加一个删除的标记字段(例如 deleted_at字段),如果被标记删除了,那就不要显示出来即可。

版本控管:建立一个 Version Model 来存储编修纪录。如果本来的资料被删除或修改,则会复制资料到这个 Model 去。 使用 paper_trail gem


 

 

Paper_trail 一个流行的版本控制gem (5000✨)

 

1. Introduction

当一个类,如Registration在model层加上has_paper_trail,就意味它加入了version control版本控制。

 

Version数据库根据属性item_type找到Registration, 根据属性item_id找到它的对应记录。


 

例子:

假设一条记录registration,经过3次数据update。那么在Version中同步insert into 3条记录

INSERT INTO "versions" ("item_type", "item_id", "event", "object", "created_at", "object_changes") VALUES (?, ?, ?, ?, ?, ?)

event字段储存的是create, update, destroy等方法。(也可以客制化event names)

object字段储存未改的记录,

object_changes储存记录更新或改变。

 

使用registration.versions方法 得到这条记录的所有之前的版本信息,返回一个数组集合,如果没有版本变化返回空数组。

#<ActiveRecord::Associations::CollectionProxy

[#<PaperTrail::Version id: 6, item_type: "Registration", item_id: 1004, event: "update", whodunnit: nil, object: "...", created_at: "...", object_changes: "...">,

#<PaperTrail::Version id: 7, item_type: "Registration", item_id: 1004, event: "update", whodunnit: nil, object: "...", created_at: "...", object_changes: "...">]

>

使用r = registrations.versions.last方法,得到最近一次的version变化。

r.event 等方法获得对应的字段的值。

r.whodunnit 得到current_user的🆔,需要先设置set_paper_trail_whodunnit 回调在admin_controller.rb中。

r.reify  让r实例化,把Version的记录转化为Registration的记录。(如果是create event返回nil)

r.reify.name 看实例化后的registration的属性值

r.reify.save  让Registration中的记录恢复到这个版本


 

 

2. Limiting What is Versioned, and When

监听Events的生命周期。

has_paper_trail on:[:update]

可以限制使用callback,默认的有4个on: [:create, :destroy, :touch, :update]

versions.event列的值默认有三个,create, destroy, update(包括touch, update两个回调)

 

使用if, unless来设置什么条件来保存新的version

class Translation < ActiveRecord::Base
  has_paper_trail if:     Proc.new { |t| t.language_code == 'US' },
                  unless: Proc.new { |t| t.type == 'DRAFT'       }
end

 

使用Attributes来Monitor监听。

ignore, only, skip可以设置attributes的监听取舍。

比如has_paper_trail ignore: [:title, :description], 则这记录仅变化2个属性的值,不会增加一条version。

ignore, only也接受Hash参数,这样就可以使用块变量了,如:

has_paper_trail only: { title: Proc.new { |obj| !obj.title.blank? } }

当title不为空,并变化,增加一条version record。其他属性不版本控制。

 

skip除了ignore的功能,还有一个功能:其他原因创建的version记录, 属性不会留存( 不懂😢 )


 

3. Working With Versions

这章节介绍了具体的使用:如何定位到一个版本,然后进行相关操作。

 

3b

一条记录.paper_trail.previous_version

previou_version, next_version 它们默认包括了reify方法.

一条记录.live?  #返回boolean,如果这条记录对象是储存在加版本控制的model中的就返回true,如果是储存在Version中的就返回false。

 

3c 区别版本

在开始使用rails g paper_trail:install --with-changes中的--with-change option会增加一个column, object_changes

每次version update都会把变化的属性值存入这个字段,可以使用version.changeset方法来检索它

widget = Widget.create name: 'Bob'
widget.versions.last.changeset
# {
#   "name"=>[nil, "Bob"],
#   "created_at"=>[nil, 2015-08-10 04:10:40 UTC],
#   "updated_at"=>[nil, 2015-08-10 04:10:40 UTC],
#   "id"=>[nil, 1]
# }

<% version.changeset.each do |key, value| %>
    <li><%= key %>从<%= value[0] || "'无'"%>改成<%= value[1]%></li>
<% end %>


 

posted @ 2018-08-07 17:54  Mr-chen  阅读(319)  评论(0编辑  收藏  举报