Setter methods can't see child built in parents after_initialize callback - ruby-on-rails

An Email has many Variants (for ab testing purposes) and always has one set as the master
I want to ensure an email always has a master variant built on initialization.
I also want to delegate attr accessors 'subject' and 'body' to the master variant.
I originally tried using
delegate :subject, :body, to: :master
but rails complained master was nil.
So I tried hand rolling my own subject= setter method and via pry I found that whilst my master is being set in the after_initialize callback, the subsequent call to subject= complains master is nil. I dont understand why.
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
def subject=(str)
master.subject = str # Rails says master is nil here
end
#
# Callbacks
#-----------------------------------------
after_initialize :ensure_master
def ensure_master
return unless new_record?
self.master ||= build_master
end
end
Email.new(:subject => 'yah') # undefined method `subject=' for nil:NilClass

when your email instance is initialized your master is nil, you need to trigger build_master before you set anything on it
how about:
# app/models/email.rb
class Email < ActiveRecord::Base
def subject=(str)
master.subject = str # Rails says master is nil here
end
def master
super || build_master
end
end
Don't know much when and why you need after_initialize callback in your project, but if you sure you need this functionality, I would consider to use custom service class to achieve this
# app/models/email.rb
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
def subject=(str)
master.subject = str # this way Rails won't says master is nil here
end
end
# app/lib/email_builder.rb
class EmailBuilder
attr_reader :args
def self.build(args={})
new(args).build
end
def initialize(args)
#args = args
end
def build
email = Email.new
email.build_master
email.attributes = args
email
end
end
email = EmailBuilder.build subject: 'yah'
email.class # => Email
...and another variation of this for persisted record
Update
or even better
# app/models/email.rb
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
end
# app/lib/email_builder.rb
class EmailBuilder
attr_reader :args, :subject
def self.build(args={})
new(args).build
end
def initialize(args)
#subject = args.fetch(:subject)
#args = args
end
def build
email = Email.new args
email.build_master
email.master.subject = subject
email
end
end
email = EmailBuilder.build subject: 'yah'
email.class # => Email

Related

Rails association build with strong parameters and argument

