Elegant way so access a `belongs_to` -> `has_many` relationship - ruby-on-rails

In my app, an user belongs_to a customer, and a customer has_many construction_sites. So when I want to show the current_user only his construction_sites, I have multiple possibilities of which none is elegant:
#construction_sites = ConstructionSite.where(customer: current_user.customer)
This works and looks good, except for the case that the user is not yet associated with a customer. Then, I get a PG::UndefinedColumn: ERROR: column construction_sites.customer does not exist error.
#construction_sites = ConstructionSite.where(customer_id: current_user.customer_id)
This seems to work fine on first sight, but again for the case that the user is not yet associated with a customer current_user.customer_id is nil and ConstructionSite.where(customer_id: nil) gets called which selects all (or all unassigned?) sites, which is not what I want.
unless...
unless current_user.customer.nil?
#construction_sites = ConstructionSite.where(customer: current_user.customer)
else
#construction_sites = []
end
Well this works, but does not look nice.
ConstructionSite.joins(customer: :users).where('users.id' => current_user.id)
works but does not look nice.
So, what is the most elegant solution to this problem?

Try using delegate keyword. Add this to your user model.
delegate :construction_sites, to: :customer, allow_nil: true
After that you can use statements like
current_user.construction_sites
Which I find the most elegant of all options.

def user_construction_sites
#construction_sites = []
#construction_sites = current_user.customer.construction_sites if current_user.customer.present?
#construction_sites
end

How about moving your logic to a named scope and putting in a guard clause?
class SomeController < ApplicationController
def some_action
#construction_sites = ConstructionSite.for(current_user)
end
end
class ConstructionSite < ActiveRecord::Base
def self.for(user)
return [] if user.customer.blank?
where(customer: user.customer)
end
end

Related

Rails: Duplicate records created at the same time

There is something weird happening with my Rails app. A controller action is called and saves a record to a table each time a user visits a unique url I created before.
Unfortunately, sometimes two identical records are created instead of only one. I added a "validates_uniqueness_of" but it's not working.
My controller code:
class ShorturlController < ApplicationController
def show
#shorturl = ShortUrl.find_by_token(params[:id])
#card = Card.find(#shorturl.card_id)
#subscriber = BotUser.find_by_sender_id(params['u'])
#letter_campaign = Letter.find(#card.letter_id).campaign_name.downcase
if AnalyticClic.where(card_id: #card.id, short_url_id: #shorturl.id, bot_user_id: #subscriber.id).length != 0
#object = AnalyticClic.where(card_id: #card.id, short_url_id: #shorturl.id, bot_user_id: #subscriber.id)
#ccount = #object[0].clicks_count
#object.update(updated_at: Time.now, clicks_count: #ccount += 1)
else
AnalyticClic.create(card_id: #card.id, short_url_id: #shorturl.id, bot_user_id: #subscriber.id, clicks_count: "1".to_i)
end
#final_url = #card.cta_button_url
redirect_to #final_url, :status => 301
end
end
And the model:
class AnalyticClic < ApplicationRecord
validates_uniqueness_of :bot_user_id, scope: :card_id
end
Any idea why sometimes I have duplicated records? The if should prevent that as well as the validates_uniqueness_of.
First off, I believe your validation may need to look something like (although, TBH, your syntax may be fine):
class AnalyticClic < ApplicationRecord
validates :bot_user_id, uniqueness: { scope: :card_id }
end
Then, I think you should clean up your controller a bit. Something like:
class ShorturlController < ApplicationController
def show
#shorturl = ShortUrl.find_by_token(params[:id])
#card = Card.find(#shorturl.card_id)
#subscriber = BotUser.find_by_sender_id(params['u'])
#letter_campaign = Letter.find(#card.letter_id).campaign_name.downcase
analytic_clic.increment!(:click_count, by = 1)
#final_url = #card.cta_button_url
redirect_to #final_url, :status => 301
end
private
def analytic_clic
#analytic_clic ||= AnalyticClic.find_or_create_by(
card_id: #card.id,
short_url_id: #shorturl.id,
bot_user_id: #subscriber.id
)
end
end
A few IMPORTANT things to note:
You'll want to create an index that enforces uniqueness at the database level (as max pleaner says). I believe that'll look something like:
class AddIndexToAnalyticClic < ActiveRecord::Migration
def change
add_index :analytic_clics [:bot_user_id, :card_id], unique: true, name: :index_bot_user_card_id
end
end
You'll want to create a migration that sets :click_count to a default value of 0 on create (otherwise, you'll have a nil problem, I suspect).
And, you'll want to think about concurrency with increment! (see the docs)
You need to create unique index in your database table. There might be 2 processes getting to create condition together. The only way to stop these duplicate records is by having uniqueness constraint at DB level.

