Rails + Will paginate paginating a list presenter - ruby-on-rails

Today i am trying to present the items from a list that were paginated:
users = User.order('name').paginate(:per_page => 10, :page => 1).map{|u| UserPresenter.new(e)}
The problem is that users is no longer (after mapping to my presenter) an array wrapped with will_paginate magic, so it does not contains such things like total_entries, per_page or page, then the will_paginate helper is not working in my view :S .
How should i proceed to paginate a list of post-processed objects?
I've tried doing the other way around but then if i have a huge amount of records in my table it will be really painful because it will paginate from a big result set that is already retrieved:
users = User.order('name').map{|u| UserPresenter.new(u)}.paginate(:per_page => 10, :page => 1)
Thanks in advance

Maybe I would try to create a UserCollectionPresenter and a UserPresenter this way
class UserCollectionPresenter
include Enumerable
delegate :paginate, to: :collection
attr_reader :collection
def initialize(collection)
#collection = collection
#_presentable_collection = collection.map { |user| UserPresenter.new(user) }
end
def each(&block)
#_presentable_collection.each(&block)
end
def total_count
#collection.limit(nil).offset(nil).count
end
end
And
class UserPresenter
def initialize(user)
#user = user
end
private
attr_reader :user
end
In the controller u should send the paginated AR collection and later u can use paginate. Not sure which methods u need from will_paginate but i would think of either delegating, "overriding" them or include them from will_paginate directly.

I would suggest that you still use users for will_paginate and add another instance variables for presenters.
#users = User.order('name').paginate(:per_page => 10, :page => 1)
#presenters = #users.map { |u| UserPresenter.new(u) }

Related

Ignoring blank fields when submitting rails form

I am using several dropdowns to create a search and they are submitting '' if the user doesn't select one. I need a catchall, something like * in SQL as the default value. ie if I have 5 brands in a dropdown, I want the default query to be all 5 brands. Something like Brand.where(brand: ALL). Thanks in advance.
<%= select_tag(:brand, options_for_select(["Brand 1","Brand 2","Brand 3","Brand 4","Other"].map{ |num| [num,num] }),id: 'brand', prompt: 'Brand', class: "table") %>
How about something like:
product.rb
class Product < ActiveRecord::Base
scope :by_brand, -> (brand) { where brand: brand }
# put more scopes for the other drop-down boxes
end
product_controller.rb
class ProductsController < ApplicationController
def search
#products = Product.all
#products = #products.by_brand(params[:brand]) unless params[:brand].blank?
# more filtering here
end
end
You probably want include_blank and a prompt in your selects.
select_tag(..., include_blank: true, prompt: 'All')
Now the first entry in the dropdown will have a blank value, and display "All" for its label.
You'll then need to just make sure you don't use that criteria when you query, something like this (you didn't post any code, so I don't know your model):
class MyController ...
def show
#items = Item.all
#items = #items.where(brand: params[:brand]) if params[:brand].present?
#items = #items.where(size: params[:size]) if params[:size].present?
#items = #items.where(year: params[:year]) if params[:year].present?
#items = #items.where(color: params[:color]) if params[:color].present?
end
end

Ruby on Rails search with multiple parameters

For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.

