I have a users_manager engine which has a User model class.
In an other shopping engine, I add some associations in the User model with the code below, in shopping/lib/shopping.rb:
module Shopping
class Engine<Rails::Engine
initializer :shopping_append_user do
UsersManager::User.class_eval do
has_many :products,:class_name=>"Shopping::Product"
has_many :virtues,:class_name=>"Shopping::Virtue"
has_many :containers,:class_name=>"Shopping::Container"
has_many :concerns,:class_name=>"Shopping::Concern"
has_many :remarks,:class_name=>"Shopping::Remark"
has_many :praisings,:class_name=>"Shopping::Praising"
has_one :cart,:class_name=>"Shopping::Cart"
has_one :shop_information,:class_name=>"Shopping::ShopInformation"
has_many :comments,:class_name=>"Shopping::Comment"
has_many :created_orders,:class_name=>"Shopping::Order",:foreign_key=>"creator_id"
has_many :processing_orders,:class_name=>"Shopping::Order",:foreign_key=>"processor_id"
end
end
initializer :shopping_append_file do
Upload::File.class_eval do
has_many :image_uuids,:class_name=>"Shopping::ImageUuid"
end
end
end
def self.table_name_prefix
"shopping_"
end
end
After running rails server, the application works fine. However, after modifying one controller file, I browse the web page and it gives me the following message :
undefined method `products' for #<UsersManager::User:0x00000003022a58>
How does rails reload the file after modifying them? How can I make my engine work right?
My version of rails is 3.2.0.pre from github, Ruby is 1.9.0.
Your initializer isn't reloaded on every request, this means that your customizations on the UsersManager::User class are lost when it is reloaded.
You can do the following instead:
module Shopping
class Engine < Rails::Engine
config.to_prepare do
Shopping.customize_user
Shopping.customize_file
end
end
def self.customize_user
UsersManager::User.class_eval do
has_many :products,:class_name=>"Shopping::Product"
has_many :virtues,:class_name=>"Shopping::Virtue"
has_many :containers,:class_name=>"Shopping::Container"
has_many :concerns,:class_name=>"Shopping::Concern"
has_many :remarks,:class_name=>"Shopping::Remark"
has_many :praisings,:class_name=>"Shopping::Praising"
has_one :cart,:class_name=>"Shopping::Cart"
has_one :shop_information,:class_name=>"Shopping::ShopInformation"
has_many :comments,:class_name=>"Shopping::Comment"
has_many :created_orders,:class_name=>"Shopping::Order",:foreign_key=>"creator_id"
has_many :processing_orders,:class_name=>"Shopping::Order",:foreign_key=>"processor_id"
end
end
def self.customize_file
Upload::File.class_eval do
has_many :image_uuids,:class_name=>"Shopping::ImageUuid"
end
end
def self.table_name_prefix
"shopping_"
end
end
The config.to_prepare block is run once in production and before every request in development (source).
Related
Trying to build a friends feature on top of a Solidus framework, but the .friends method does not work. It does in rails console, however.
SpreeUsers Controller (current_spree_user.friends causes error):
class SpreeUsersController < ApplicationController
def my_friends
#friendships = current_spree_user.friends
end
def search
#spree_users = SpreeUser.search(params[:search_param])
render json: #spree_users
end
end
Friendship Model:
class Friendship < ActiveRecord::Base
belongs_to :spree_user
belongs_to :friend, :class_name => 'SpreeUser'
end
SpreeUser Model:
class SpreeUser < ActiveRecord::Base
has_many :friendships
has_many :friends, through: :friendships
end
Error:
undefined method `friends' for # Did you mean? friendly_id?
https://i.stack.imgur.com/u4F8K.png
Console Input/Output:
https://i.stack.imgur.com/YuVjb.png
I believe I know what is happening here.
You have declared a model SpreeUser with a friends relationship. This is why in your console it works. You are correctly calling the friends method on the class SpreeUser.
The error you are receiving is for the class Spree::User (Note the different class names). I am assuming you are using spree_auth_devise which provides that class.
You will need to properly add your logic from SpreeUser to Spree::User. I believe Spree/Solidus recommends doing so using decorators.
EG
Spree::User.class_eval do
has_many :friendships
has_many :friends, through: :friendships
end
Above not tested and a best guess taken from this SO answer
You could also test my theory by running Spree::User.first.friends in your console. You should receive a similar error to the browser.
I think it is more of a "Model Design" issue than a rails issue.
For clarity sake here is the business logic: I've Venues and I want to implement multiple APIs to get data about those venues. All this APIs have a lot in common, therefore I used STI.
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
end
# /app/models/api.rb
class Api < ApplicationRecord
belongs_to :venue
end
# /app/models/google_api.rb
class GoogleApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
# /app/models/other_api.rb
class OtherApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
That part works, now what I'm trying to add is Photos to the venue. I will be fetching those photos from the API and I realise that every API might be different. I thought about using STI for that as well and I will end up with something like that
# /app/models/api_photo.rb
class ApiPhoto < ApplicationRecord
belongs_to :api
end
# /app/models/google_api_photo.rb
class GoogleApiPhoto < ApiPhoto
def url
"www.google.com/#{reference}"
end
end
# /app/models/other_api_photo.rb
class OtherApiPhoto < ApiPhoto
def url
self[url] || nil
end
end
My goal being to have this at the end
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
has_many :photos :through => :apis
end
# /app/views/venues/show.html.erb
<%# ... %>
#venue.photos.each do |photo|
photo.url
end
<%# ... %>
And photo.url will give me the right formatting that is dependent of the api it is.
As I'm going deeper in the integration, something seems not right. If I had to Api the has_many :google_api_photo then every Api will have GoogleApiPhoto. What does not make sense to me.
Any idea how I should proceed from here?
I think I solved it.
By adding this to venue.rb
has_many :apis, :dependent => :destroy
has_many :photos, :through => :apis, :source => :api_photos
By calling venue.photos[0].url call the right Class based on the type field of the ApiPhoto
I am running into a weird issue, and reading the callbacks RoR guide didn't provide me an answer.
class User < ActiveRecord::Base
...
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
has_many :user_teams, dependent: :destroy
has_many :teams, through: :user_teams
before_create :check_company!
after_create :check_team
def check_company!
return if self.companies.present?
domain = self.email_domain
company = Company.find_using_domain(domain)
if company.present?
assign_company(company)
else
create_and_assign_company(domain)
end
end
def check_team
self.companies.each do |company|
#do stuff
end
end
...
end
The after_create :check_team callback is facing issues because the line
self.companies.each do |company|
Here, self.companies is returning an empty array [] even though the Company and User were created and the User was associated with it. I know I can solve it by making it a before_create callback instead. But I am puzzled!
Why does the after_create callback not have access to self's associations after the commit?
Solution: Please read my comments in the accepted answer to see the cause of the problem and the solution.
inside before_create callbacks, the id of the record is not yet available, because it is before... create... So it is not yet persisting in the database to have an id. This means that the associated company_user record doesn't have a user_id value yet, precisely because the user.id is still nil at that point. However, Rails makes this easy for you to not worry about this "chicken-and-egg" problem, provided that you do it correctly:
I recreated your setup (Company, User, and CompanyUser models), and the following is what should work on your case (tested working):
class User < ApplicationRecord
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
before_create :check_company!
after_create :check_team
def check_company!
# use `exists?` instead of `present?` because `exists?` is a lot faster and efficient as it generates with a `LIMIT 1` SQL.
return if companies.exists?
## when assigning an already persisted Company record:
example_company = Company.first
# 1) WORKS
companies << example_company
# 2) WORKS
company_users.build(company: example_company)
## when assigning and creating a new Company record:
# 1) WORKS (this company record will be automatically saved/created after this user record is saved in the DB)
companies.build(name: 'ahaasdfwer') # or... self.companies.new(name: 'ahaasdfwer')
# 2) DOES NOT WORK, because you'll receive an error `ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved`
companies.create(name: 'ahaasdfwer')
end
def check_team
puts companies.count
# => 1 if "worked"
puts companies.first.persisted?
# => true if "worked"
end
end
I've seen a couple examples of the older syntax, but I can't find an example using the new 3.x syntax (one such older example: factory girl multiple has_many through's).
Models
class RawPosition < ActiveRecord::Base
has_many :position_translations
has_many :specific_positions, through: :position_translations
end
class SpecificPosition < ActiveRecord::Base
has_many :position_translations
has_many :raw_positions, through: :position_translations
end
class PositionTranslation < ActiveRecord::Base
belongs_to :raw_position
belongs_to :specific_position
end
Factories
factory :raw_poisition_multiple, class: RawPosition do
raw_input "WR/QB"
sport_type_id 5
after_create do |a|
#a.specific_positions.create({specific_position: "WR"})
#a.specific_positions.create({specific_position: "QB"})
FactoryGirl.create(:specific_position, raw_position: a)
FactoryGirl.create(:qb_specific_position, raw_position: a)
end
end
factory :specific_position do
specific_position "WR"
end
factory :qb_specific_position do
specific_position "QB"
end
Spec
describe "WR/QB" do
before do
#player.player_dict['POS'] = "WR/QB"
FactoryGirl.create(:raw_poisition_multiple)
#player.clean_position(#player_to_team_history)
end
....
end
If I uncomment the lines from the raw_position_multiple factory and comment out the FactoryGirl lines in the after_create block, things work fine. I'd just like the ability to use a factory to create the associations.
I was able to get past a similar issue by reloading the root model after creating the associations. In your example, that would mean adding:
a.reload
to the end of your after_create block.
I have the following three models
LegacyRole:
class LegacyRole < LegacyModel
has_many :permissions_roles
has_many :permissions, :through => :permissions_roles
end
LegacyPermissionsRole:
class LegacyPermissionsRole < LegacyModel
belongs_to :role
belongs_to :permission
end
and LegacyPermission:
class LegacyPermission < LegacyModel
has_many :permissions_roles
has_many :roles, :through => :permissions_roles
end
And in order for these to all work, and connect the legacy database and whatnot, I have the following class LegacyModel which is possibly trying to be too clever...
require 'active_record'
class LegacyModel < ActiveRecord::Base
self.abstract_class = true
establish_connection "legacy_#{::Rails.env}"
def self.inherited(subclass)
tabeleized_name = subclass.name.tableize
raise "Legacy models must be prefixed with 'Legacy'" unless tabeleized_name.start_with?('legacy_')
logger.info "***********LOAD***********"
logger.info "Loaded legacy model: #{subclass.name} using table: #{tabeleized_name.gsub('legacy_', '')}"
super
subclass.set_table_name tabeleized_name.gsub('legacy_','')
end
# these methods do much the same thing, can probably abstract some of this out
def self.belongs_to(association_id, options = {})
new_association = association_id.to_s.insert(0, 'legacy_').to_sym
old_association = association_id
logger.info "Legacy model has belongs_to association: '#{association_id}'"
association_id = association_id.to_s.insert(0, 'legacy_').to_sym
logger.info "Converting association to: '#{association_id}'"
unless options.has_key?(:foreign_key)
# our foreign_key is missing
options[:foreign_key] = old_association.to_s.foreign_key
logger.info("Foreign_key was missing, is now: #{options[:foreign_key]}")
end
super
alias_method old_association, new_association
end
def self.has_many(association_id, options = {})
new_association = association_id.to_s.insert(0, 'legacy_').to_sym
old_association = association_id
logger.info "Legacy model has_many to association: '#{association_id}'"
association_id = association_id.to_s.insert(0, 'legacy_').to_sym
logger.info "Converting association to: '#{association_id}'"
logger.debug("Association options are: #{options.inspect}")
if options.has_key?(:through)
options[:through] = options[:through].to_s.insert(0, 'legacy_')
logger.info("Through mutated, is now: #{options[:through]}")
end
super
alias_method old_association, new_association
end
end
Whenever I try to access permissions on an instance of LegacyRole, I get the following Active Record error:
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association "legacy_permissions_roles" in model LegacyRole
I've stepped through all this as best I can and I really can't figure out why this is occurring, obviously with this being a bit more complicated than standard by the LegacyModel class I really don't know how to diagnose this further... I'm now at the point with it where I can't see the forest for the trees and feel it might just be something really simple that I've missed out!
Edit:
Here is the log output from the models loading
****************************
Loaded legacy model: LegacyPermission using table: permissions
Legacy model has_many association: 'permissions_roles'
Converting association to: 'legacy_permissions_roles'
Association options are: {}
Legacy model has_many association: 'roles'
Converting association to: 'legacy_roles'
Association options are: {:through=>:permissions_roles}
Changed :through to: 'legacy_permissions_roles'
****************************
Loaded legacy model: LegacyPermissionsRole using table: permissions_roles
Legacy model has belongs_to association: 'role'
Converting association to: 'legacy_role'
Legacy model has belongs_to association: 'permission'
Converting association to: 'legacy_permission'
Foreign_key was missing, is now: 'permission_id'
****************************
Loaded legacy model: LegacyRole using table: roles
Legacy model has_many association: 'permissions_roles'
Converting association to: 'legacy_permissions_roles'
Association options are: {}
Legacy model has_many association: 'permissions'
Converting association to: 'legacy_permissions'
Association options are: {:through=>:permissions_roles}
Changed :through to: 'legacy_permissions_roles'
Perhaps you want
class LegacyRole < LegacyModel
has_many :permissions_roles
has_many :permissions, :through => :legacy_permissions_roles # note the `legacy` prefix
end
Or was this a typo in your post?