Manipulating searched object(child) in (parent)model

In my model subject.rb i have the following defined
has_many :tutors, through: :profiles
def self.search(param)
where("name like ?", "%#{param}%")
end
So something like Subject.search("English") works perfectly fine in rails console.
What i would like to know is that if i do subject = Subject.first and i can do stuff like subject.id and it returns the subject ID to me.
Whereas when i do subject = Subject.search("English") i am unable to do something like subject.id
Because i'm trying to link the search function to my tutor.rb model with the following code.
def self.subject_search(s)
#tutor = Tutor.all
#tutor.each do |x|
y = x.subjects.search(s)
unless y.empty?
return x
end
end
end
Which works but only returns one Tutor and not all Tutors that have the subject.
I also tried this instead
def self.subject_search(s)
#subject = Subject.search(s)
if #subject
#subject.tutors
end
end
But thats when i realised #subject.tutors doesn't work, as explained above, if i do subject = Subject.search("English") i can't manipulate subject with any methods.
What am i doing wrongly?
Using a #where returns an array of objects meeting the criteria.
Subject.search('Math') => ['Math1', 'Math2', 'Math3'] # Objects of course
In your case, you should be doing Subject.find_by_name('English') which returns the first object satisfying your query. Then you can call #tutors on your Subject model assuming you have the method defined.
If you do have to use the like operator no matter what (which I do not recommend), here's what will happen.
s = Subject.search('En') # => ['English', 'Environmental Science', ..]
s.tutors # => Undefined method tutors for Array class
Here, s is an array of Subject models rather than a singular Subject which is the reason why its not working. You either need something to narrow it down more or loop through it which is probably not what you want anyway.

rails callback before 'new' of a model?

I have a model base_table, and I have a extended_table which has extra properties to further extend my base_table. (I would have different extended_tables, to add different properties to my base_table, but that's non-related to the question I'm asking here).
The model definition for my base_table is like:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def self.included(base)
base.belongs_to :base_table, autosave:true, dependent: :destroy
# do something more when this module is included
end
end
end
And the model definition for my extended_table is like:
class TennisQuestionaire < ActiveRecord::Base
include BaseTable::BaseTableInclude
end
Now I what I want is the code below:
params = {base_table: {name:"Songyy",age:19},tennis_ball_num:3}
t = TennisQuestionaire.new(params)
When I created my t, I want the base_table to be instantiated as well.
One fix I can come up with, is to parse the params to create the base_table object, before TennisQuestionaire.new was called upon the params. It's something like having a "before_new" filter here. But I cannot find such kind of filter when I was reading the documentation.
Additionally, I think another way is to override the 'new' method. But this is not so clean.
NOTE: There's one method called accepts_nested_attributes_for, seems to do what I want, but it doesn't work upon a belongs_to relation.
Any suggestions would be appreciated. Thanks :)
After some trails&error, the solution is something like this:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def initialize(*args,&block)
handle_res = handle_param_args(args) { |params| params[:base_table] = BaseTable.new(params[:base_table]) }
super(*args,&block)
end
private
def handle_param_args(args)
return unless block_given?
if args.length > 0
params = args[0]
if (params.is_a? Hash) and params[:base_table].is_a? Hash
yield params
end
end
end
end
end

