• 自订列表顺序, 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的

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

  1. Rails-Treasure chest2 嵌套表单;

    嵌套表单1-1 嵌套表单1-多 选日期时间的UI (一个jquery Plugin) 拆除前后台css和js Rich Editor, 显示输入的HTML tag 批次编辑/删除 嵌套表单1-1 核心 ...

  2. 对一个表中所有列数据模糊查询adoquery

    如何用adoquery对一个表中所有列进行模糊查询: procedure TForm3.Button4Click(Sender: TObject); var ASql,AKey: string; I: ...

  3. Rails中关联数据表的添加操作(嵌套表单)

    很早就听说有Web敏捷开发这回事,最近终于闲了下来,可以利用业余的时间学些新东西,入眼的第一个东东自然是Ruby on Rails.Rails中的核心要素也就是MVC.ORM这些了,因此关于Rails ...

  4. Oracle 表分组 group by和模糊查询like

    分组group by写法 select 字段名 from 表名 group by 字段名 查询这个字段名里的种类分组后可以加聚合函数select 字段名,聚合函数 from 表名 group by 字 ...

  5. IOS-CoreData(增删改查、表关联、分页和模糊查询、多个数据库)

    1>什么是CoreData Core Data是iOS5之后才出现的一个框架,它提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数 ...

  6. javascript 提取表单元素生成用于提交的对象(序列化 html 表单)

    function serialize(f) { var o = {}; var s = f.getElementsByTagName("select"); for (var i = ...

  7. 【Reporting Services 报表开发】— 怎么根据当前表单的guid作为参数查询相关数据?

    select AId from FilteredA as CRMAF_FilteredA 用这个 作为一个DataSet1 , 然后添加在报表里面添加一个参数 @AId,设置的默认的查询为前面Data ...

  8. selenium——表单嵌套

    <html> <iframe id="id-iframe" name="iframee1"> --切换表单 <html> & ...

  9. Angular 表单嵌套、动态表单

    说明: 组件使用了ng-zorro (https://ng.ant.design/docs/introduce/zh) 第一类:嵌套表单 1. 静态表单嵌套 demo.component.html & ...

随机推荐

  1. IIS Admin Service 服务由于下列服务特定错误而终止: 无效签名。

    于是查看系统日志: 具体信息如下:日志名称:          System来源:            Service Control Manager日期:            2015/11/2 ...

  2. 【gulp】前端自动化工具---gulp的使用(一)------【巷子】

    什么是gulp?   基于node的自动化构建工具   扩展:开发的时候分为2个节点一个是开发阶段  另一个是部署阶段        开发阶段:源文件不会被压缩            部署阶段:所有文 ...

  3. lambda 缩写推演

  4. Warm up---hdu4612(缩点,树的直径)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4612 给一个无向图, 加上一条边后,求桥最少有几个: 那我们加的那条边的两个顶点u,v:一定是u,v之 ...

  5. python通过os.walk() 遍历出多级目录下所有文件绝对路径

    代码如下 将遍历出来的路径全部添加到列表中: def get_all_abs_path(source_dir): path_list = [] for fpathe, dirs, fs in os.w ...

  6. php中使用Curl、socket、file_get_contents三种方法POST提交数据

    抓取远程内容,之前一直都在用file_get_content函数,其实早就知道有curl这么一个好东西的存在,但是看了一眼后感觉使用颇有些复杂,没有file_get_content那么简单,再就是需求 ...

  7. PAT 1082 Read Number in Chinese[难]

    1082 Read Number in Chinese (25 分) Given an integer with no more than 9 digits, you are supposed to ...

  8. The same month as the adidas NMD Singapore is releasing

    Earlier this December 2017, the inaugural adidas NMD Singapore silhouette released in the first colo ...

  9. Excel error 64-bit version of SSIS

    问题是 在windows server 2008 64位的计划任务执行 ssis 的错误 ,ssis你们带有读取excel 日期 2015/3/17 11:50:34日志 作业历史记录 (SSIS_U ...

  10. 测试人必备:国内外最好用的6款Bug跟踪管理系统

    在移动互联网产品中,Bug会导致软件产品在某种程度上不能满足用户的需要.确保一个项目进展顺利,关键在于妥善处理软件中的BUG,那么,如何高效的管理BUG,解决BUG?在这里,我为大家搜集了几款优秀的B ...