There's probably a simple answer to this, but I'm lost.
I created my first Polymorphic Association today to create an activity field.
Here's the activity.rb:
class Activity < ActiveRecord::Base
belongs_to :trackable, :polymorphic => true
end
In the database for activities, I have the columns:
id
name
trackable_id
trackable_type
created_at
updated_at
Here's the note.rb:
class Note < ActiveRecord::Base
has_many :activities, :as => :trackable
after_create :create_an_activity
def create_an_activity
self.activities.build(:name => candidate_id)
end
end
In my index.html.erb view I have:
<% #activities.each do |activity| %>
<p>activity.name</p>
<% end >
My question is:
Currently, activity.name in the view is outputting the id because I have :name => candidate_id. A note is created for a candidate. But, what I really want it to output is candidate.full_name (which is in the candidates table). However, this doesn't work because full_name is not in the notes table. It's in the candidates table. Is there any way to access that? Candidates has_many notes and a note belongs_to a candidate.
enjoyed your skill share with Vin a couple months ago!
I believe what you're looking for can be accessed by going through the parent association, by calling self -> parent -> attribute:
def create_an_activity
self.activities.create(:name => self.candidate.full_name)
end
Also correct me if i'm wrong, but unless you are calling a save later on, it seems like self.activities.create is what you are looking for instead of .build
Related
So, I have read through quite a few rails active records pages, stack O questions and answers (about 12 hours of time) trying to figure out how the heck to tie all of these things together into a single query to display them on my page.
Here is my page view
Secrets with owner info
</h3>
<% #secretInfo.each do |i| %>
<p><%= i.content %> - <%= i.first_name %></p>
<p><%= i.created_at %></p>
--> "this is where I'd like to have likes for post" <--
<% end %>
and here is my controller
def show
#user = User.find(params[:id])
#secrets = Gossip.all
#mySecrets = Gossip.where(user_id: [params[:id]])
#secretInfo = Gossip.joins(:user).select("content", "first_name", "created_at")
#secretWLikesNInfo = WTF MATE?
end
Also, may help to see my models and schema so here are those
class User < ActiveRecord::Base
attr_accessor :password
has_many :gossips
has_many :likes
has_many :liked_secrets, :through => :gossips, :source => :gossip
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :gossip
class Gossip < ActiveRecord::Base
belongs_to :user
has_many :likes
has_many :liking_users, :through => :likes, :source => :user
I don't know why this seems so impossible or it could be something very simple that I am just overlooking. This all was very easy in PHP/MySQL. All help is appreciated.
Additional points for coming up with a query that allows me to see all posts that I as a user has created AND liked!
Well, what you want to do is eager loading: load data associated with a record in a single roundtrip to the database. For example, i think you can load all your data like this:
#user = User.where(id: params[:id])
.joins(:liked_secrets)
.includes(:liked_secrets => :likes)
.first!
#secretInfo = #user.liked_secrets.map do |secret|
OpenStruct.new(
content: secret.content,
first_name: user.first_name,
created_at: secret.created_at,
likes: secret.likes
)
end
This works by including in the data fetched from the database in the first query all the data associated included in the include parameter. So, calling #user.liked_secrets will return the secrets but won't call the database because that information already came from the database in the first query. The same happens if you do #user.liked_secrets.first.likes because of the :linked_secrets => :likes parameter on the initial query.
I'll let a link to a good blog post about this here:
http://blog.arkency.com/2013/12/rails4-preloading/.
And, if you feel the Rails ORM (ActiveRecord) doesn't really works for your use case, you can just use sql in a string or fallback to use another Ruby ORM out there (like Sequel).
I have three tables User, User_types and Purchases.
user: id etc
user_purchase_types: id, typename, user_id
purchases: id, user_id, user_purchase_type_id, note
Users can have any number of purchases and user_types. A purchase can have one user_purchase_type.
Users can log in, create types, do purchases etc - this all works fine
However I want, when listing purchases, to see the user_purchase_types.typename, rather than the id number. Simple I think, use belongs_to, they already have the right id fields, should just work. But it doesn't
I have tried a million variations of belongs_to, has_many , has_many through etc etc but cannot get the right relationship and so show the typename rather than the id.
There are the right foreign_key id fields in the tables so this should work.
When listing the purchases in the purchase controller I use #purchase = current_user.purchases.
When looping this to display in the view I think I should be able to use purchase.user_purchase_type.typename but this gives me a 'NoMethodError'.
What am I doing wrong or should I just denormalise the DB and have done with it?
EDIT Models as req
class UserPurchaseType < ActiveRecord::Base
belongs_to :user
belongs_to :purchase
attr_accessible :typename, :id
end
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :purchases, :dependent => :destroy
has_many :user_purchase_types, :dependent => :destroy
end
class Purchase < ActiveRecord::Base
belongs_to :user
has_many :user_purchase_types
attr_accessible :date, :user_purchase_type_id, :note
end
index.erb file
<% #purchases.each do |purchase| %> #this works
<tr>
<td><%= purchase.user_purchase_type.typename %></td> #this fails
<td><%= purchase.date %></td>
<td><%= purchase.note %></td>
</tr><p>
<% end %>
purchase controller
def index
#user_purchase_types = current_user.user_purchase_types # this works
#purchases = current_user.purchases #this works
You just need to add foreign key in your existing association.
class Purchase < ActiveRecord::Base
belongs_to :user
has_many :user_purchase_types, :foreign_key => 'user_purchase_type_id'
attr_accessible :date, :user_purchase_type_id, :note
end
there is a one to one relationship between them rgt?
yes you should be able to be able to access typename, but by using #purchase.user_type.typename. and not purchase.user_type.typename.
and would be better if you could show user models too.
and is current_user defined?
also you can try by finding out the usertype first and then access it:
#user_type = UserType.find(#purchase.user_type_id)
#user_type.typename
You should always try the relationships first on console and see if your getting it rgt
Your relationships are all messed up.
You have a one to many relationships between purchases and user purchase type.
and therefore you can not use #purchase.user_purchase_type.typename
You have to use
#purchase.user_purchase_types.each do |i|
i.typename
end
class Purchase < ActiveRecord::Base
belongs_to :user
has_many :user_purchase_types
attr_accessible :date, :user_purchase_type_id, :note
end
The Purchase -> UserPurchaseType association is a has_many relationship but you seem to be trying to use it with user_purchase_type_id which would indicate a belongs_to :user_purchase_type relationship, but your association is has_many :user_purchase_types
It sounds like you may want something like:
<% #purchases.each do |purchase| %> #this works
<tr>
<td><%= purchase.user_purchase_types.collect(&:typename).join(',') %></td>
<td><%= purchase.date %></td>
<td><%= purchase.note %></td>
</tr><p>
<% end %>
to list all your UserPurchaseType typename's, comma separated.
If you do this, make sure when you load your #purchases in the controller to also include a .includes(:user_purchase_types). This will eager load the association and avoid rails loading each UserPurchaseType as you iterate through.
I'm using Rails' accepts_nested_attributes_for method with great success, but how can I have it not create new records if a record already exists?
By way of example:
Say I've got three models, Team, Membership, and Player, and each team has_many players through memberships, and players can belong to many teams. The Team model might then accept nested attributes for players, but that means that each player submitted through the combined team+player(s) form will be created as a new player record.
How should I go about doing things if I want to only create a new player record this way if there isn't already a player with the same name? If there is a player with the same name, no new player records should be created, but instead the correct player should be found and associated with the new team record.
When you define a hook for autosave associations, the normal code path is skipped and your method is called instead. Thus, you can do this:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
This code is untested, but it should be pretty much what you need.
Don't think of it as adding players to teams, think of it as adding memberships to teams. The form doesn't work with the players directly. The Membership model can have a player_name virtual attribute. Behind the scenes this can either look up a player or create one.
class Membership < ActiveRecord::Base
def player_name
player && player.name
end
def player_name=(name)
self.player = Player.find_or_create_by_name(name) unless name.blank?
end
end
And then just add a player_name text field to any Membership form builder.
<%= f.text_field :player_name %>
This way it is not specific to accepts_nested_attributes_for and can be used in any membership form.
Note: With this technique the Player model is created before validation happens. If you don't want this effect then store the player in an instance variable and then save it in a before_save callback.
A before_validation hook is a good choice: it's a standard mechanism resulting in simpler code than overriding the more obscure autosave_associated_records_for_*.
class Quux < ActiveRecord::Base
has_and_belongs_to_many :foos
accepts_nested_attributes_for :foos, reject_if: ->(object){ object[:value].blank? }
before_validation :find_foos
def find_foos
self.foos = self.foos.map do |object|
Foo.where(value: object.value).first_or_initialize
end
end
end
When using :accepts_nested_attributes_for, submitting the id of an existing record will cause ActiveRecord to update the existing record instead of creating a new record. I'm not sure what your markup is like, but try something roughly like this:
<%= text_field_tag "team[player][name]", current_player.name %>
<%= hidden_field_tag "team[player][id]", current_player.id if current_player %>
The Player name will be updated if the id is supplied, but created otherwise.
The approach of defining autosave_associated_record_for_ method is very interesting. I'll certainly use that! However, consider this simpler solution as well.
Just to round things out in terms of the question (refers to find_or_create), the if block in Francois' answer could be rephrased as:
self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save!
This works great if you have a has_one or belongs_to relationship. But fell short with a has_many or has_many through.
I have a tagging system that utilizes a has_many :through relationship. Neither of the solutions here got me where I needed to go so I came up with a solution that may help others. This has been tested on Rails 3.2.
Setup
Here are a basic version of my Models:
Location Object:
class Location < ActiveRecord::Base
has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
has_many :city_tags, :through => :city_taggables
accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end
Tag Objects
class CityTaggable < ActiveRecord::Base
belongs_to :city_tag
belongs_to :city_taggable, :polymorphic => true
end
class CityTag < ActiveRecord::Base
has_many :city_taggables, :dependent => :destroy
has_many :ads, :through => :city_taggables
end
Solution
I did indeed override the autosave_associated_recored_for method as follows:
class Location < ActiveRecord::Base
private
def autosave_associated_records_for_city_tags
tags =[]
#For Each Tag
city_tags.each do |tag|
#Destroy Tag if set to _destroy
if tag._destroy
#remove tag from object don't destroy the tag
self.city_tags.delete(tag)
next
end
#Check if the tag we are saving is new (no ID passed)
if tag.new_record?
#Find existing tag or use new tag if not found
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
else
#If tag being saved has an ID then it exists we want to see if the label has changed
#We find the record and compare explicitly, this saves us when we are removing tags.
existing = CityTag.find_by_id(tag.id)
if existing
#Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
if tag.label != existing.label
self.city_tags.delete(tag)
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
end
else
#Looks like we are removing the tag and need to delete it from this object
self.city_tags.delete(tag)
next
end
end
tags << tag
end
#Iterate through tags and add to my Location unless they are already associated.
tags.each do |tag|
unless tag.in? self.city_tags
self.city_tags << tag
end
end
end
The above implementation saves, deletes and changes tags the way I needed when using fields_for in a nested form. I'm open to feedback if there are ways to simplify. It is important to point out that I am explicitly changing tags when the label changes rather than updating the tag label.
Answer by #François Beausoleil is awesome and solved a big problem. Great to learn about the concept of autosave_associated_record_for.
However, I found one corner case in this implementation. In case of update of existing post's author(A1), if a new author name(A2) is passed, it will end up changing the original(A1) author's name.
p = Post.first
p.author #<Author id: 1, name: 'JK Rowling'>
# now edit is triggered, and new author(non existing) is passed(e.g: Cal Newport).
p.author #<Author id: 1, name: 'Cal Newport'>
Oringinal code:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
It is because, in case of edit, self.author for post will already be an author with id:1, it will go in else, block and will update that author instead of creating new one.
I changed the code(elsif condition) to mitigate this issue:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
elsif author && author.persisted? && author.changed?
# New condition: if author is already allocated to post, but is changed, create a new author.
self.author = Author.new(name: author.name)
else
# else create a new author
self.author.save!
end
end
end
#dustin-m's answer was instrumental for me - I am doing something custom with a has_many :through relationship. I have a Topic which has one Trend, which has many children (recursive).
ActiveRecord does not like it when I configure this as a standard has_many :searches, through: trend, source: :children relationship. It retrieves topic.trend and topic.searches but won't do topic.searches.create(name: foo).
So I used the above to construct a custom autosave and am achieving the correct result with accepts_nested_attributes_for :searches, allow_destroy: true
def autosave_associated_records_for_searches
searches.each do | s |
if s._destroy
self.trend.children.delete(s)
elsif s.new_record?
self.trend.children << s
else
s.save
end
end
end
I am trying to figure out how to display a count for records that have been created in a table since the last_request_at of a user.
In my view I am counting the notes of a question with the following code:
<% unless #questions.empty? %>
<% #questions.each do |question| %>
<%= h(question.notes.count) %>
end
end
This is happening in the /views/users/show.html.erb file. Instead of counting all the notes for the question, I would only like to count the notes that have been created since the users last_request_at datetime. I don't neccessarily want to scope notes to display this 'new notes' count application wide, just simply in this one instance.
To accomplish I am assuming I need to create a variable in the User#show action and call it in the view but not really sure how to do that.
Other information you may need:
class Note < ActiveRecord::Base
belongs_to :user
belongs_to :question
end
class Question < ActiveRecord::Base
has_many :notes, :dependent => :destroy
belongs_to :user
end
Just create a named scope and then use it only when it applies:
class Note < ActiveRecord::Base
named_scope :added_since, lambda { |time| {
:conditions => time && [ 'created_at>=?', time ]
}}
end
This should only enforce conditions if a time is provided. If you submit a nil time, the default behavior is to scope all notes.
This way you can do something along the lines of:
#new_notes = #user.notes.added_since(#user.last_login_at)
Adding a named scope does not alter the default scope.
I have two Models: Campaign and Contact.
A Campaign has_many Contacts.
A Contact has_many Campaigns.
Currently, each Contact has a contact.date_entered attribute. A Campaign uses that date as the ate to count down to the different Events that belong_to the Campaign.
However, there are situations where a Campaign for a specific Contact may need to be delayed by X number of days. In this instance, the campaigncontact.delaydays = 10.
In some cases, the Campaign must be stopped altogether for the specific Contact, so for now I set campaigncontact.delaydays = 1. (Are there major problems with that?)
By default, I am assuming that no campaigncontact exists (but not sure how that works?)
So here's what I've tried to do:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
end
class Campaign < ActiveRecord::Base
has_many :campaigncontacts
has_many :contacts, :through => :campaigncontacts
end
script/generate model campaigncontact campaign_id:integer contact_id:integer delaydays:integer
class Campaigncontact < ActiveRecord::Base
belongs_to :campaign
belongs_to :contact
end
So, here's the question: Is the above correct? If so, how do I allow a user to edit the delay of a campaign for a specific Contact.
For now, I want to do so from the Contact View.
This is what I tried:
In the Contact controller (?)
in_place_edit_for :campaigncontact, column.delaydays
And in the View
<%= in_place_editor_field :campaigncontact, :delaydays %>
How can I get it right?
I would add an integer field to your Campaigncontacts resource called days_to_delay_communication_by, since this information relates to the association of a campaign and a contact rather than a contact itself.
in your migration:
def self.up
add_column(:campaigncontacts, :days_to_delay_communication_by, :integer)
end
def self.down
remove_column(:campaigncontacts, :days_to_delay_communication_by)
end
Now you can set that value by:
campaigncontact = Campaigncontacts.find(:first, :conditions => { :campaign_id => campaign_id, :contact_id => contact_id })
campaigncontact.days_to_delay_communication_by = 10
Then in the admin side of your application you can have a controller and a view for campaign communications that lets you set the days_to_delay_communication_by field for campaigncontacts. I can expand on this further for you if you're interested, but I think you get the idea.
Then you'll need to run a background process of some sort (probably a cron job, or use the delayed_job plugin), to find communications that haven't happened yet, and make them happen when the date has passed. You could do this in a rake task like so:
namespace :communications do
namespace :monitor do
desc 'Monitor and send communications for campaigns'
task :erma => :environment do
Rails.logger.info "-----BEGIN COMMUNICATION MONITORING-----"
unsent_communications = Communication.all(:conditions => { :date_sent => nil})
unsent_communications.each do |communication|
Rails.logger.info "**sending communication**"
communication.send if communication.time_to_send < Time.now
Rails.logger.info "**communication sent**"
end
Rails.logger.info "-----END COMMUNICATION MONITORING-----"
end #end erma task
end #end sync namespace
end #end db namespace
Then your cron job would do something like:
cd /path/to/application && rake communications:monitor RAILS_ENV=production
Also, I'd consider changing the name of your join model to something more descriptive of it's purpose, for instance memberships, a campaign has many memberships and a contact has many memberships. Then a membership has a days_to_delay_communication field.
A good way to do this is use a "fake" attribute on your Contact model like so:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
attr_accessor :delay
def delay #edit
self.campaigncontacts.last.delaydays
end
def delay=(val)
self.campaigncontacts.each do |c|
c.delaydays = val
end
end
end
Then just set the in_place_editor for this fake field:
in_place_edit_for :contact, :delay
and
<%= in_place_editor_field :contact, :delay %>
I'm not sure I understood exactly what you wanted to accomplish, but I hope this at least points you into the right direction.