[Tech] Filter interface met named scope

Sinds versie 2.1 van Ruby on Rails kan je in ActiveRecord gebruik maken van named_scope, een manier om een subset van de inhoud van je tabel simpel aan te spreken. Voor een nieuw product was het nodig om een filter te implementeren. Via dit filter is het mogelijk de weergegeven items te filteren op een datum of status.

Facturen filteren

Dit artikel bespreekt de manier waarop we het filter geïmplementeerd hebben door gebruik te maken van named_scope.

Filter macro’s definieren

Allereerst hebben we in het model de scopes toegevoegd waarop we onze gebruikers willen laten filteren. Deze scopes geven de condities aan die Rails moet gebruiken om de items te vinden in de database.

named_scope :filter_all
named_scope :filter_this_month,   lambda { { :conditions => ["invoice_date >= ? and invoice_date <= ?", Date.today.at_beginning_of_month, Date.today.at_end_of_month] } }
named_scope :filter_draft,        :conditions => ["state = ?", "draft"]
named_scope :filter_open,         :conditions => ["state IN (?)", ["open", "late"]]

Elke scope hebben we een naam gegeven beginnende met ‘filter’ zodat het aanroepen vanuit de controller straks automatisch kan gebeuren, zonder gelijk het hele model open te zetten. Een ander interessant punt is het gebruik van een lambda bij de datum berekeningen in filter_this_month. Het gebruik van lambda zorgt ervoor dat de datum elke keer opnieuw berekend wordt en niet opgeslagen wordt bij het laden van de class.

Het omzetten van een request met filter actie naar een aanroep in het model gebeurt uiteraard in de controller. We hebben er voor gekozen de aanroepen van de scopes die we in het model gedefinieerd hebben rechtstreeks door te sturen naar het model via send. Een extra controle op toegestane filters zorgt ervoor dat niet zomaar een willekeurige methode aangeroepen kan worden in het model.

ALLOWED_FILTERS = %w{all this_month last_month this_year draft open late paid advanced}
def index
  if params[:macro].nil? or !ALLOWED_FILTERS.include?(params[:macro])
    @invoices = Invoice.filter_all
  else
    @invoices = Invoice.send("filter_#{params[:macro]}")
  end
end

Geavanceerd filteren

Naast de macro’s die wij gedefinieerd hebben, willen we onze gebruikers ook de mogelijkheid geven zelf een begin- en einddatum op te geven. Dit geavanceerde filter hebben we ook toegevoegd aan het model, ditmaal via een named_scope die een hash met opties accepteert. De lambda procedure maakt een correcte conditie voor het filter.

named_scope :filter_advanced,     lambda{ |options|
    where, arguments = [], []
    where << "state IN (?)"         and arguments << options[:states].map(&:to_s)  unless options[:states].nil? or options[:states].empty?
    where << "invoice_date >= ?"    and arguments << options[:from_date]           unless options[:from_date].nil?
    where << "invoice_date <= ?"    and arguments << options[:to_date]             unless options[:to_date].nil?
    { :conditions => [where.join(" AND "), *arguments] }
  }

Om ook dit filter te ondersteunen moeten we een extra geval toevoegen aan de controller:

ALLOWED_FILTERS = %w{all this_month last_month this_year draft open late paid advanced}
def index
  @filter = InvoiceFilter.new(params[:filter])
  if params[:macro].nil? or !ALLOWED_FILTERS.include?(params[:macro])
    @invoices = Invoice.filter_all
  elsif params[:macro] == "advanced"
    @invoices = Invoice.filter_advanced(@filter.options)
  else
    @invoices = Invoice.send("filter_#{params[:macro]}")
  end
end

De InvoiceFilter class controleert de opgeven waarden uit de parameters en parsed de datum velden. Daarnaast geeft het enkele standaardwaarden voor het filter als er niets opgegeven is, zodat @filter gebruikt kan worden bij het opbouwen van het formulier in de view.

class InvoiceFilter

  attr_reader :states, :from_date, :to_date

  def initialize(filter)
    unless filter.nil?
      @states = filter[:states]
      @from_date = Date.new(filter['from_date(1i)'].to_i, filter['from_date(2i)'].to_i, filter['from_date(3i)'].to_i)
      @to_date = Date.new(filter['to_date(1i)'].to_i, filter['to_date(2i)'].to_i, filter['to_date(3i)'].to_i)
    else
      @states = %w{draft open late paid}
      @from_date = Date.today.at_beginning_of_month
      @to_date = Date.today.at_end_of_month
    end
  end

  def options
    { :states => @states, :from_date => @from_date, :to_date => @to_date }
  end

end

De named_scope feature in Rails 2.1 heeft de implementatie van het filter zeker makkelijker gemaakt. Doordat het nu mogelijk is op een simpele manier deze condities mee te geven aan het model kan je de controller stukken schoner houden.

Deel dit artikel op je favoriete social netwerk