Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow ransacker to have its arguments #513

Merged
merged 2 commits into from
Mar 12, 2015

Conversation

sineed
Copy link

@sineed sineed commented Feb 18, 2015

This is related to #508 but I have another use case:

In my project I have Customer and Deal models. Customers have many Deals and each deal has amount attribute. I want to find customers which have made deals with total amount more than specified value for the last week. So my ransacker is:

# app/models/customer.rb
ransacker :sum_of_deals_last_week do |parent|
  deals_table = Deal.arel_table

  Arel::Nodes::Grouping.new(
    deals_table
      .project(Arel::Nodes::Sum.new([deals_table[:amount]]))
      .group(deals_table[:customer_id])
      .where(deals_table[:customer_id].eq(parent.table[:id])
        .and(deals_table[:created_at].gteq(1.week.ago.to_date)
          .and(deals_table[:created_at].lteq(Date.today))
        )
      ).ast
  )
end

And I can easily filter:

Customer.ransack(sum_of_deals_last_week_gt: 100)

The problem occurs when I want to change period of time. Say, for last month period I need to create new ransacker. What if I need to dynamically specify this period?

So with changes in this PR the following ransacker can be created:

# app/models/customer.rb
ransacker :sum_of_deals, args: [:parent, :rargs] do |parent, rargs|
  date_from = rargs[0]
  date_till = rargs[1]
  deals_table = Deal.arel_table

  Arel::Nodes::Grouping.new(
    deals_table
      .project(Arel::Nodes::Sum.new([deals_table[:amount]]))
      .group(deals_table[:customer_id])
      .where(deals_table[:customer_id].eq(parent.table[:id])
        .and(deals_table[:created_at].gteq(date_from)
          .and(deals_table[:created_at].lteq(date_till))
        )
      ).ast
  )
end

And I can use this ransacker:

Customer.ransack(
  c: [{
    a: { '0' => { name: :sum_of_deals, rargs: [1.week.ago.to_date, Date.today] } },
    p: :gt,
    v: [100]
  }]
)

jonatack added a commit that referenced this pull request Mar 12, 2015
Enable passing arguments to Ransackers
@jonatack jonatack merged commit f3c4198 into activerecord-hackery:master Mar 12, 2015
@jonatack
Copy link
Contributor

Thank you, @sineed. Would you like to update the change log and the wiki about this?

I'll admit I'm hesitant about the name rargs. How about a more self-explanatory name like ransacker_args?

jonatack added a commit that referenced this pull request Mar 12, 2015
rename the test and ransacker to hopefully make the intent and API more
explicit, wrap line at 80 characters.

Follow-up to #513.

`ransacker_args` is a bit longer but the tradeoff in being more
self-explanatory and explicit and less cryptic seems worthwhile.
@sineed
Copy link
Author

sineed commented Mar 12, 2015

@jonatack I've updated wiki page Using Ransackers and added #519

@jonatack
Copy link
Contributor

@sineed thanks. The test and wiki example query generates the following SQL query, which does not seem to be valid SQL.

SELECT "people".* FROM "people" WHERE
  ((SELECT title FROM articles
    WHERE articles.person_id = people.id
    AND CHAR_LENGTH(articles.body) BETWEEN 10 AND 100
  )
  LIKE '%Rails has been released%') 
ORDER BY "people"."id" DESC

"PG::CardinalityViolation: ERROR: more than one row returned by a subquery used as an expression"

Could you create an example that generates valid SQL?

@melhotiby
Copy link

@sineed @jonatack How would this be used in a sort_link I have a sort link like the following

= sort_link @q, :read_items, 'Only Items Ive read'
ransacker :read_items, args: [:parent, :ransacker_args] do |parent, args|
  # Need args to have the current_user id
end

which I want to pass over the current_user id, is that possible with this? The wiki only does not make it clear on how to pass an argument like this

@sineed
Copy link
Author

sineed commented Mar 15, 2015

@jonatack
I've updated #519 and wiki. Please take a look.

@railsdevmatt
I don't use sorting functionality of ransack. And I don't know if a ransacker can be used as an attribute for sort_link method.
Ransacker arguments were implemented primary for advanced searching. And maybe it's possible to perform sorting without need of ransacker, can you add more info about your use case?

@melhotiby
Copy link

@sineed - Ok so i have a forum with topics

# forums controller
def show
  @forum = Forum.find(params[:id])
  @q = @forum.topics.ransack(params[:q])
  @topics = @q.result.page(params[:page])
end

# view show.html.haml
- unless @topics.empty?

  %ul
    %li= sort_link @q, :read_topics, 'Only topics Ive read'

  - @topics.each do |topic|
    ...
    ...

I want to pass in the current_user to the read_topics ransacker that i created and filter only the read topics. I do know this is possible without ransack, but figured if there was a way to accomplish this with a ransacker then I would give it a go.

@sineed
Copy link
Author

sineed commented Mar 15, 2015

@railsdevmatt Am I right if your topics have a have_and_belongs_to_many association with current_user' class? If so you can use https://github.com/activerecord-hackery/ransack#associations logic, something like this (say your association is named readers):

  link_to 'Only topics Ive read', forum_path(id: @forum.id, q: { readers_user_id: current_user.id })

@melhotiby
Copy link

@sineed - actually the relationship is like this

# topic model
class Topic < ActiveRecord::Base
  has_many :topic_reads, dependent: :destroy
  has_many :subscribers, through: :topic_subscriptions
class TopicRead < ActiveRecord::Base
  belongs_to :topic
  belongs_to :reader, class_name: "User", foreign_key: "reader_id"
class TopicSubscription < ActiveRecord::Base
  belongs_to :topic
  belongs_to :subscriber, -> { where "users.email != '' AND users.email IS NOT NULL" }, class_name: "Contact", foreign_key: "subscriber_id"

@jonatack
Copy link
Contributor

The sort_link is for setting the order_by SQL via the Ransack :sorts (or :s) key.

Any filtering should be happening in your controller action (where you have ready access to current_user).

@reubenbrown
Copy link

@sineed @jonatack Love this feature! If I were constructing a ransack query in my controller like so (by "manually" merging together attribute names with predicates):

User.ransack({"s"=>"created_at desc", "created_at_eq"=>"2015-06-10"}).result

And I wanted to pass arguments to my ransacker for created_at

ransacker :created_at, args: [:parent, :ransacker_args] do |parent, args|
  start_time, end_time = args
  User.where('users.created_at > ? AND users.created_at < ?', start_time, end_time)
end

Is it possible to pass ransacker_args given the way I am constructing my query hash?

I've been playing around trying to properly include it in my hash but keep getting an undefined method error ransacker_args for the sort node. Since the documentation provides only one example of passing arguments to a ransacker it's a bit unclear if what I'm trying to do is even possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants