linked attributes between classes (ruby on rails) - ruby-on-rails

Good morning, I'm having an issue in my rails app when trying to link attributes between two classes. Let me explain better:
I have a Systemclass, which belongs_to my Area class (one area has_many systems). Both of them have an attribute called price. The price of an area must be the sum of the prices of all the systems it has.
Is there any way to make this relation without having to update the area's price every time I change one of it system's price? (I do something like #system.area.price = #system.area.price + #system.price)

If you're ok with handling this in the database, the sum calculation will do it for you: http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html
class Area < ActiveRecord::Base
has_many :systems
def price
systems.sum('price')
end
end
You could remove the Area's price field altogether.

Unless you want to go down the dark, dangerous road of introducing triggers into your database, short answer is not really.
The long answer is you need to have after_save handlers that do this adjustment for you. Always be sure that the way you're applying this does an increment and not a reset or you will have race conditions.
In your short example it's not clear what you're asking for. Unless Area has a base_price then you'll keep adding on the System price indefinitely. You'll need to work out the exact logic here, but an example of your requirements are:
class System < ActiveRecord::Base
belongs_to :area
after_save :update_area_price
protected
def update_area_price
self.area and self.area.increment!(:price, self.price)
end
end

Related

Rails: How do you show a (number) difference temporarily?

I am trying to implement a reputation system in my app (similar to stackoverflow). I want to be able to show any recent additions or subtractions to the reputation score next to it. What is the best way to go about such implementation?
For e.g Reputation Score: 150 +10 or Reputation Score: 150 -20
The only method I can think of right now is create another column/field called temporary_reputation and keep the most recent addition/subtraction in there. And then maybe create a cron to clear that column every 20-30minutes. Maybe I can do something with the cache?
I think what you suggest will not solve your problem.
I would prefer implementing an extra model eg: ScoreChange which will have
timestamp
value
and the relations that are needed.
That way for each item with a reputation score you can have an aggregated score field and then show all the changes you want.
eg:
class Item < ActiveRecord::Base
has_many :score_changes
def last_changes(n)
score_changes.last_changes_sum(n)
end
def score_with_last_change(n = 3)
[score, last_changes(n)]
end
and
class ScoreChange < ActiveRecord::Base
belongs_to :item
def self.last_changes_sum(n)
order('timestamp desc').limit(n).pluck(:value).inject(&:+)
end
end

How to populate rails table with data from other tables?

I'm a bit of a noob programmer so apologies if the question isn't clear enough.
I'm trying to create a basic rails app where I have 3 different tables: usages(month, usage), prices(month, price) and spends(month, spend).
I'm trying to get it so that spend = usages.usage * prices.price. I've put the following code into my Spend model:
class Spend < ActiveRecord::Base
c = Usage.all.count
i = 1
while i <= c
u = Usage.find(i)
p = Price.find(i)
Spend.create(month:u.month, spend:u.usage*p.price)
i += 1
end
end
This works great initially, but as soon as I start adding and removing usages and prices, their id's change so it isn't as clear cut. How can I do this in a much better way?
Thanks,
Kev
In this case, I would lean against making a separate Spend model, since all it does is calculate data that is already present in the database. Unless you have severe caching requirements (and I doubt it in your case), you can use simple instance methods to retrieve the data you want.
First figure out how your Usage and Price models are related. Since you seem to be associating them by id, it appears to be a one-to-one relationship (correct me if I'm wrong on this). However, associating by assuming they have the same primary key is a dangerous approach - rather have one model point to the other using a foreign key. We'll pick the Price model to hold a primary key for Usage, but the reverse can also work. You'll need to add a column using a migration like this:
def change
add_column :prices, :usage_id, :integer
end
Your models should then look like this:
class Usage < ActiveRecord::Base
has_one :price
def spend
usage * price.price
end
end
class Price < ActiveRecord::Base
belongs_to :usage
end
And you can find your spend value for an individual usage item like this:
usage = Usage.find(some_id)
puts usage.spend
Or you can get multiple 'spends' like this:
Usage.include(:price).each do |usage|
puts usage.spend
end
I've left out any reference to month, as I'm not sure how you are using it or if it's needed at all for calculating spend.
Have a look at the Active Record association guide: http://guides.rubyonrails.org/association_basics.html

Rails: set a value upon submit based on nested models values

Suppose in my rails application I have a model Entry, which has a nested model Measures, such that each entry has_many measures (and measures belongs_to entry).
Each measure has its own incentive. Is it possible that Entry has an integer also named incentive, whose value is equal to the sum of all of its measures? How do you achieve this?
To me, it seems like this kind of becomes a two part question:
How to make a models field, upon submission, be defined based on another fields value? Then.. How to make a value, upon submission, be defined based on its nested models values?
Try implement a callback using after_update in the model of the nested attributes, which updates its parent:
class Measure < ActiveRecord::Base
after_update :calculate_measure_sum
...
private
def calculate_measure_sum
# calculate sum
self.entry.save
end
end
You might need to use the same method on the after_create callback as well.
EDIT:
After having read about touch in another question, I'd like to update my approach:
class Entry < ActiveRecord::Base
has_many :measures
after_touch :calculate_measure_sum
...
private
def calculate_measure_sum
# calculate sum
self.entry.save
end
end
class Measure < ActiveRecord::Base
belongs_to :entry, touch: true
...
end
What happens here, is that everytime a Measure is created or edited, it informs its Entry that it is updated by calling its touch method. In the entry, we may use the callback after_touch in order to recalculate the sum of the measures. Note that the after_touch-callback is called on creation, deletion and modification of the measures.
Compared to my previous approach, this approach puts the responsability on the Entry-objects, which is favourable from a design point-of-view.

Rails rolling up values in a model

I'm quite new to Rails, so forgive me if I'm going about this in the wrong way.
I have a model, transaction, which has an amount associated with it. However, since the amount is calculated as the sum of the amounts on a child model (transactor), I didn't want to create redundancy by actually having an amount field on my transaction table. I'd like to be able to get and set the amount at the transaction level (a value set at the transaction would be divided evenly among the transactors).
My question is this: Is composed_of the appropriate implementation for this situation?
No, I don't think composed_of is what you want. The read part is actually pretty easy:
class Transaction < ActiveRecord::Base
has_many :transactors
...
def amount
self.transactors.sum(:amount)
end
...
end
The write part (evenly dividing up among child models) is pretty unusual though and a bit more complicated, I suppose you'd do something like this:
class Transaction < ActiveRecord::Base
has_many :transactors
...
def amount=(value)
self.transactors.each do |transactor|
transactor.update_attributes(:amount => value.to_f / self.transactors.count)
end
end
...
end

performance/ruby/rails/db question

I'm creating an online bookmaker odds comparison site for soccer and I'm wondering how to calculate the best odds in Ruby/Rails.
I have two models: fixture and odds
Fixture has home and away teams, and odds model has bookmaker ID, home odds, draw odds and away odds.
I have selections which just stores the selected fixtures/teams in the DB.
I'm thinking of doing it this way where I create an multi-dimensional array of the different bookmakers and then add the fixture_id and 1,2 or 3 for home/draw/away and then use that as the key to add the odds
Something like odds[bookmaker][fixture][1/2/3] = price then add up the odds = count(odds[bookmaker][fixture][1/2/3])?
Is there an easier way? Maybe do it in the DB?
Without taking performance into account - it's probably not an issue and anyway, we shouldn't optimise for performance until we know we have a problem - I'd say you might introduce a Bookmaker model (if only to store the name) and start making use of ActiveRecord associations. I'd also consider splitting Odds into the three individual result types, which could be more flexible, especially if you want to add more bets later. You might get something like:
class Bookmaker < ActiveRecord::Base
has_many :odds
end
class Odd < ActiveRecord::Base # good name? Price is almost as common and less likely to be misinterpreted
belongs_to :fixture
belongs_to :bookmaker
# let's assume we use result type = 1/2/3 or maybe :home/:draw/:away
end
class Fixture < ActiveRecord::Base
has_many :odds
end
What you look to be trying to do is calculate the best price for each result across all bookies making a price on that fixture, or the "overround". If it's less than 100% then a potential arbitrage exists.
class Odd
named_scope :for_result, lambda { |res_tp| {:conditions => ['type = ?', res_tp]}}
end
class Fixture
def best_price(res_type)
# assumes you have odds stored as a percentage
odds.for_result(res_type).minimum(:pctage)
end
def overround
[:home, :away, :draw].inject(0.0){|sum, res_tp| sum + best_price(res_tp)}
end
end
I'm sure the above doesn't exactly fit your data, but it might give an idea of how you might go about it.

Resources