In my messages controller I wish to build a message that belongs_to a #sender (class Character). However, messages belong_to not only Characters but also Conversations, and this is causing the problem. The build method of the message needs to be passed the Conversation id to which it belongs so the message can pass validations. My code successfully finds the #conversation via an sql search, but how do I correctly pass both the #conversation.id and the message_params to the #sender.messages.build as strong parameters?
messages_controller.rb
def create
#sender = Character.find_by(callsign: params[:callsign])
#recipient = Character.find_by(callsign: params[:recipient])
sender_id = #sender.id
recipient_id = #recipient.id
conversationID = Conversation.find_by_sql("
SELECT senderConversations.conversation_id FROM Chats AS senderConversations
INNER JOIN Chats AS recipientConversations
ON senderConversations.conversation_id=recipientConversations.conversation_id
WHERE senderConversations.character_id='#{sender_id}'
AND recipientConversations.character_id='#{recipient_id}'
GROUP BY senderConversations.conversation_id
HAVING count(distinct senderConversations.character_id) = 2
; ")
#conversation = Conversation.find_by(id: conversationID)
if(#conversation.nil?)
#conversation = #sender.conversations.create
#recipient.conversations << #conversation
end # It all works great up to here!
#message = #sender.messages.build(message_params, #conversation) # Can't get this working
if #message.save
#conversation.messages << #message
respond_to do |format|
format.html do
end
format.js do
end
end
else
redirect_to request.referrer || root_url
end
end
private
def message_params
params.require(:message).permit( :content, :picture )
end
character.rb
has_many :chats, foreign_key: "character_id",
dependent: :destroy
has_many :conversations, through: :chats, source: :conversation
has_many :messages
conversation.rb
has_many :messages
chat.rb
belongs_to :character
belongs_to :conversation
message.rb
belongs_to :character
belongs_to :conversation
validates :character_id, presence: true
validates :conversation_id, presence: true
To add conversation to the message, try
#message = #sender.messages.build(message_params.merge(:conversation => #conversation))
A couple suggestions -
First, move all the querying code to the Message model. Given the params you're setting, the sender and recipient, you should be able to find or create the conversation and attach it correctly. Reduce the complexity of the controller.
Also, try to move that sql into a rails query if you can and set a scope. Not sure I completely understand the schema, but something like...
class Conversation < ActiveRecord::Base
scope :between, (sender, recipient) -> { joins(:chats).where(:charcter_id => [sender, recipient]).group('chats.conversations_id').having("count(distinct senderConversations.character_id) = 2") }
Then you can look for the conversation on a before_save or before_validation callback, ensuring the conversation exists. In a scope, you'll be able to reuse the sql more easily to find the conversation in other situations.

Rails undefined method 'each' for nil:NilClass

I'm setting up an internal messaging system in my rails app and I'm having trouble getting the message to actually send to another user.
class User < ActiveRecord::Base
# messages and conversations
has_many :user_conversations
has_many :conversations, through: :user_conversations
has_many :messages
class UserConversation < ActiveRecord::Base
belongs_to :user
belongs_to :conversation
before_create :create_user_conversations
accepts_nested_attributes_for :conversation
delegate :subject, to: :conversation
delegate :users, to: :conversation
attr_accessor :to
private
def create_user_conversations
to.each do |recip|
recipient = User.find(recip)
UserConversation.create(user_id: recip, conversation_id: 1)
end
end
end
class Conversation < ActiveRecord::Base
has_many :user_conversations
has_many :users, through: :user_conversations
has_many :messages
accepts_nested_attributes_for :messages
class Message < ActiveRecord::Base
belongs_to :user_conversation
belongs_to :user
And here is my user_conversation_controller:
class UserConversationsController < ApplicationController
def new
#user = User.find(params[:user_id])
#conversation = #user.user_conversations.build
#conversation.build_conversation.messages.build
end
def create
#conversation = UserConversation.new(conversation_params)
#conversation.user = current_user
#conversation.conversation.messages.first.user = current_user
if #conversation.save
redirect_to user_conversation_path(current_user, #conversation)
else
flash[:error] = "There was an error"
render 'new'
end
end
private
def conversation_params
params.require(:user_conversation).permit(:to => [],
conversation_attributes: [:subject,
messages_attributes: [:body]])
end
The error comes in the create_user_conversations method in the UserConversation model. When I try to run
to.each do |recip|
I get an "undefined method 'each' for nil:NilClass" error. However, the "to" array has a value in it, in this case the parameters looked like this:
{"utf8"=>"✓",
"user_conversation"=>{"to"=>["2"],
"conversation_attributes"=>{"subject"=>"Hey",
"messages_attributes"=>{"0"=>{"body"=>"hey"}}}},
"commit"=>"Create User conversation",
"user_id"=>"1"}
Any ideas on why that array isn't getting passed in correctly? Thanks.
You define to as an attr_accessor, which will create get/set methods for an instance variable #to. You're using to as a local variable in your private method create_user_conversations though. This explains the nil:NilClass error.
Try changing the local variable to be an instance variable instead.
I solved my problem by going ahead and adding a recipients_id column to my user_conversations table, then in my UserConversations controller I was able to do
def create
#conversation = UserConversation.new(user_conversation_params)
#conversation.user = current_user
#conversation.conversation.messages.first.user_id = current_user.id
if #conversation.save
UserConversation.recipient_id = #conversation.recipient_id
redirect_to user_conversation_path(current_user, #conversation)
create_user_conversations
else
flash[:error] = "There was an error"
render 'new'
end
end
With the private method create_user_conversations also in my UserConversations controller:
def create_user_conversations
UserConversation.recipient_id.each do |recip|
recipient = User.find(recip)
UserConversation.create(user: recipient, conversation: #conversation.conversation)
end
end
I doubt this is the most elegant way to do this, but it at least gets the job done.

Newsfeed in Rails, unidentified method

I'm creating a simple newsfeed in rails. The aim is for it to return all the posts from the groups the user is following. I am using socialization for my follow functionality.
The exact error is:
NoMethodError (undefined method `followees' for false:FalseClass)
Here are my basic models not including like and follow as they're empty:
User:
class User < ActiveRecord::Base
authenticates_with_sorcery!
attr_accessible :username, :password, :email
has_many :groups
has_many :posts
acts_as_follower
acts_as_liker
before_create :generate_auth_token
def auth_token_expired?
auth_token_expires_at < Time.now
end
def generate_auth_token(expires = nil)
self.auth_token = SecureRandom.hex(20)
self.auth_token_expires_at = expires || 1.day.from_now
end
def regenerate_auth_token!(expires = nil)
Rails.logger.info "Regenerating user auth_token"
Rails.logger.info " Expiration: #{expires}" if expires
generate_auth_token(expires)
save!
end
end
Group:
class Group < ActiveRecord::Base
attr_accessible :description, :name, :user_id
has_many :posts
belongs_to :user
acts_as_followable
end
Post:
class Post < ActiveRecord::Base
attr_accessible :body, :user_id, :group_id
belongs_to :user
belongs_to :group
acts_as_likeable
end
I have setup a function named newsfeed in my post controller. The function grabs all the groups that a user is following and then grabs all the posts that have group_ids matching group_ids in the returned groups array. But I keep getting unidentified method followees(socialization provides this). Yet it appears to work when using single users and posts in irb.
def newsfeed
#groups = current_user.followees(Group)
#posts = Post.where(:group_id => #groups)
respond_to do |format|
format.html # show.html.erb
format.json { render json: #posts }
end
end
Thanks for any help.
Apparently, your current_user method returns false, instead of a user. Check what's returned from that method, as find out why you get the error...
Your current_user return false instead of instance of User. You may see it from error text.

How to create another object when creating a Devise User from their registration form in Rails?

There are different kinds of users in my system. One kind is, let's say, a designer:
class Designer < ActiveRecord::Base
attr_accessible :user_id, :portfolio_id, :some_designer_specific_field
belongs_to :user
belongs_to :portfolio
end
That is created immediately when the user signs up. So when a user fills out the sign_up form, a Devise User is created along with this Designer object with its user_id set to the new User that was created. It's easy enough if I have access to the code of the controller. But with Devise, I don't have access to this registration controller.
What's the proper way to create a User and Designer upon registration?
In a recent project I've used the form object pattern to create both a Devise user and a company in one step. This involves bypassing Devise's RegistrationsController and creating your own SignupsController.
# config/routes.rb
# Signups
get 'signup' => 'signups#new', as: :new_signup
post 'signup' => 'signups#create', as: :signups
# app/controllers/signups_controller.rb
class SignupsController < ApplicationController
def new
#signup = Signup.new
end
def create
#signup = Signup.new(params[:signup])
if #signup.save
sign_in #signup.user
redirect_to projects_path, notice: 'You signed up successfully.'
else
render action: :new
end
end
end
The referenced signup model is defined as a form object.
# app/models/signup.rb
# The signup class is a form object class that helps with
# creating a user, account and project all in one step and form
class Signup
# Available in Rails 4
include ActiveModel::Model
attr_reader :user
attr_reader :account
attr_reader :membership
attr_accessor :name
attr_accessor :company_name
attr_accessor :email
attr_accessor :password
validates :name, :company_name, :email, :password, presence: true
def save
# Validate signup object
return false unless valid?
delegate_attributes_for_user
delegate_attributes_for_account
delegate_errors_for_user unless #user.valid?
delegate_errors_for_account unless #account.valid?
# Have any errors been added by validating user and account?
if !errors.any?
persist!
true
else
false
end
end
private
def delegate_attributes_for_user
#user = User.new do |user|
user.name = name
user.email = email
user.password = password
user.password_confirmation = password
end
end
def delegate_attributes_for_account
#account = Account.new do |account|
account.name = company_name
end
end
def delegate_errors_for_user
errors.add(:name, #user.errors[:name].first) if #user.errors[:name].present?
errors.add(:email, #user.errors[:email].first) if #user.errors[:email].present?
errors.add(:password, #user.errors[:password].first) if #user.errors[:password].present?
end
def delegate_errors_for_account
errors.add(:company_name, #account.errors[:name].first) if #account.errors[:name].present?
end
def persist!
#user.save!
#account.save!
create_admin_membership
end
def create_admin_membership
#membership = Membership.create! do |membership|
membership.user = #user
membership.account = #account
membership.admin = true
end
end
end
An excellent read on form objects (and source for my work) is this CodeClimate blog post on Refactoring.
In all, I prefer this approach vastly over using accepts_nested_attributes_for, though there might be even greater ways out there. Let me know if you find one!
===
Edit: Added the referenced models and their associations for better understanding.
class User < ActiveRecord::Base
# Memberships and accounts
has_many :memberships
has_many :accounts, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :account
end
class Account < ActiveRecord::Base
# Memberships and members
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
has_many :admins, through: :memberships,
source: :user,
conditions: { 'memberships.admin' => true }
has_many :non_admins, through: :memberships,
source: :user,
conditions: { 'memberships.admin' => false }
end
This structure in the model is modeled alongside saucy, a gem by thoughtbot. The source is not on Github AFAIK, but can extract it from the gem. I've been learning a lot by remodeling it.
If you don't want to change the registration controller, one way is to use the ActiveRecord callbacks
class User < ActiveRecord::Base
after_create :create_designer
private
def create_designer
Designer.create(user_id: self.id)
end
end

Rails association with delegate methods. possible bug with AR?

Isolated this question into it's own rails app: and added a git repo as an example
Module:
module SeoMeta
def self.included(base)
base.extend(ClassMethods)
base.send :include, InstanceMethods
end
module ClassMethods
def is_seo_meta
has_one :meta,
class_name: SeoMetum,
as: :metumable,
dependent: :destroy,
autosave: true
delegate :browser_title, :meta_description, :meta_author,
:meta_keywords, :browser_title=, :meta_keywords=,
:meta_description=, :meta_author=,
to: :meta
after_save :save_meta_tags!
attr_accessible :browser_title, :meta_keywords,
:meta_description, :meta_author
end
end
module InstanceMethods
class << self
def included(base)
base.module_eval do
alias :original_meta_method :meta
end
end
end
def meta
find_meta || build_meta
end
def find_meta
#meta ||= ::SeoMetum.where(metumable_type: self.class.name, metumable_id: self.id).first
end
def build_meta
#meta ||= ::SeoMetum.new(metumable_type: self.class.name, metumable_id: self.id)
end
def save_meta_tags!
meta.metumable_id ||= self.id
meta.save
end
end
end
Models:
class User < ActiveRecord::Base
include SeoMeta
is_seo_meta
has_many :collections
accepts_nested_attributes_for :collections
def collection
default_collection = self.collections.first
default_collection ||= self.collections.create
default_collection
end
end
class Collection < ActiveRecord::Base
include SeoMeta
is_seo_meta
belongs_to :user
end
class SeoMetum < ActiveRecord::Base
attr_accessible :browser_title, :meta_author, :meta_description, :meta_keywords,
:metumable, :metumable_id, :metumable_type
belongs_to :metumable, polymorphic: true
end
Rspec Tests:
context "user and collection" do
context 'responds to' do
it 'meta_description' do
user.collection.respond_to?(:meta_description).should be_true
end
it 'browser_title' do
user.collection.respond_to?(:browser_title).should be_true
end
end
context 'individual allows us to assign to' do
it 'meta_description' do
the_collection = user.collection
the_collection.meta_description = 'This is my description of the user for search results.'
the_collection.meta_description.should == 'This is my description of the user for search results.'
end
it 'browser_title' do
the_collection = user.collection
the_collection.browser_title = 'An awesome browser title for SEO'
the_collection.browser_title.should == 'An awesome browser title for SEO'
end
end
context 'allows us to assign to' do
it 'meta_description' do
user.collection.meta_description = 'This is my description of the user for search results.'
user.collection.meta_description.should == 'This is my description of the user for search results.'
end
it 'browser_title' do
user.collection.browser_title = 'An awesome browser title for SEO'
user.collection.browser_title.should == 'An awesome browser title for SEO'
end
end
context 'allows us to update' do
it 'meta_description' do
user.collection.meta_description = 'This is my description of the user for search results.'
user.collection.save
user.collection.reload
user.collection.meta_description.should == 'This is my description of the user for search results.'
end
it 'browser_title' do
user.collection.browser_title = 'An awesome browser title for SEO'
user.collection.save
user.collection.reload
user.collection.browser_title.should == 'An awesome browser title for SEO'
end
end
end
The first four tests pass and the second four fail. I think it might be a bug with rails polymorphic associations but I'm not sure how to isolate it further. Comments on my module design are also appreciated.
Best,
Scott
The problem in your code is in this place:
class User
#Other stuff
#HERE!
def collection
default_collection = self.collections.first
default_collection ||= self.collections.create
default_collection
end
end
Every time you call collection method you look up the first collection in the database. Therefore even if you set some values by user.collection.meta_description = "abc" then later when you call user.collection it's not the same collection object cause it's been new lookup from the database. Therefore all the attributes not saved to the database are gone. You can see this by looking at logs - every time you call user.collection you get new hit to db and also every time you call user.collection.object_id you get a different value.
You can fix it by doing something like
def collection
return #collection if #collection
default_collection = self.collections.first
default_collection ||= self.collections.create
#collection = default_collection
end

Resources