filter data via boolean wildcard - ruby-on-rails

This is somewhat of an updated question from a problem I had earlier. I am creating a 'match.com' style app for local dog adoptions, where users and dogs have a profile. In each profile, there are Boolean fields. I am trying to show the end user only the dogs that match their user profile. A simple one-to-one match does not work however since the logic changes depending on the user's Boolean. For example, if a user has kids (user.kids_under_10: true) then I simply match it to a dog that can be placed with kids (dog.kids_under_10:true). The model would look like
class Dog < ActiveRecord::Base
def self.by_profile(user)
if user.kids_under_10 == true
Dog.where(kids_under_10: user.kids_under_10)
end
end
If the user does not have kids however, dogs that answer both true and false to this question can be displayed, since dogs that are AND are not kid friendly can be placed with this user. I realize that if this were the only criteria for matching, I could simply add a else, Dog.all statement to the above method. Since there will other Boolean fields to match however (i.e. user.has_cats and dog.cat_friendly), this solution would not work. I think I need a variable containing an array here, but not really sure... any suggestions.

Try to use something like this:
class Dog < ActiveRecord::Base
def self.by_profile(user)
dogs = scoped
dogs = dogs.where(kids_under_10: true) if user.kids_under_10
dogs = dogs.where(cat_friendly: true) if user.own_cats
# other conditions
dogs
end
end
Basically the scoped method gives you an empty relation. You can then use it to conditionally add where statements (or anything else AR supports).

So I found a solution, however I 'm not sure it's the most efficient solution, so any suggestions are very welcome (I am new to programming and looking to learn the 'best' way to handle different situations). Here is how I solved the issue:
class Dog < ActiveRecord::Base
def self.by_profile(user)
if user.kids_under_10?
kids_under_10 = true
else
kids_under_10 = [true, false]
end
Dog.where(kids_under_10: kids_under_10)
end
end
I can foresee this solution becoming a bit cumbersome as I add more parameters. The conditional statements could get quite long. Thanks for any suggestions!

def self.by_profile(user)
user.kids_under_10 ? Dog.where(kids_under_10: true) : Dog.all
end
not very elegant, but should work.

Related

Using method in a where clause Rails Ruby Active Record

I wondering if it is posible to use a model instance method as a where clause query. I mean. I have a model School with a method defined
class School < ActiveRecord::Base
def my_method
users.where(blablabla).present?
end
end
Is it posible to get something like:
School.all.where(my_method: true)
I know that I can do something like:
School.all.map{|x| x if x.my_method}
But this way has a huge penalization in performance compared to where query. Furthermore, the return of what I'm searching is an ActiveRecord Relation and map returns an array.
UPDATE:
Also there is another way to do it like:
School.all.joins(:users).where("users.attribute = something")
But this do not fit exactly what I want for several reasons.
Thanks in advance
I don't really know the relations between your models.
but where clause gets a hash of key - value.
So you can for example return the ID's of the users in a some kind of a hash and then use it.
def my_method
{user_id: users.where(blablabla).ids}
end
and use it:
School.all.where(my_method)

Ideal way to get association of ActiveRecord::Relation

