How can I update and save one model from another in Rails? - ruby-on-rails

In my program, I have a model, Calorie, that takes what a person ate and gives them a point total. After that point value is calculated for each day's nutritional information, I want to update the 'points' variable in the User model.
The code I have in the Calorie model is
before_save :calculate_points
def calculate_points
# snipped calculations
User.where(user_id).first.point_calculation
end
In the User model, I have
def point_calculation
self.points = Calorie.where(user_id: id).sum(:points)
end
I've tested the point_calculation model by creating a callback before_save, and it works fine there. But it makes a lot more sense to update after each new Calorie entry, instead of a user updating their settings. Any advice? What am I missing?
Thanks for your help.

I'm assuming your Calorie model has a has_one relationship with the User, and User has_many Calories.
In Calorie model:
after_save :update_user_points
def update_user_points
self.user.update_calorie_points!
end
In User model:
def update_calorie_points!
self.update_column(:points, self.calories.sum(:points))
end

Related

Increase a counter in parent when a child is added in has_many association

I have a schema where User has many Student with a user_id field.
In the User table, I am saving a counter next_student_number with default value as 1, and there is a roll_number column in Student.
In the Student class, I have before_create :generate_roll_number callback which sets the student's roll number to next_student_number and increments the value in User class.
Some thing like this :-
def generate_roll_number
self.roll_number = user.next_roll_number
user.increment! :next_roll_number
end
I feel there will be an issue when two records are trying to save at the same time here. Either they'll have a clash, or some roll numbers will be skipped.
What is the best way to implement this?
I think this should work fine:
Controller
def create
Student.transaction do
Student.create(user_id: current_user, ...)
end
end
Student Model
before_create :generate_roll_number
def generate_roll_number
user.increment! :next_roll_number
# Fires query like
# UPDATE users SET next_roll_number=2, WHERE id=xxx
self.roll_number = user.next_roll_number
end
Now, if any error happens while Student record is saved, the transaction will also rollback the incremented next_roll_number value in User table

Rails use Boolean similar to counter_cache?

A Miniatures model has many Collections. Users can have and vote for the best Collection version of a Miniature. The votes are in a model called Imagevotes which update a counter_cache attribute in the Collections model.
What I want to do is flag Collections which are ranked first for a Miniature as GOLD, then rank the 2nd, 3rd and 4th as SILVER. I realise I can do this on the Miniature model by selecting the #miniature.collection.first, but I would like to be able to store that like you would store the vote-count in a counter_cache so that I could display the total number of GOLDS or SILVERS for any one user.
Is there a way that each model could have Boolean fields called GOLD and SILVER which would be updated as new votes are cast in the same way that a counter_cache is updated?
Any pointers and further reading much appreciated.
Update:
It occurs to me that this could also be done with a sort of second index column. A vote_position column if you will, that updated with a number from "1" for the record with the highest counter_cache number and ascended from there. Then I could use #miniature.collection.where(:vote_position => "1") or similar. Perhaps this is more ideal?
As it seems for me you just need to implement method in Miniature model:
def set_gold_and_silver
top_collections = self.collections.order("imagevotes_count desc").limit(4)
gold = top_collections.shift
gold.update_attribute :is_gold, true if gold
top_collections.each {|s| s.update_attribute :is_silver, true}
end
after that you can add it to after_create filter of Imagevotes model:
class Imagevotes < ActiveRecord::Base
after_create :set_gold_and_silver
def set_gold_and_silver
self.miniature.set_gold_and_silver
end
end

Rails default data

Every time I create a new company record in rails, I need to add some default (blank) contact records at that company. Front Desk, Receiving, HR, IT and so on...they won't have any data in them besides the name, just a placeholder for the user to fill in later.
So, my company model has_many contacts, and contacts belong_to company. The contact records are static and the same for every new company that gets added, but I need to pre-populate the contacts table with data, so my users don't have to.
I've read a lot about seeding the database, but I won't be able to use the terminal every time a user dynamically creates a company, and it needs to be dynamically tied to that company, the records are not agnostic. Seeding doesn't seem to be the right thing. How should this be done?
you should use a before_save filter, which checks if an attribute is empty, and otherwise set it to the default.
Using a before_save will guard against deletions later on.
But be careful only to do this for fields which will never be empty.
class Company < ActiveRecord::Base
has_many :contacts
before_save :add_defaults
def add_defaults
contacts ||= Contact.default_list # this only sets it if it's nil
# you can implement Contact#default_list as a method, or as a scope in the contacts model
end
end
What about after_create callback in Company Model?
Smth like this:
class Company < ActiveRecord::Base
has_many :contacts
after_create :add_contacts
def add_contacts
contacts.create(name: "Some name", phone: "...", ....)
end
end
Although it notionally exists for generating test data, the FactoryGirl gem is very useful for this purpose. Use it in conjunction with the after_save approach mentioned here, and you'll have a nice place to centrally define your blank records.

Rails 3.1 How To Use Parent Attribute When Saving A Child?

Working on a Rails 3.1 project using Devise.
My User model has_many :courses and a Course belongs_to :user.
The User has a boolean attribute miles that the Course model needs to check before saving. I'm trying to use the before_save callback in the Course model like this:
# check if user preference is miles, if so convert entered value to kilometers. All
# distances are stored as kilometers and converted to miles if necessary
before_save :convert_distance
def convert_distance
if course.user.miles
course.distance = course.distance * 1.6
end
end
Obviously I can't use course.user.miles because the new course hasn't had the user_id set yet.
So how can I check the value of the user.miles attribute? Is this something I need to do in the controller?
Actually, it depends on how you're creating the Course for the User. For example, this works in a CoursesController that also knows who the user is:
course = user.courses.build(params[:course])
if course.save
# ...
end
Using #build on an association assigns the appropriate parent id, so the user will be visible from the course.
You could use a after_commit callback in this case.

virtual attribute in a query

User model has all_scores attribute and i created the method below
models/user.rb
def score
ActiveSupport::JSON.decode(self.all_scores)["local"]
end
What i'm trying to do this using this virtual attribute score to filter users. For example:
I need the users whose score is over 50. The problem is i can't use virtual attribute as a regular attribute in a query.
User.where("score > 50") # i can't do this.
Any help will be appreciated.
Well, the "easiest" solution would probably be User.all.select{|user| user.score > 50}. Obviously that's very inefficient, pulling every User record out of the database.
If you want to do a query involving the score, why don't you add a score column to the users table? You could update it whenever all_scores is changed.
class User < AR::Base
before_save :set_score, :if => :all_scores_changed?
protected
def set_score
self.score = ActiveSupport::JSON.decode(self.all_scores)["local"] rescue nil
end
end
This will also avoid constantly deserializing JSON data whenever you access user#score.

Resources