Ransack: How to use existing scope?

Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:
class Post < ActiveRecord::Base
scope :year, lambda { |year|
where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'")
}
end
So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:
ransacker :year,
:formatter => proc {|v|
year(v)
}
But this does not work:
Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation
I tried some variations of the ransacker declaration, but none of them work. I Need some help...
UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.
ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.
From manual
Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:
class Employee < ActiveRecord::Base
scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
def self.hired_since(date)
where('start_date >= ?', date)
end
private
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
%i(activated hired_since salary_gt)
else
# allow other users to search on `activated` and `hired_since` only
%i(activated hired_since)
end
end
end
Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates
Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34
I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.
You can read full explanation here. Meanwhile here's the gist of it
The View
= form_for #product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for #product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
```
ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:
The Controller
# products_controller.rb
def index
#product_search = ProductSearch.new(params[:product_search])
#products ||= #product_formobject.result.page(params[:page])
end
The Form Object
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are Product.scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
#params = params || {}
super
#q = Product.search( #params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end

Filtering results in Rails

I am showing a list of questions when the user use the index action. I want to filter this list, showing only rejected questions, questions that only have images attached to them etc.
How do you do that? Do you just add code in the index action that checks if the different named parameters is in the request parameter hash and use them build a query.
myurl.com/questions?status=approved&only_images=yes
Or are there better ways?
You can use has_scope to do this elegantly:
# Model
scope :status, proc {|status| where :status => status}
scope :only_images, ... # query to only include questions with images
# Controller
has_scope :status
has_scope :only_images, :type => boolean
def index
#questions = apply_scopes(Question).all
end
To keep your controller thin and avoid spaghetti code you can try to use following way:
Controller:
def index
#questions = Question.filter(params.slice(:status, :only_images, ...) # you still can chain with .order, .paginate, etc
end
Model:
def self.filter(options)
options.delete_if { |k, v| v.blank? }
return self.scoped if options.nil?
options.inject(self) do |scope, (key, value)|
return scope if value.blank?
case key
when "status" # direct map
scope.scoped(:conditions => {key => value})
when "only_images"
scope.scoped(:conditions => {key => value=="yes" ? true : false})
#just some examples
when "some_field_max"
scope.scoped(:conditions => ["some_field <= ?", value])
when "some_field_min"
scope.scoped(:conditions => ["some_field >= ?", value])
else # unknown key (do nothing. You might also raise an error)
scope
end
end
end
So, I think there are places where you need to code to be good in such a scenario; the model and the controller.
For the model you should use scopes.
#Model
scope :rejected, lambda { where("accepted = false") }
scope :accepted lambda { where("accepted = true") }
scope :with_image # your query here
In the controller,
def index
#questions = #questions.send(params[:search])
end
You can send the method name from the UI and directly pass that to the scope in the model. Also, you can avoid an "if" condition for the .all case by passing it from the UI again.
But as this directly exposes Model code to view, you should filter any unwanted filter params that come from the view in a private method in the controller using a before_filter.

Ruby on Rails will_paginate an array

I was wondering if someone could explain how to use will_paginate on an array of objects?
For example, on my site I have an opinion section where users can rate the opinions. Here's a method I wrote to gather the users who have rated the opinion:
def agree_list
list = OpinionRating.find_all_by_opinion_id(params[:id])
#agree_list = []
list.each do |r|
user = Profile.find(r.profile_id)
#agree_list << user
end
end
Thank you
will_paginate 3.0 is designed to take advantage of the new ActiveRecord::Relation in Rails 3, so it defines paginate only on relations by default. It can still work with an array, but you have to tell rails to require that part.
In a file in your config/initializers (I used will_paginate_array_fix.rb), add this
require 'will_paginate/array'
Then you can use on arrays
my_array.paginate(:page => x, :per_page => y)
You could use Array#from to simulate pagination, but the real problem here is that you shouldn't be using Array at all.
This is what ActiveRecord Associations are made for. You should read that guide carefully, there is a lot of useful stuff you will need to know if you're developing Rails applications.
Let me show you a better way of doing the same thing:
class Profile < ActiveRecord::Base
has_many :opinion_ratings
has_many :opinions, :through => :opinion_ratings
end
class Opinion < ActiveRecord::Base
has_many :opinion_ratings
end
class OpinionRating < ActiveRecord::Base
belongs_to :opinion
belongs_to :profile
end
It's important that your database schema is following the proper naming conventions or all this will break. Make sure you're creating your tables with Database Migrations instead of doing it by hand.
These associations will create helpers on your models to make searching much easier. Instead of iterating a list of OpinionRatings and collecting the users manually, you can make Rails do this for you with the use of named_scope or scope depending on whether you're using Rails 2.3 or 3.0. Since you didn't specify, I'll give both examples. Add this to your OpinionRating class:
2.3
named_scope :for, lambda {|id|
{
:joins => :opinion,
:conditions => {
:opinion => { :id => id }
}
}
}
named_scope :agreed, :conditions => { :agree => true }
named_scope :with_profiles, :includes => :profile
3.0
scope :agreed, where(:agree => true)
def self.for(id)
joins(:opinion).where(:opinion => { :id => id })
end
In either case you can call for(id) on the OpinionRatings model and pass it an id:
2.3
#ratings = OpinionRating.agreed.for(params[:id]).with_profiles
#profiles = #ratings.collect(&:profile)
3.0
#ratings = OpinionRating.agreed.for(params[:id]).includes(:profile)
#profiles = #ratings.collect(&:profile)
The upshot of all this is that you can now easily paginate:
#ratings = #ratings.paginate(:page => params[:page])
Update for Rails 4.x: more or less the same:
scope :agreed, ->{ where agreed: true }
def self.for(id)
joins(:opinion).where(opinion: { id: id })
end
Although for newer Rails my preference is kaminari for pagination:
#ratings = #ratings.page(params[:page])
The gem will_paginate will paginate both ActiveRecord queries and arrays.
list = OpinionRating.where(:opinion_id => params[:id]).includes(:profile).paginate(:page => params[:page])
#agree_list = list.map(&:profile)
If you don't want to use the config file or are having trouble with it, you can also just ensure you return an ActiveRecord::Relation instead of an array. For instance, change the agree_list to be a list of user ids instead, then do an IN on those ids to return a Relation.
def agree_list
list = OpinionRating.find_all_by_opinion_id(params[:id])
#agree_id_list = []
list.each do |r|
user = Profile.find(r.profile_id)
#agree_id_list << user.id
end
#agree_list = User.where(:id => #agree_id_list)
end
This is inefficient from a database perspective, but it's an option for anybody having issues with the will_paginate config file.
I took advantage of rails associations, and came up with a new method:
def agree_list
o = Opinion.find(params[:id])
#agree_list = o.opinion_ratings(:conditions => {:agree => true}, :order => 'created_at DESC').paginate :page => params[:page]
rescue ActiveRecord::RecordNotFound
redirect_to(profile_opinion_path(session[:user]))
end
In my view I looked up the profile like so:
<% #agree_list.each do |rating| %>
<% user = Profile.find(rating.profile_id) %>
<% end %>
Please post up if there's a better way to do this. I tried to use the named_scope helper in the OpinionRating model with no luck. Here's an example of what I tried, but doesn't work:
named_scope :with_profile, lambda {|id| { :joins => [:profile], :conditions => ['profile_id = ?', id] } }
That seemed like the same as using the find method though.
Thanks for all the help.
I am using rails 3 ruby 1.9.2. Also, I am just starting app, so no css or styles included.
Install will_paginate:
gem install will_paginate
Add to Gemfile and run bundle.
Controller
class DashboardController < ApplicationController
include StructHelper
def show
#myData =structHelperGet.paginate(:page => params[:page])
end
end
module StructHelper queries a service, not a database.
structHelperGet() returns an array of records.
Not sure if a more sophisticated solution would be to fake a model, or to grab the data every so often and recreate a sqllite table once in a while and have a real model to query. Just creating my first rails app ever.
View
<div id="Data">
<%= will_paginate #myData%>
<table>
<thead>
<tr>
<th>col 1</th>
<th>Col 2</th>
<th>Col 3</th>
<th>Col 4</th>
</tr>
</thead>
</tbody>
<% #myData.each do |app| %>
<tr>
<td><%=app[:col1]%> </td>
<td><%=app[:col2]%> </td>
<td><%=app[:col3]%> </td>
<td><%=app[:col4]%> </td>
</tr>
<% end %>
</tbody>
</table>
<%= will_paginate #myData%>
</div>
This will give you pagnation of the default 30 rows per page.
If you have not read http://railstutorial.org yet, start reading it now.
You can implement pagination even without any gem.I saw this How do I paginate an Array?. Simple implementation in kaminari gems doc. Please see the below example which i got from kaminari gems doc
arr = (1..100).to_a
page, per_page = 1, 10
arr[((page - 1) * per_page)...(page * per_page)] #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
page, per_page = 2, 10
arr[((page - 1) * per_page)...(page * per_page)] #=> [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

Resources