Consider the following:
class User < ActiveRecord::Base
has_many :posts
end
Now I want to get posts for some banned users.
User.where(is_banned: true).posts
This produces a NoMethodError as posts is not defined on ActiveRecord::Relation.
What is the slickest way of making the code above work?
I can think of
User.where(is_banned: true).map(&:posts).flatten.uniq
But this is inefficient.
I can also think of
user_scope = User.where(is_banned: true)
Post.where(user: user_scope)
This requires the user association to be set up in the Post model and it appears to generate a nested select. I don't know about the efficiency.
Ideally, I would like a technique that allows traversing multiple relations, so I can write something like:
User.where(is_banned: true).posts.comments.votes.voters
which should give me every voter (user) who has voted for a comment on a post written by a banned user.
Why not just use joins?
Post.joins(:user).where(users: {is_banned: true})
This will generate SQL to the effect of
SELECT *
FROM posts
INNER JOIN users ON posts.user_id = users.id
WHERE users.is_banned = true
This seems to be exactly what you are looking for. As far as your long chain goes you can do the same thing just with a much deeper join.
In your code:
User.where(is_banned: true)
will be and ActiveRecord::Relation and you need one record. So doing if from the User model would be more complicated. Depending on how the relationship is set up you could add a scope in your Post model.
scope :banned_users, -> { joins(:users).where('is_banned = ?', true) }
Then you would just call Post.banned_users to get all the post created by banned users.
Here's a start of a solution for your ideal technique. It probably doesn't work as written with extended chaining, and performance would probably be pretty bad. It would also require that you define the inverse_of for each association —
module LocalRelationExtensions
def method_missing(meth, *args, &blk)
if (assoc = self.klass.reflect_on_association(meth)) && (inverse = assoc.inverse_of)
assoc.klass.joins(inverse.name).merge(self)
else
super
end
end
end
ActiveRecord::Relation.include(LocalRelationExtensions)
But really you should use the comment of #engineersmnky.

How to refactor that complex singelton model method for create nested models in Rails?

I have following complex method which I cut off from controller:
def self.create_with_company_and_employer(job_params)
company_attributes = job_params.delete(:company_attributes)
employer_attributes = job_params.delete(:employer_attributes)
new(job_params) do |job|
job.employer = Employer.find_or_create_by_email(employer_attributes)
company_attributes[:admin_id] = job.employer.id if Company.find_by_nip(company_attributes[:nip]).nil?
job.company = Company.find_or_create_by_nip(company_attributes)
Employment.create(employer_id: job.employer.id, company_id: job.company.id)
end
end
I using here two nested_attributes functionality for create company and employer.
Whole code you can find here: https://gist.github.com/2c3b52c35df763b6d9b4
company_attributes[:admin_id] = job.employer.id if Company.find_by_nip(company_attributes[:nip]).nil?
Employment.create(employer_id: job.employer.id, company_id: job.company.id)
Basically I would like to refactor that two lines:
I looked at your gist and i think this is a design issue.
your Employment and Job models seem somewhat redundant, but i don't know what are their actual purpose exactly so i can't really help for now on this matter (i have a hunch that your schema could be remodeled with the employements belonging to the jobs). However, if you really want to, you can use an after_create callback to manage the replication :
class Job < ActiveRecord::Base
after_create :create_corresponding_employment
def create_corresponding_employment
Employment.create( employer_id: employer.id, company_id: company.id )
end
end
this gets you rid of the last line of your method.
the other line you want to get rid of is tricky : you assign an admin_id to your company, but why would you want to do that ? In fact, you're just creating a 'hidden' relation between Company and Employer (a belongs_to one). Why do you need that ? Give more information and i can help.
one more thing: it is not advised to delete keys form the params, or even modify the hash directly. Use a copy.

Rails 3 nested attributes: How can I assign a matching record to the parent model instead of creating a new record every time?