rails, how to pass self in function

message and user. my message belongs_to user and user has_many messages.
in one of my views, i call something like
current_user.home_messages?
and in my user model, i have...
def home_messages?
Message.any_messages_for
end
and lastly in my message model, i have
scope :any_messages_for
def self.any_messages_for
Message.where("to_id = ?", self.id).exists?
end
ive been trying to get the current_users id in my message model. i could pass in current_user as a parameter from my view on top but since im doing
current_user.home_messages?
i thought it would be better if i used self. but how do i go about referring to it correctly?
thank you.
You could use a lambda. In your Message model:
scope :any_messages_for, lambda {|user| where('user_id = ?', user.id)}
This would work like so:
Message.any_messages_for(current_user)
And you could add a method to your user model to return true if any messages are found. In this case you use an instance method and pass in the instance as self:
def home_messages?
return true if Message.any_messages_for(self)
end
But really, I'd just do something like this in the User model without having to write any of the above. This uses a Rails method that is created when declaring :has_many and :belongs_to associations:
def home_messages?
return true if self.messages.any?
end
You can do either of the following
def self.any_messages_for(id) #This is a class method
Message.where("to_id = ?", id).exists?
end
to call above method you have to do
User.any_messages_for(current_user.id) #I am assuming any_messages_for is in `User` Model
OR
def any_messages_for #This is a instance method
Message.where("to_id = ?", self.id).exists?
end
to call above method you have to do
current_user.any_messages_for
This stuff in your Message class doesn't make a lot of sense:
scope :any_messages_for
def self.any_messages_for
Message.where("to_id = ?", self.id).exists?
end
The scope macro defines a class method on its own and there should be another argument to it as well; also, scopes are meant to define, more or less, a canned set of query parameters so your any_messages_for method isn't very scopeish; I think you should get rid of scope :any_messages_for.
In your any_messages_for class method, self will be the class itself so self.id won't be a user ID and so it won't be useful as a placeholder value in your where.
You should have something more like this in Message:
def self.any_messages_for(user)
where('to_id = ?', user.id).exists?
# or exists?(:to_id => user.id)
end
And then in User:
def home_messages?
Message.any_messages_for(self)
end
Once all that's sorted out, you can say current_user.home_messages?.

Overriding ActiveRecord collection (association) methods

I'm new to Ruby and I'm trying to create a model where collections are assembled if they do not exist.
I already overloaded individual attributes like so:
class My_class < ActiveRecord::Base
def an_attribute
tmp = super
if tmp
tmp
else
#calculate this and some similar, associated attributes, for example:
self.an_attribute = "default" #{This does work as it is calling an_attribute=() }
self.an_attribute
end
end
end
test = My_class.new
p test.an_attribute # => "default"
This works great and basically gives me ||= functionality.
So, without thinking, I went and wrote myself a pretty big function to do the same with a collection (i.e. if there are no objects in the collection, then go and work out what they should be)
class My_class < ActiveRecord::Base
def things
thngs = super
if thngs.empty? #we've got to assemble the this collection!
self.other_things.each do |other_thing| #loop through the other things
#analysis of the other_things and creation and addition of things
self.things << a_thing_i_want
end
else #just return them
thngs
end
end
end
test = My_class.new
test.other_things << other_thing1
test.other_things << other_thing2
p test.things # => METHOD ERROR ON THE 'super' ARGH FAIL :(
This fails unfortunately. Any ideas for solutions? I don't think extensions are the right solution here
Attempted so far:
super --> 'method_missing': super: no superclass method
self[:things] --> test.things.each generates a nilClass error
Potential solution:
Would be rename the table column and has_many association from things to priv_things. This would allow me to simply create the things method and use self.priv_things instead of super

Resources