Asking this question re: a student project I'm working on and I have tinkered for too long without being able to come up with a solution.
I have a class called Game, a game has many quotes. If I create a game #game = Game.create and then associate it with characters #game.characters = [#character1, #character2]. Because of my associations, I have access to all of the quotes of both characters with #game.quotes (hundreds of objects returned).
I'd like to be able to grab a sample of 10 of the quotes, something like #game.ten_quotes (an array of objects) will return a random sample of #game.quotes. I also want #game.ten_quotes to be saved to the database.
My first thought is that I need a new attribute for Game in the migration:
class CreateGames < ActiveRecord::Migration[5.1]
def change
create_table :games do |t|
t.text :state, default: "[]"
t.boolean :completed, default: false
t.something :ten_quotes
# what would this look like if I'm saving an array of objects?
t.timestamps
end
end
end
In my rails controller below I was able to generate the ten quotes but I feel that I'm working in the wrong direction:
class Game < ApplicationRecord
has_many :game_logs
has_many :characters, through: :game_logs
has_many :quotes, through: :characters
def generate_quotes
if self.ten_quotes == []
x = quotes.shuffle.sample(10)
self.ten_quotes = x
else
return false
end
end
end
How can I get a sample of quotes, associate that sample with a game instance and then save the game instance to the database one time with no chance to overwrite in the future? Do I need a new model?
Thanks in advance if you'd like to assist. Otherwise, have a great day!
class AddTenQuotesIdsColumnToGames < ActiveRecord::Migration[5.1]
def change
add_column :games, :ten_quotes_ids, :text
end
end
class Game < ApplicationRecord
has_many :game_logs
has_many :characters, through: :game_logs
has_many :quotes, through: :characters
serialize :ten_quotes_ids, Array
def ten_quotes
if ten_quotes_ids.length == 10
quotes.where(id: ten_quotes_ids)
else
ten_quotes = quotes.order('RANDOM()').limit(10) # for MySQL use `quotes.order('RAND()').limit(10)`
update ten_quotes_ids: ten_quotes.map(&:id)
ten_quotes
end
end
end
Add a column of type text to your Game model called ten_quotes_ids. In your model, serialize the ten_quotes_ids column as an Array. This lets you store arrays of Ruby objects to the database. You could store instances of quotes, but they will not be kept in sync with your database if there are changes, so better to just store the ids so you can fetch the current records on demand.
In your ten_quotes method you're checking if ten_quotes_ids has 10 elements, and if so querying the Game's quotes based on those ids and returning them, or else selecting a random set of 10 quotes belonging to the Game from the database, updating the ten_quotes_ids attribute, and returning the ten quotes.
edit 2
I removed my first idea, based your comment, as your information "a quote should belong to many samples" and "ten quotes were for a game that happened a week ago", my idea creating new model for samples with this relation as follow
class Game < ApplicationRecord
# -> quotes -> samples
has_many :quotes, :dependent => :destroy
accepts_nested_attributes_for :quotes, :allow_destroy => :true
has_many :samples, through: :quotes
end
class Quote < ApplicationRecord
belongs_to :game
has_many :samples, :dependent => :destroy
accepts_nested_attributes_for :samples, :allow_destroy => :true
end
class Sample < ApplicationRecord
belongs_to :quote
scope :just_last_week, lambda { where('created_at >= ?', 1.week.ago)}
end
I add accepts_nested_attributes_for and allow_destroy to make easier you create child record from parent
here is link in case you want to know more
from game controller
# if you want get samples and
# for specific game
#game = Games.find(params[:id])
#samples = #game.samples.limit(10) # this will get 10 samples from samples
#samples = #game.samples.just_last_week.limit(10) # this will get 10 samples created last week
# for samples that created last week no matter what game is
#samples = Sample.all.just_last_week
Related
I have the following models.
class Company < ApplicationRecord
has_many :company_users
has_many :users, :through => :company_users
after_update :do_something
private
def do_something
# check if users of the company have been updated here
end
end
class User < ApplicationRecord
has_many :company_users
has_many :companies, :through => :company_users
end
class CompanyUser < ApplicationRecord
belongs_to :company
belongs_to :user
end
Then I have these for the seeds:
Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]
Let's say I want to update Company 1 users, I will do the following:
Company.first.update :users => [User.first, User.second]
This will run as expected and will create 2 new records on CompanyUser model.
But what if I want to update again? Like running the following:
Company.first.update :users => [User.third, User.fourth]
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?
However, updating an attribute works just fine:
Company.first.update :name => 'New Company Name'
How can I make it work on associations too?
So far I have tried the following but no avail:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
There is a collection callbacks before_add, after_add on has_many relation.
class Project
has_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
#non persisted developer
...
end
end
For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
You can use attr_accessor for this and check if it changed.
class Company < ApplicationRecord
attr_accessor :user_ids_attribute
has_many :company_users
has_many :users, through: :company_users
after_initialize :assign_attribute
after_update :check_users
private
def assign_attribute
self.user_ids_attribute = user_ids
end
def check_users
old_value = user_ids_attribute
assign_attribute
puts 'Association was changed' unless old_value == user_ids_attribute
end
end
Now after association changed you will see message in console.
You can change puts to any other method.
I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
Knowing that I will advice you to try the following code:
Company.first.users << User.third
In this way you will not override current associations.
If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.
You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference
Hope it will be helpful.
Edit:
Ok I thought it wasn't your real issue.
Maybe 2 solutions:
#1 Observer:
what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.
gem rails-observers
Inside this observer call a service or whatever you like that will do what you want to do with the values
class CompanyUserObserver < ActiveRecord::Observer
def after_save(company_user)
user = company_user.user
company = company_user.company
...do what you want
end
def before_destroy(company_user)
...do what you want
end
end
You can user multiple callback in according your needs.
#2 Keep records:
It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.
Sorry for the confusion.
I'm having a hard time wrapping my head around how to build this query. I'm not sure if I should try to create a bunch of scopes and try to chain them. Or do I put it into a class method? Or would I do a combo of both? If anyone could give me a short example it would keep me from jumping out the window, I've been working on this for over a week now.
class CensusDetail < ActiveRecord::Base
belongs_to :apartment
belongs_to :resident
end
class Apartment < ActiveRecord::Base
belongs_to :apartment_type
has_many :census_details
has_many :residents, :through => :census_details
end
class ApartmentType < ActiveRecord::Base
has_many :apartments
end
class Resident < ActiveRecord::Base
has_many :census_details
has_many :apartments, :through => :census_details
end
apartment.rent_ready = boolean value if apartment is ready
apartment_type.occupany = the number of allowed occupants in an apartment
census_detail.status = either "past", "present", or "new"
census_detail.moveout_date = date time resident is scheduled to move out
I need to build a query that does the following:
- if the apartment is rent ready then do the following:
- pull a list from census_details of all residents set as "current" for each apartment
-if the # of residents is less than the value of apartment_type.occupancy for this
apartment, then list it as available
-if the # of residents is = to the value of apartment_type.occupancy then
-does any resident have a scheduled move out date
-if not, do not list the apartment
-if yes, then list apartment
Thanks in advance for any help or suggestions.
I haven't cleaned it up yet, but this is working for me, so I wanted to share.
apartment_type.rb
class ApartmentType < ActiveRecord::Base
has_many :apartments, :dependent => :nullify
end
census_detail.rb
class CensusDetail < ActiveRecord::Base
belongs_to :resident_contacts
belongs_to :apartments
scope :get_current_residents, ->(id) { where("status = ? and apartment_id = ?", "current", id) }
end
apartment.rb where most of the work is happening
class Apartment < ActiveRecord::Base
belongs_to :apartment_type
has_many :census_details
has_many :residents, :through => :census_details
scope :rent_ready, -> { where(:enabled => true) }
def self.available
available_apartments = []
rent_ready_apartments = self.rent_ready.all
rent_ready_apartments.each do |apt|
tmp = ApartmentType.where('id = ?', apt.apartment_type_id).pluck(:occupancy)
occupancy = tmp[0]
current_residents = CensusDetail.get_current_residents(apt.id)
resident_count = current_residents.count
if resident_count < occupancy
available_apartments << apt
end
if resident_count = occupancy
scheduled = false
current_residents.each do |res|
if res.moveout_date
scheduled = true
end
end
if scheduled == true
available_apartments << apt
end
end
end
available_apartments
end
end
The code is a bit ugly but its so far passing my tests. I would have edited my original question instead of answering in case someone has a better way of doing this but each time I do my question gets voted down. If anyone knows a better way please let me know because I'm at a point in the app where there are about 50 tables and now it's all getting tied together with complex queries.
I have a has_many through association setup between a song model and an artist model.
My code looks something like this
SongArtistMap Model
class SongArtistMap < ActiveRecord::Base
belongs_to :song
belongs_to :artist
end
Artist Model
class Artist < ActiveRecord::Base
has_many :song_artist_maps
has_many :songs, :through => :song_artist_maps
validates_presence_of :name
end
Song Model
class Song < ActiveRecord::Base
has_many :song_artist_maps
has_many :artists, :through => :song_artist_maps
accepts_nested_attributes_for :artists
end
I have a form where a user submits a song and enters in the song title and the song artist.
So when a user submits a song and my Artists table doesn't already have the artist for the song I want it to create that artist and setup the map in SongArtistMap
If a user submits a song with an artist that is already in the Artists table I just want the SongArtistMap created but the artist not duplicated.
Currently everytime a user submits a song a new artist gets created in my artists table even if the same one already exists and a SongArtistMap is created for that duplicated artist.
Any idea on how to tackle this issue? I feel like rails probably has some easy little trick to fix this already built in. Thanks!
Ok I got this figured out awhile ago and forgot to post. So here's how I fixed my problem. First of all I realized I didn't need to have a has_many through relationship.
What I really needed was a has_and_belongs_to_many relationship. I setup that up and made the table for it.
Then in my Artists model I added this
def self.find_or_create_by_name(name)
k = self.find_by_name(name)
if k.nil?
k = self.new(:name => name)
end
return k
end
And in my Song model I added this
before_save :get_artists
def get_artists
self.artists.map! do |artist|
Artist.find_or_create_by_name(artist.name)
end
end
And that did exactly what I wanted.
I use a method in the model of the table the other two go through, that is called with before_create. This can probably be made much neater and faster though.
before_create :ensure_only_one_instance_of_a_user_in_a_group
private
def ensure_only_one_instance_of_a_user_in_a_group
user = User.find_by_id(self.user_id)
unless user.groups.empty?
user.groups.each do |g|
if g.id == self.group_id
return false
end
end
end
return true
end
Try this:
class Song < ActiveRecord::Base
has_many :song_artist_maps
has_many :artists, :through => :song_artist_maps
accepts_nested_attributes_for :artists, :reject_if => :normalize_artist
def normalize_artist(artist)
return true if artist['name'].blank?
artist['id'] = Artist.find_or_create_by_name(artist['name']).id
false # This is needed
end
end
We are essentially tricking rails by over-loading the reject_if function(as we never return true).
You can further optimize this by doing case insensitive lookup ( not required if you are on MySQL)
artist['id'] = (
Artist.where("LOWER(name) = ? ", artist['name'].downcase).first ||
Artist.create(:name => artist['name'])
).id
I need some help with a rails development that I'm working on, using rails 3.
This app was given to me a few months ago just after it's inception and I have since become rather fond of Ruby.
I have a set of Projects that can have resources assigned through a teams table.
A team record has a start date and a end date(i.e. when a resource was assigned and de-assigned from the project).
If a user has been assigned and deassigned from a project and at a later date they are to be assigned back onto the project,
instead of over writting the end date, I want to create a new entry in the Teams table, to be able to keep a track of the dates that a resource was assigned to a certain project.
So my question is, is it possible to have multiple entries in a :has_many through association?
Here's my associations:
class Resource < ActiveRecord::Base
has_many :teams
has_many :projects, :through => :teams
end
class Project < ActiveRecord::Base
has_many :teams
has_many :resources, :through => :teams
end
class Team < ActiveRecord::Base
belongs_to :project
belongs_to :resource
end
I also have the following function in Project.rb:
after_save :update_team_and_job
private
def update_team_and_job
# self.member_ids is the selected resource ids for a project
if self.member_ids.blank?
self.teams.each do |team|
unless team.deassociated
team.deassociated = Week.current.id + 1
team.save
end
end
else
self.teams.each do |team|
#assigning/re-assigning a resource
if self.member_ids.include?(team.resource_id.to_s)
if team.deassociated != nil
team.deassociated = nil
team.save
end
else
#de-assigning a resource
if team.deassociated == nil
team.deassociated = Week.current.id + 1
team.save
end
end
end
y = self.member_ids - self.resource_ids
self.resource_ids = self.resource_ids.concat(y)
self.member_ids = nil
end
end
end
Sure, you can have multiple associations. has_many takes a :uniq option, which you can set to false, and as the documentation notes, it is particularly useful for :through rel'ns.
Your code is finding an existing team and setting deassociated though, rather than adding a new Team (which would be better named TeamMembership I think)
I think you want to just do something like this:
add an assoc for active memberships (but in this one use uniq: => true:
has_many :teams
has_many :resources, :through => :teams, :uniq => false
has_many :active_resources,
:through => :teams,
:class_name => 'Resource',
:conditions => {:deassociated => nil},
:uniq => true
when adding, add to the active_resources if it doesn't exist, and "deassociate" any teams that have been removed:
member_ids.each do |id|
resource = Resource.find(id) #you'll probably want to optimize with an include or pre-fetch
active_resources << resource # let :uniq => true handle uniquing for us
end
teams.each do |team|
team.deassociate! unless member_ids.include?(team.resource.id) # encapsulate whatever the deassociate logic is into a method
end
much less code, and much more idiomatic. Also the code now more explicitly reflects the business modelling
caveat: i did not write a test app for this, code may be missing a detail or two
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.