Here's the basic setup:
I have an Order model. An Order has one Address and it accepts_nested_attributes_for :address.
I have a basic order form where I ask a user to input her address. This is handled with nested_fields_for. Everything works great - new addresses are validated and assigned nicely.
However, the problem is that it creates a new Address every time, even if an Address already exists with identical attributes.
I would like to modify the behavior so that if the user-inputted address matches all the attributes for an existing Address, the order assigns the existing Address to itself rather than creating a new one.
The methods I have tried are:
In the controller, try to find an existing Address record with the nested attributes (params[:order][:address_attributes]). If a match exists, delete all the nested attributes and replace them with params[:order][:address_id].
Don't use nested_attributes_for at all and instead override the address= method in the model, then just use the controller to create a new Address based on the parameters and then hand it off to the model.
Both of these solutions seem various degrees of messy. Could somebody please enlighten me on whether this is a controller or model responsibility, and perhaps suggest an elegant way to accomplish this?
Thanks in advance.
Have you tried something like this?
class Order < ActiveRecord::Base
# [..]
before_save :replace_existing_address!
def replace_existing_address!
db_address = Address.where(:city => self.address.city,
:street => self.address.street,
:number => self.address.number).first
self.address = db_address if db_address
end
end
Since I'm asking this as a survey of good ways to do this, I figured I'd offer the solution I'm currently using as well as a basis for comment.
In the Controller:
#new_address = Address.new( params[:order][:address] )
#order.address = new_address
#order.update_attributes( params[:order] )
In the Model:
def address=( address )
return unless address and address.is_a? Address
duplicate_address = Address.where( address_1: address.address_1,
address_2: address.address_2,
[etc. etc.] ).first
if duplicate_address
self.address_id = duplicate_address.id
else
address.save
self.address_id = address.id
end
end
I it's truly a :has_one relationship as you say and not a :has_many, you don't need to explicitly assign the address like you do in your own answer. That's what accepts_nested_attributes is for, after all. This line by itself should work:
#order.update_attributes( params[:order] )
That should create a new address if none exists, and update an existing one.
Your solution may work, but it a) doesn't take advantage of accepts_nested_attributes and b) will leave lots of orphaned addresses in your database.

rails: mass-assignment security concern with belongs_to relationships

I've been reading up on rails security concerns and the one that makes me the most concerned is mass assignment. My application is making use of attr_accessible, however I'm not sure if I quite know what the best way to handle the exposed relationships is. Let's assume that we have a basic content creation/ownership website. A user can have create blog posts, and have one category associated with that blog post.
So I have three models:
user
post: belongs to a user and a category
category: belongs to user
I allow mass-assignment on the category_id, so the user could nil it out, change it to one of their categories, or through mass-assignment, I suppose they could change it to someone else's category. That is where I'm kind of unsure about what the best way to proceed would be.
The resources I have investigated (particularly railscast #178 and a resource that was provided from that railscast), both mention that the association should not be mass-assignable, which makes sense. I'm just not sure how else to allow the user to change what the category of the post would be in a railsy way.
Any ideas on how best to solve this? Am I looking at it the wrong way?
UPDATE: Hopefully clarifying my concern a bit more.
Let's say I'm in Post, do I need something like the following:
def create
#post = Post.new(params[:category])
#post.user_id = current_user.id
# CHECK HERE IF REQUESTED CATEGORY_ID IS OWNED BY USER
# continue on as normal here
end
That seems like a lot of work? I would need to check that on every controller in both the update and create action. Keep in mind that there is more than just one belongs_to relationship.
Your user can change it through an edit form of some kind, i presume.
Based on that, Mass Assignment is really for nefarious types who seek to mess with your app through things like curl. I call them curl kiddies.
All that to say, if you use attr_protected - (here you put the fields you Do Not want them to change) or the kid's favourite attr_accessible(the fields that are OK to change).
You'll hear arguments for both, but if you use attr_protected :user_id in your model, and then in your CategoryController#create action you can do something like
def create
#category = Category.new(params[:category])
#category.user_id = current_user.id
respond_to do |format|
....#continue on as normal here
end
OK, so searched around a bit, and finally came up with something workable for me. I like keeping logic out of the controllers where possible, so this solution is a model-based solution:
# Post.rb
validates_each :asset_category_id do |record, attr, value|
self.validates_associated_permission(record, attr, value)
end
# This can obviously be put in a base class/utility class of some sort.
def self.validates_associated_permission(record, attr, value)
return if value.blank?
class_string = attr.to_s.gsub(/_id$/, '')
klass = class_string.camelize.constantize
# Check here that the associated record is the users
# I'm leaving this part as pseudo code as everyone's auth code is
# unique.
if klass.find_by_id(value).can_write(current_user)
record.errors.add attr, 'cannot be found.'
end
end
I also found that rails 3.0 will have a better way to specify this instead of the 3 lines required for the ultra generic validates_each.
http://ryandaigle.com/articles/2009/8/11/what-s-new-in-edge-rails-independent-model-validators

Resources