• 自订列表顺序, 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. Yii 各种url地址写法

    echo Url::home(); 生成入口地址/yii2test/frontend/web/index.php: echo  Url::base();生成入口文件夹地址:/yii2test/fron ...

  2. Session的存储原理

    一.session是怎么存储,提取的? 1.在服务器端有一个session池,用来存储每个用户提交session中的数据,Session对于每一个客户端(或者说浏览器实例)是“人手一份”,用户首次与W ...

  3. [.Net]System.OutOfMemoryException异常

    1. 一个异常情景 加载15000条等高线,平均每条线有400个点到三维球上,等待时间太长.而且可能会报内存异常. 2. 不错的分析 http://wenku.baidu.com/view/14471 ...

  4. c primer plus(五版)编程练习-第八章编程练习

    1.设计一个程序,统计从输入到文件结尾为止的字符数. #include<stdio.h> int main(void){ int ch; int i; i=; while((ch = ge ...

  5. 找回 linux root密码的几种方法

    第1种方法: 1.在系统进入单用户状态,直接用passwd root去更改  2.用安装光盘引导系统,进行linux rescue状态,将原来/分区挂接上来,作法如下: Java代码  #> c ...

  6. STL学习笔记--算法

    关于STL算法需要注意的是: (1) 所有STL算法被设计用来处理一个或多个迭代器区间.第一个区间通常以起点和终点表示,至于其他区间,多数情况下只需提供起点即可,其终点可自动以第一区间的元素数推导出来 ...

  7. [golang note] 接口使用

    侵入式接口 √ 在其他一些编程语言中,接口主要是作为不同组件之间的契约存在,即规定双方交互的规约. √ 对契约的实现是强制的,即必须确保用户的确实现了该接口,而实现一个接口,需要从该接口继承. √ 如 ...

  8. C++ Builder创建和调用dll中的资源

    程序开发中经常会用到一些图标.图片.光标.声音等,我们称它们为资源(Resource).当多个窗口用到同样的资源时,可以将这些公共的资源放到一个dll文件里调用,这样,由于定位资源比在磁盘中定位文件花 ...

  9. 26种基于PHP的开源博客系统

    26种基于PHP的开源博客系统 来源:本站原创 PHP学习笔记 以下列举的PHP开源Blog系统中,除了我们熟知的WordPress之外,大多都没有使用过,其中一些已经被淘汰,或者有人还在使用.除了做 ...

  10. FTRL与Online Optimization

    1. 背景介绍 最优化求解问题可能是我们在工作中遇到的最多的一类问题了:从已有的数据中提炼出最适合的模型参数,从而对未知的数据进行预测.当我们面对高维高数据量的场景时,常见的批量处理的方式已经显得力不 ...