Skip to content

Customizing Filters

denispeplin edited this page Jan 11, 2013 · 14 revisions

Setup

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

Basic Filters

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:

Restricting filters

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.

Filters Explained

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.

Extending Filter Objects

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:

Adding Default Filters

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.

Disabling Empty Filters

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.

Changing Attribute Names

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.

Adding Custom Conditions

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.

Joining Models Together

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:

Conclusion

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.