-
Notifications
You must be signed in to change notification settings - Fork 76
Customizing Filters
In this document we will be using 3 models: User, Event and EventUser so we can demonstrate some amazing filtering capabilities of the gem. Here are the table definitions of the models.
Users Table
create_table :users do |t| t.string :first_name t.string :last_name t.date :birthday t.string :sex t.timestamps end
Events Table
create_table :events do |t| t.integer :creator_id t.string :type t.string :name t.string :headline t.datetime :start_time t.datetime :end_time t.timestamps end add_index :events, [:creator_id]
Event Users Table
create_table :event_users do |t| t.integer :event_id t.integer :user_id t.timestamps end add_index :event_users, [:event_id] add_index :event_users, [:user_id] add_index :event_users, [:event_id, :user_id]
Here are the actual model objects with their associations:
User Model
class User < ActiveRecord::Base has_many :events has_many :events_users end
Event Model
class Event < ActiveRecord::Base belongs_to :user, :class_name => 'User', :foreign_key => :creator_id has_many :event_users end
EventUser Model
class EventUser < ActiveRecord::Base belongs_to :event belongs_to :user end
We will also use a samples_controller with an index action. The controller code looks like this:
class SamplesController < ApplicationController def index # All examples assume you are placing the code in here end end
The index.html.erb will simply look like this:
<%= will_filter_tag(@users)%>
In some cases we will use @users in other cases @events or @event_users
Creating a basic filter is just a matter of adding the following lines of code to your controller:
class OrdersController < ApplicationController def index @users = User.filter(:params => params) end end
And adding a filter display tags to your page: (index.html.erb)
<%= will_filter_tag(@users) %>
If you would like to display the will_filter table, you can add the following tag right below the filter tag: (index.html.erb)
<%= will_filter_table_tag(@orders) %>
Note: Since we are not going to be looking at the table tag here, assume it is always present on the page.
This will create the following filter on the page:
Filter with conditions will look like:
To exclude some attributes from filtering form, create custom filter:
class UserFilter < WillFilter::Filter def definition super.except(:id, :created_at, :updated_at) end end
and change filter call to use it:
@users = User.filter(:params => params, :filter => :user_filter)
In this example, attributes :id, :created_at and :updated_at are excluded.
What is a filter? Well, filter is simply a collection of conditions and some additional attributes, like pagination, sorting, etc…
A filter condition is comprised of 3 elements: model attribute key, operator and container. Operators and containers options are dynamically discovered based on the column data type.
If you look at the config/will_filter/config.yml you will see that operators and containers can be easily added or modified based on the application needs.
Here is what the config looks like:
containers: # container implementation mapping nil: WillFilter::Containers::Nil numeric: WillFilter::Containers::Numeric numeric_range: WillFilter::Containers::NumericRange numeric_delimited: WillFilter::Containers::NumericDelimited double: WillFilter::Containers::Double double_range: WillFilter::Containers::DoubleRange double_delimited: WillFilter::Containers::DoubleDelimited date_time_range: WillFilter::Containers::DateTimeRange single_date: WillFilter::Containers::SingleDate date: WillFilter::Containers::Date date_time: WillFilter::Containers::DateTime date_range: WillFilter::Containers::DateRange text: WillFilter::Containers::Text text_delimited: WillFilter::Containers::TextDelimited boolean: WillFilter::Containers::Boolean list: WillFilter::Containers::List filter_list: WillFilter::Containers::FilterList data_types: # mapping between data types and containers bigint: [nil, numeric, numeric_range, numeric_delimited] numeric: [nil, numeric, numeric_range, numeric_delimited] smallint: [nil, numeric, numeric_range, numeric_delimited] integer: [nil, numeric, numeric_range, numeric_delimited] int: [nil, numeric, numeric_range, numeric_delimited] float: [nil, double, double_range, double_delimited] double: [nil, double, double_range, double_delimited] timestamp: [nil, date_time, date_time_range, single_date] datetime: [nil, date_time, date_time_range, single_date] date: [nil, date, date_range] char: [nil, text, text_delimited] character: [nil, text, text_delimited] varchar: [nil, text, text_delimited] text: [nil, text, text_delimited] text[]: [nil, text, text_delimited] bytea: [nil, text, text_delimited] boolean: [nil, boolean] tinyint: [nil, boolean] operators: # operators precedence is: 100 is_not: 200 is_on: 300 is_in: 400 is_provided: 500 is_not_provided: 600 is_after: 700 is_before: 800 is_in_the_range: 900 contains: 1000 does_not_contain: 1100 starts_with: 1200 ends_with: 1300 is_greater_than: 1400 is_less_than: 1500 is_filtered_by: 1600
The data_types section maps each data type to an array of available containers, while each container provides information on which operators it supports.
The entire framework is very customizable so you can add custom containers, operators and data types.
Let’s look at the basic user filter again. Notice that when we choose the “sex” attribute and “is” operator, we get a text container. So the user has to type in the sex value.
Here is what it looks like:
Well, that’s a little annoying that we have to type sex value every time. Sex can only have 3 options: male, female and unknown.
Let’s extend the basic filter and map the ‘is’ operator to the ‘list’ container. Here is what the code will look like:
class UserFilter < WillFilter::Filter def model_class User end def definition defs = super defs[:sex][:is] = :list defs[:sex][:is_not] = :list defs end def value_options_for(condition_key) if condition_key == :sex return ["male", "female", "unknown"] end return [] end end
Since we are using a custom filter now, we have to change our filter call to the following:
@users = User.filter(:params => params, :filter => :user_filter)
This time when we choose the ‘sex’ attribute and ‘is’ container, we will see a drop down list instead of a text box:
The top left corner of the filter allows user to choose saved or default filters. Let’s see how we can add some default filters to the list.
All you really have to do is add a couple of methods, like this:
class UserFilter < WillFilter::Filter def model_class User end def default_filters [ ["Male Only", "male_only"], ["Females With Last Name 'Adams'", "adams"], ["Susans Born Between 2000 and 2010", "susans"], ] end def default_filter_conditions(key) return [:sex, :is, "male"] if (key == "male_only") return [ [:sex, :is, "female"],[:last_name, :is, "Adams"] ] if (key == "adams") if (key == "susans") return [ [:first_name, :is, "Susan"], [:birthday, :is_in_the_range, [Date.new(2000,1,1), Date.new(2010,1,1)] ] ] end end end
So now when we look at the default filter list, we can see those options. Selecting the options will pull the right conditions.
What if we don’t want to allow users to have empty filters, which by default return all results. If a table is large it’s a definite no-no.
Well, just add the following method to the filter:
... def default_filter_if_empty "male_only" end ...
Now when users looks at the page, the filter will be defaulted to the ‘male_only’ conditions.
What if we don’t like to use ‘sex’ to represent gender. We can easily correct that by adding the following method:
... def condition_title_for(key) return 'Gender' if key == :sex super end ...
Now sex will be replace with gender.
All of the above conditions will result in SQL statements that will be executed in the database and return results. But what if we need some conditions that are so complicated that it is nearly impossible to execute them in the database and we need some help from Ruby.
Well, not a big deal, let’s create a new condition, called “custom_condition” which will have an “is” operator and a couple of complicated options, like: “Third letter in the First Name is an ‘a’” and “Id is divisible by 3”
Good luck trying to write SQL statements for these conditions. But we can easily do this in the custom filter. Here is the code:
class UserFilter < WillFilter::Filter def model_class User end def definition defs = super defs[:custom_condition] = { :is => :list } defs end def value_options_for(condition_key) if condition_key == :custom_condition return [ ["Third letter in the First Name is an 'a'", 'third_letter_a'], ['Id is divisible by 3', 'divisible_by_3'] ] end return [] end def custom_conditions [:custom_condition] end def custom_condition_met?(condition, object) if condition.key == :custom_condition if condition.operator == :is if (condition.container.value == 'third_letter_a') return (object.first_name[2..2] == 'a') elsif (condition.container.value == 'divisible_by_3') return (object.id % 3 == 0) end end end return false end end
Not bad! So first, all sql conditions are executed on the server and the list of results is returned. Then the results are passed through custom conditions and only the records that meet the conditions are finally paginated and returned back.
Here is what it would look like:
You could easily add other conditions and operators and make filter do some amazing work for you.
What if we have a requirement to filter the EventUser object by the User and Event. This means that we need to join all those tree tables somehow.
Not a big deal. Here is the code for the EventUserFilter:
class EventUserFilter < WillFilter::Filter def model_class EventUser end def inner_joins [:user, :event] end def definitions defs = super defs[:"user.sex"] = {:is => :list, :is_not => :list} defs end def value_options_for(criteria_key) if criteria_key == :"user.sex" return ["male", "female"] end return [] end def default_filters [ ["Events That Start Tomorrow And Attended By Users With Name 'David'", "start_tomorrow_with_davids"], ["Events That Start Tomorrow And Attended By Laddies Who Are 18 And Older", "start_tomorrow_with_females"] ] end def default_filter_conditions(key) if (key == "start_tomorrow_with_davids") return [ [:"event.start_time", :is_on, Date.today + 1.day], [:"user.first_name", :is, 'David'] ] end if (key == "start_tomorrow_with_females") return [ [:"event.start_time", :is_on, Date.today + 1.day], [:"user.sex", :is, 'female'], [:"user.birthday", :is_after, Date.today - 18.years] ] end end end
The magic happens in the inner_joins method, which creates conditions out of the joined models.
Here is what it would look like:
The filtering framework is very powerful. The 3 tags: will_filter_tag, will_filter_table_tag and will_filter_action_bar_tag will allow you to create powerful admin pages in matter of minutes.
If you find bugs or would like more features, don’t hesitate to create tickets or contact us.