Chaining this query throws error - ruby-on-rails

I am trying to make a query that:
Finds/Gets the object (Coupon.code)
Checks if the coupon is expired (expires_at)
Checks if the coupon has been used up. (coupons_remaining)
I got some syntax using a newer version of ruby but it isnt working with my version 2.2.1 The syntax I have is
def self.get(code)
where(code: normalize_code(code)).
where("coupon_count > ? OR coupon_count IS NULL", 0).
where("expires_at > ? OR expires_at IS NULL", Time.now).
take(1)
end
This throws an error of wrong number of arguments (2 for 1) which is because my rails doesn't seem to recognize the 2 arguments ("coupon_count > ? OR coupon_count IS NULL", 0) so I have tried to change it but when I change them to something like this (which in my heart felt horribly wrong)
def self.get(code)
where(code: normalize_code(code)).
where(coupon_count: self.coupon_count > 0 || self.coupon_count.nil? ).
where(expires_at: self.expires_at > Time.now || self.expires_at.nil? ).
take(1)
end
I get undefined method `coupon_count' for Coupon:Class
I am short on ideas can someone help me get the syntax for this get method in my model? By the way if it matters I am using mongoid 5.1.0

I feel your pain. Combining OR and AND in MongoDB is a bit messy because you're not really working with a query language at all, you're just building a hash. Similar complications apply if you might apply multiple conditions to the same field. This is also why you can't include SQL-like snippets like you can with ActiveRecord.
For example, to express:
"coupon_count > ? OR coupon_count IS NULL", 0
you need to build a hash like:
:$or => [
{ :coupon_count.gt => 0 },
{ :coupon_count => nil }
]
but if you try to add another OR to that, you'll overwrite the existing :$or key and get confusion. Instead, you need to be aware that there will be multiple ORs and manually avoid the duplicate by saying :$and:
:$and => [
{
:$or => [
{ :coupon_count.gt => 0 },
{ :coupon_count => nil }
]
}, {
:$or => [
{ :expires_at.gt => Time.now },
{ :expires_at => nil }
]
}
]
Then adding the code condition is straight forward:
:code => normalize_code(code),
:$and => [ ... ]
That makes the whole thing a rather hideous monstrosity:
def self.get(code)
where(
:code => normalize_code(code),
:$and => [
{
:$or => [
{ :coupon_count.gt => 0 },
{ :coupon_count => nil }
]
}, {
:$or => [
{ :expires_at.gt => Time.now },
{ :expires_at => nil }
]
}
]
).first
end
You could also use find_by(that_big_mess) instead of where(that_big_mess).first. Also, if you expect the query to match multiple documents, then you probably want to add an order call to make sure you get the one you want. You could probably use the and and or query methods instead of a single hash but I doubt it will make things easy to read, understand, or maintain.
I try to avoid ORs with MongoDB because the queries lose their little minds fast and you're left with some gibbering eldritch horror that you don't want to think about too much. You're usually better off precomputing parts of your queries with generated fields (that you have to maintain and sanity check to make sure they are correct); for example, you could add another field that is true if coupon_count is positive or nil and then update that field in a before_validation hook when coupon_count changes.

You've defined a class method, so self in this circumstance references the Coupon class rather than a Coupon instance.
Try the following:
scope :not_expired, -> { where("expires_at > ? OR expires_at IS NULL", Time.now) }
scope :previously_used, -> { where("coupon_count > 0 OR coupon_count IS NULL") }
def self.get(code)
previously_used.not_expired.find_by!(code: normalize_code(code))
end

Related

How to group_by into a given hash_map

Currently I have two models
class Author
# gender
# name
end
class Book
# status -> ['published', 'in_progress']
has_one :author
end
I decided to use group_by to group the dataset
def group_by_gender_by_status
books.group_by { |book| [book.author.gender, book.status] }
end
What do I get instead is this
{["male", "published"] => [{BooksRecord}]
["female", "published"] => [{BooksRecord}]
["male", "in_progress"] => [{BooksRecord}]
["female", "in_progress"] => [{BooksRecord}]}
My goal is to get this result
{
female: {
published: 10,
in_progress: 7
},
male: {
published: 6,
in_progress: 9
}
}
so that I can access via data[:male][:published], easier to present the data
I think you can do something like this:
books.group_by { |book| book.author.gender }
.transform_values { |books| books.map(&:status).tally }
In particular, this is leveraging Enumerable#tally, which as added to ruby version 2.7.
You didn't specify which ruby version you're actually using though, so if you're stuck on an older one, you could replace the last line with:
.transform_values { |books| books.group_by(&:status).transform_values(&:count) }
Enumerable#group_by just creates keys for grouping so you cannot use this exclusively in order to produce your desired result. Additionally as books grows iterating in this fashion will be come less and less performant.
You will be better off putting the grouping and counting on the database so that return is closer to your desired end result, like so:
def group_by_gender_by_status
books.joins(:author)
.group(Author.arel_attribute(:gender),Book.arel_attribute(:status))
.count
end
This will have a similar resulting Hash as your current group_by implementation however the counting and grouping will be performed on the database side before returning:
{["male", "published"] => 6,
["female", "published"] => 10,
["male", "in_progress"] => 9,
["female", "in_progress"] => 7}
To transition this into your desired nesting we will need to post process this data.
def group_by_gender_by_status
books.joins(:author)
.group(Author.arel_attribute(:gender),Book.arel_attribute(:status))
.count
.each_with_object(Hash.new {|h,k| h[k] = {}}) do |((gender,status),counter),obj|
obj[gender.to_sym][status.to_sym] = counter
end
end
The end result will be equivalent to your desired result and by moving the grouping and the counting to the database level it should degrade at a much slower rate.
Note: I have no idea where books came from or where this method currently exists. The implementation could potentially be further reduced by this understanding.

How to get all documents and hide this which are expired if they are defined?

I want to hide past events if they are defined and get all other. How to show all documents even if :once_at is nil and if :once_at is defined then hide these ones which are expired?
My recent approach, shows only events with defined :once_at, (I tryed with :once_at => nil, but without results):
default_scope where(:once_at.gte => Date.today)
or (also not working)
default_scope excludes(:once_at.lte => Date.today)
When do you think Date.today is evaluated? If you say this:
default_scope where(:once_at.gte => Date.today)
Date.today will be evaluated when the class is being loaded. This is almost never what you want to happen, you usually want Date.today to be evaluated when the default scope is used and the usual way to make that happen is to use a proc or lambda for the scope:
default_scope -> { where(:once_at.gte => Date.today) }
The next problem is what to do about documents that don't have a :once_at or those with an explicit nil in :once_at. nil won't be greater than today so you'd best check your conditions separately with an :$or query:
default_scope -> do
where(
:$or => [
{ :once_at => nil },
{ :once_at.gte => Date.today }
]
)
end

How to remove hash keys which hash value is blank?

I am using Ruby on Rails 3.2.13 and I would like to remove hash keys which corresponding hash value is blank. That is, if I have the following hash
{ :a => 0, :b => 1, :c => true, :d => "", :e => " ", :f => nil }
then the resulting hash should be (note: 0 and true are not considered blank)
{ :a => 0, :b => 1, :c => true }
How can I make that?
If using Rails you can try
hash.delete_if { |key, value| value.blank? }
or in case of just Ruby
hash.delete_if { |key, value| value.to_s.strip == '' }
There are a number of ways to accomplish this common task
reject
This is the one I use most often for cleaning up hashes as its short, clean, and flexible enough to support any conditional and doesn't mutate the original object. Here is a good article on the benefits of immutability in ruby.
hash.reject {|_,v| v.blank?}
Note: The underscore in the above example is used to indicate that we want to unpack the tuple passed to the proc, but we aren't using the first value (key).
reject!
However, if you want to mutate the original object:
hash.reject! {|_,v| v.blank?}
select
Conversely, you use select which will only return the values that return true when evaluated
hash.select {|_,v| v.present? }
select!
...and the mutating version
hash.select {|_,v| v.present? }
compact
Lastly, when you only need to remove keys that have nil values...
hash.compact
compact!
You have picked up the pattern by now, but this is the version that modifies the original hash!
hash.compact!
With respect to techvineet's solution, note the following when value == [].
[].blank? => true
[].to_s.strip == '' => false
[].to_s.strip.empty? => false

Building a Mongo query using rails? How?

This is how I went about to query for one specific element.
results << read_db.collection("users").find(:created_at => {:$gt => initial_date}).to_a
Now, I am trying to query by more than one.
db.inventory.find({ $and: [ { price: 1.99 }, { qty: { $lt: 20 } }, { sale: true } ] } )
Now how do I build up my query? Essentially I will have have a bunch of if statements, if true, i want to extend my query. I heard there is a .extend command in another langue, is there something similar in ruby?
Essentially i want to do this:
if price
query = "{ price: 1.99 }"
end
if qty
query = query + "{ qty: { $lt: 20 } }"
end
and than just have
db.inventory.find({ $and: [query]})
This syntax is wrong, what is the best way to go about doing this?
You want to end up with something like this:
db.inventory.find({ :$and => some_array_of_mongodb_queries})
Note that I've switched to the hashrocket syntax, you can't use the JavaScript notation with symbols that aren't labels. The value for :$and should be an array of individual queries, not an array of strings; so you should build an array:
parts = [ ]
parts.push(:price => 1.99) if(price)
query.push(:qty => { :$lt => 20 }) if(qty)
#...
db.inventory.find(:$and => parts)
BTW, you might run into some floating point problems with :price => 1.99, you should probably use an integer for that and work in cents instead of dollars. Some sort of check that parts isn't empty might be a good idea too.

MongoDB/MongoMapper Modifiers on Embedded Documents

Need some help with how to use atomic modifiers on an embedded document.
To illustrate, let's assume I've got a collection that looks like this.
Posts Collection
{
"_id" : ObjectId("blah"),
"title" : "Some title",
"comments" : [
{
"_id" : ObjectId("bleh"),
"text" : "Some comment text",
"score" : 0,
"voters" : []
}
]
}
What I'm looking to do with MongoMapper/MongoDB is perform an atomic update on a specific comment within a post document.
Something like:
class Comment
include MongoMapper::EmbeddedDocument
# Other stuff...
# For the current comment that doesn't have the current user voting, increment the vote score and add that user to the voters array so they can't vote again
def upvote!(user_id)
collection.update({"comments._id" => post_id, "comments.voters" => {"$ne" => user_id}},
{"$inc" => {"comments.score" => 1}, "$push" => {"comments.voters" => user_id}})
end
end
That's basically what I have now and it isn't working at all (nothing gets updated). Ideally, I'd also want to reload the document / embedded document but it seems as though there may not be a way to do this using MongoMapper's embedded document. Any ideas as to what I'm doing wrong?
Got this working for anyone that's interested. Two things I was missing
Using $elemMatch to search objects within an array that need to satisfy two conditions (such as _id = "" AND voters DOES NOT contain the user_id)
Using the $ operator on the $inc and $push operations to ensure I'm modifying the specific object that's referenced by my query.
def upvote!(user_id)
# Use the Ruby Mongo driver to make a direct call to collection.update
collection.update(
{
'meanings' => {
'$elemMatch' => {
'_id' => self.id,
'voters' => {'$ne' => user_id}
}
}
},
{
'$inc' => { 'meanings.$.votes' => 1 },
'$push' => { 'meanings.$.voters' => user_id }
})
end

Resources