I'm trying to grab a variable in my user model that's in relation to authorization model. The following works in Rails console but not sure how to translate that into the user.rb model since it keeps giving me error that it has to be a symbol or string. Doing to_s isn't correct outcome.
class User < ActiveRecord::Base
has_many :authorizations
def social
user = User.current
token = user.authorizations.pluck(:token).to_s
end
end
my authorizations model also looks like this;
class Authorization < ActiveRecord::Base
belongs_to :user
end
I've tried doing
class User < ActiveRecord::Base
def social
user = User.current
token = user.authorizations.select(:token).to_s
end
end
Doing this below in console works but not when I'm doing user.authorizations
auth = Authorization.where(:user_id => '54').token
but everything comes incorrect because it either has brackets or \ slashes within it.
If it's returning the user.authorizations then do a
user.authorizations.collect(&:token).first.to_s
pluck is on the db level. It will only query the particular field.
You are close with:
token = user.authorizations.pluck(:token).to_s
To align with Like #AnkitG's answer it would need to be
token = user.authorizations.pluck(:token).first.to_s
If you are going to consistently have a single authorization you may want to change the relationship from
has_many :authorizations
to
has_one :authorization
This way you can:
user.authorization.token
Related
I have two models: Users and PaymentMethods, the association between this models is:
class User < ApplicationRecord
has_many :payment_methods, dependent: :destroy
end
class PaymentMethod < ApplicationRecord
belongs_to :user, optional: true
end
I want to loop in each user and see in an attribute of PaymentMethod, named 'period_end_date'. so I do this:
#users = User.all
#users.each do |u|
u.payment_methods.last.period_end_date
end
I'm getting this error => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
The error is shown because I have 2 test users, in the first user there is still no data in the attribute 'period_end_date' and association exist, but is empty, in the second user there is data in the attributes, if I say, u.payment_methods.last.period_end_date I get => Wed, 13 Jun 2018 (only in the second user)
I want to filter in my loop only the users who has data in PaymentMethod attributes for get rid of => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
How I do this?
thanks
I want to filter in my loop only the users who has data in PaymentMethod attributes for get rid of => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
The actual problem seems to be you have users without payment methods (see my comment on your question).
You have some options, depending on how you're going to use the results.
1) You can filter out users without payment methods when you query them from the database like this:
#users = User.joins :payment_methods
2) If #users must include users that without payment methods, you can skip them when looping like this:
#users.map do |user|
next unless user.payment_methods.any?
user.payment_methods.last.period_end_date
end
3) You can guard by checking for payment_methods before calling .last.
User.all.map do |user|
user.payment_methods.last.period_end_date if user.payment_methods.any?
end
4) You can add a period_end_date method to the user
class User < ApplicationRecord
def period_end_date
payment_methods.limit(1).pluck :period_end_date
end
end
5) push #4 into the association by extending it with a helper method
class User < ApplicationRecord
has_many :payment_methods, class_name: 'PaymentMethod' do
def last_period_end_date
last.period_end_date if any?
end
end
end
which you can call like this
User.all.map do |user|
user.payment_methods.last_period_end_date
end
If you're really only concerned about PaymentMethods without a period_end_date then try this:
6) You can still filter users when you query them from the database
#users = User.joins(:payment_methods).where.not(payment_methods: { period_end_date: nil })
7) This can be simplified a bit by pushing the where.not conditions into a scope of the PaymentMethod class:
class PaymentMethod < ApplicationRecord
scope :period_ends, -> { where.not period_end_date: nil }
end
and merging it
#users = User.joins(:payment_methods).merge PaymentMethod.period_ends
Notes
payment_methods.last doesn't specify an order, you should set one (either as part of this chain, when you specify the association, or with a default scope) otherwise the order is up to your database and may be indeterminate.
chain .includes(:payment_methods) to eager load the payment methods and avoid n+1 queries
it sounds like a nil period_end_date could be invalid data. Consider adding a validation / database constraint to prevent this from happening
Is it possible to, within the record found through an association, retain access to the related model instance which found it?
Example:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
end
p = Person.first
p.info_of_the_moment = "I don't want this in the db"
assignment = p.assignments.first
assignment.somehow_get_p.info_of_the_moment # or some such magic!
And/or is there a way to "hang on to" the parameters of a scope and have access to them from within the found model instance? Like:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
scope :fun_assignments, -> (info) { where(fun: true) }
end
class Assignment < ApplicationRecord
belongs_to :person
def get_original_info
# When I was found, info was passed into the scope. What was it?
end
end
You can add your own extension methods to an association and those methods can get at the association's owner through proxy_association:
has_many :things do
def m
# Look at proxy_association.owner in here
end
end
So you could say things like:
class Person < ApplicationRecord
has_many :assignments do
def with_info
info = proxy_association.owner.info_of_the_moment
# Then we wave our hands and some magic happens to encode
# `info` into a properly escaped SQL literal that we can
# toss in a `select` call. If you're working with PostgreSQL
# then JSON would be a reasonable first choice if the info
# was, say, a hash.
#
# The `::jsonb` in the `select` call is there to tell everyone
# that the `info_of_the_moment` column is JSON and should be
# decoded as such by ActiveRecord.
encoded_info = ApplicationRecord.connection.quote(info.to_json)
select("assignments.*, #{encoded_info}::jsonb as info_of_the_moment")
end
end
#...
end
p = Person.first
p.info_of_the_moment = { 'some hash' => 'that does', 'not go in' => 'the database' }
assignment = p.assignments.with_info.first
assignment.info_of_the_moment # And out comes the hash but with stringified keys regardless of the original format.
# These will also include the `info_of_the_moment`
p.assignments.where(...).with_info
p.assignments.with_info.where(...)
Things of note:
All the columns in select show up as methods even when they're not part of the table in question.
You can add "extension" methods to an association by including a block with those methods when calling the association's method.
An SQL SELECT can include values that aren't columns, literals work just fine.
What format you use to tunnel your extra information through the association depends on the underlying database.
If the encoded extra information is large then this can get expensive.
This is admittedly a bit kludgey and brittle so I'd agree with you that rethinking your whole approach is a better idea.
i have a special case for which i need to know the best practice.
Given a simple has_many association:
class Authentication < ActiveRecord::Base
belongs_to :user
#provider can be :password, :facebook_oauth etc
#code is the encrypted password on provider == :password
end
class User < ActiveRecord::Base
has_many :authentications
#this works
def encrypted_password=(pw)
set = false
self.authentications.each do |auth|
if auth.provider.to_sym == :password
set = true
auth.code = pw
end
end
self.authentications.build(provider: :password, code: pw) unless set
pw
end
#this only when no password-auth exist yet
def encrypted_password=(pw)
self.authentications.find_or_initialize_by(provider: :password).code = pw
end
end
and then
user = User.last
user.password="abcdefg"
user.save
While the first solution works, it loads and iterates over ALL associated Authentication objects. It was a workaround but this is a no-go.
The second solution does not work when it loads an existing Password-Authentication object. The User object does not know about the change on the Authentication object loaded with the find_or_initialize_by method. The change won't be saved...
Is there a way to register the changed Authentication object back to the User object so that it will be autosaved when called user.save?
It seems saving associating object returned with find back to parent object is impossible as of now. Refer to this issue https://github.com/rails/rails/issues/17466.
I had the same issue, and my workaround was, even though this is not you nor I wanted, to use save in the method yourself and make all the saves inside the transaction.
def encrypted_password=(pw)
self.authentications.find_or_initialize_by(provider: :password).update_attribute(code, pw)
end
Is there a way to register the changed Authentication object back to the User object so that it will be autosaved when called user.save?
If your question only consists of needing to know how to save an associated class, you can add this to your class definition:
class User < ActiveRecord::Base
has_many :authentications, autosave: true
end
The Authentication object is already referenced back to the User object via the user_id column that should be on Authentication by way of the belongs_to method. This autosave: true will save the associated object Authentication when the parent object (User) is saved.
I've got a multi-level nested form using formtastic_cocoon (jquery version of formtastic).
I am trying to do some validation in the sense of
if value is_numeric do
insert into database
else do
database lookup on text
insert id as association
end
I was hoping tha the accepts_nested_attributes_for would have an :if option, but apparently there is only the :reject_if.
Is there a way to create a validation like I describe as part of the accepts_nested_attributes_for??
-----------------------Updated as per Zubin's Response ---------------------------
I believe Zubin is on the right track with a method, but I can't seem to get it working just right. The method I am using is
def lookup_prereq=(lookup_prereq)
return if lookup_prereq.blank?
case lookup_prereq
when lookup_prereq.is_a?(Numeric) == true
self.task_id = lookup_prereq
else
self.task = Task.find_by_title(lookup_prereq)
end
end
When I trigger this function, the self.task_id is being put in the database as '0' rather than the Task.id.
I'm wondering if I'm missing something else.
I'm not completely sure that the method is actually being called. Shouldn't I need to say
lookup_prereq(attr[:prereq_id)
at some point?
-------------------further edit -----------------------
I think from what I can find that the method is called only if it is named with the same name as the value for the database, therefore I've changed the method to
def completed_task=(completed_task)
Unfortunately this is still resulting in 0 as the value in the database.
Sounds like you need a method in your nested model to handle that, eg:
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author
def lookup_author=(lookup_author)
return if lookup_author.blank?
case lookup_author
when /^\d+$/
self.author_id = lookup_author
else
self.author = Author.find_by_name(lookup_author)
end
end
end
If I want to generate a random string for a token in rails, can I use validates_uniqueness_of on it? Given that this isn't something a user will input or get an error back for it needs to be unique straight away. Or am I just being stupid?
how about:
class Token < ActiveRecord::Base
validates_uniqueness_of :random_key
before_validation_on_create :create_key_until_valid
def create_key_until_valid
self.random_key = rand.to_s.slice(2,10)
while Token.find_by_random_key(self.random_key)
self.random_key = rand.to_s.slice(2,10)
end
end
end
validates_uniqueness_of will just make sure that the attribute is unique - it won't generate the value.
I'd use before_validation to create the unique value.