How can I improve this Rails code? - ruby-on-rails

I'm writing a little browser game as a project to learn RoR with and I'm quite new to it.
This is a little method that's called regularly by a cronjob.
I'm guessing there should be some way of adding elements to the potions array and then doing a bulk save at the end, I'm also not liking hitting the db each time in the loop to get the number of items for the market again.
def self.restock_energy_potions
market = find_or_create_market
potions = EnergyPotion.find_all_by_user_id(market.id)
while (potions.size < 5)
potion = EnergyPotion.new(:user_id => market.id)
potion.save
potions = EnergyPotion.find_all_by_user_id(market.id)
end
end

I'm not sure I'm understanding your question. Are you looking for something like this?
def self.restock_energy_potions
market = find_or_create_market
potions = EnergyPotion.find_all_by_user_id(market.id)
(potions.size...5).each {EnergyPotion.new(:user_id => market.id).save }
end
end
Note the triple dots in the range; you don't want to create a potion if there are already 5.
Also, if your potions were linked (e.g. by has_many) you could create them through the market.potions property (I'm guessing here, about the relationship between users and markets--details depend on how your models are set up) and save them all at once. I don't think the data base savings would be significant though.

Assuming that your market/user has_many potions, you can do this:
def self.restock_energy_potions
market = find_or_create_market
(market.potions.size..5).each {market.potions.create(:user_id => market.id)}
end

a) use associations:
class Market < AR::Base
# * note that if you are not dealing with a legacy schema, you should
# rename user_id to market_id and remove the foreigh_key assignment.
# * dependent => :destroy is important or you'll have orphaned records
# in your database if you ever decide to delete some market
has_many :energy_potions, :foreign_key => :user_id, :dependent => :destroy
end
class EnergyPotion < AR::Base
belongs_to :market, :foreign_key => :user_id
end
b) no need to reload the association after adding each one. also move the functionality
into the model:
find_or_create_market.restock
class Market
def restock
# * note 4, not 5 here. it starts with 0
(market.energy_potions.size..4).each {market.energy_potions.create!}
end
end
c) also note create! and not create.
you should detect errors.
error handling depends on the application.
in your case since you run it from cron you can do few things
* send email with alert
* catch exceptions and log them, (exception_notifier plugin, or hoptoad hosted service)
* print to stderror and configuring cron to send errors to some email.
def self.restock_potions
market = find_or_create
market.restock
rescue ActiveRecord::RecordInvalid
...
rescue
...
end

Related

Creating a Rails change log

I am pretty new to rails (and development) and have a requirement to create a change log. Let's say you have an employees table. On that table you have an employee reference number, a first name, and a last name. When either the first name or last name changes, I need to log it to a table somewhere for later reporting. I only need to log the change, so if employee ref 1 changes from Bill to Bob, then I need to put the reference number and first name into a table. The change table can have all the columns that mnight change, but most only be populated with the reference number and the changed field. I don't need the previous value either, just the new one. hope that makes sense.
Looked at gems such as paper trail, but they seem very complicated for what I need. I don't ever need to manipulate the model or move versions etc, I just need to track which fields have changed, when, and by whom.
I'd appreciate your recommendations.
If you insist on building your own changelog, based on your requirements you can do so using a few callbacks. First create your log table:
def up
create_table :employee_change_logs do |t|
t.references :employee
# as per your spec - copy all column definitions from your employees table
end
end
In your Employee model:
class Employee < ActiveRecord::Base
has_many :employee_change_logs
before_update :capture_changed_columns
after_update :log_changed_columns
# capture the changes before the update occurs
def capture_changed_columns
#changed_columns = changed
end
def log_changed_columns
return if #changed_columns.empty?
log_entry = employee_change_logs.build
#changed_columns.each{|c| log_entry.send(:"#{c}=", self.send(c))}
log_entry.save!
end
end
I recommend the gem vestal_versions.
To version an ActiveRecord model, simply add versioned to your class like so:
class User < ActiveRecord::Base
versioned
validates_presence_of :first_name, :last_name
def name
"#{first_name} #{last_name}"
end
end
And use like this:
#user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler")
#user.version # => 2
#user.versions.last.user # => "Tyler"
The first thing we did was put an around filter in the application controller. This was how I get the current_employee into the employee model, which was the challenge, especially for a newbie like me!
around_filter :set_employee_for_log, :if => Proc.new { #current_account &&
#current_account.log_employee_changes? && #current_employee }
def set_employee_for_log
Thread.current[:current_employee] = #current_employee.id
begin
yield
ensure
Thread.current[:current_employee ] = nil
end
end
end
Next, in the employee model I defined which fields I was interested in monitoring
CHECK_FIELDS = ['first_name', 'last_name', 'middle_name']
then I added some hooks to actually capture the changes IF logging is enabled at the account level
before_update :capture_changed_columns
after_update :log_changed_columns, :if => Proc.new { self.account.log_employee_changes? }
def capture_changed_columns
#changed_columns = changed
#changes = changes
end
def log_changed_columns
e = EmployeeChangeLog.new
Employee::CHECK_FIELDS.each do |field|
if self.send("#{field}_changed?")
e.send("#{field}=", self.send(field))
end
end
if e.changed?
e.update_attribute(:account_id, self.account.id)
e.update_attribute(:employee_id, self.id)
e.update_attribute(:employee_ref, self.employee_ref)
e.update_attribute(:user_id, Thread.current[:current_employee])
e.save
else return
end
end
And that;s it. If the account enables it, the app keeps an eye on specific fields and then all changes to those fields are logged to a table, creating an simple audit trail.

Validate that the sum of the part is equal to the total with parent/children records

I have 2 models:
Invoice has_many :lines
Line belongs_to :invoice
I want to ensure that the sum of the Line for a given Invoice match the total of the related Invoice.
I've tried this:
validate :total_amount
def total_amount
inv_id = self.invoice_id
target_amount = Invoice.find(inv_id).total
total_lines = Line.where(invoice_id: inv_id).sum(:line_value)
errors.add(:total, " should be lower or equal to the total amount of the invoice") if total_lines > target_amount
end
But
it doesn't work for new objects (just updates)
even for updates it systematically throws an error
I've also seen a question talking about AssociatedValidator, but I haven't been able to grasp how to use that :(
It's not clear what exactly you want to validate, since your example is different from what you were describing prior to that.
I think something like this should work, using a before_add callback:
class Invoice < AR::Base
has_many :lines, :before_add => :validate_total
def validate_total(invoice, line)
totals = invoice.lines.sum(:line_value)
if totals + line.line_value > invoice.total
invoice.errors.add(:total, " should be lower or equal to the total amount of the invoice")
return false # I think you can alternatively raise an exception here
end
...
I might be interpreting it wrong, but if total is a column in the invoices table, I suggest removing it. Instead, have it as a method and have the method add up the Line prices plus any adjustments. Otherwise, you have duplication in the database. And that way you won't need to validate anything anyway :)
On a more general note, adding validations on associated models in ActiveRecord is not working very well. In some cases it's almost impossible, in other - pretty hard to get right. I think you've seen that it goes wrong easily. I suggest avoiding it and trying to design your database so that you won't need to (having Invoice#total as a method in this case).
It took a little while to find an question/answer to a problem that cropped up using accepts_nested_attributes_for. But the answer just said It's hard, if not impossible!. accepts_nested_attributes_for is a somewhat complicated approach, but it works - unless you are trying to validate a model based on a calculation of the children model. I may have found a way to to overcome the calculation problem.
I'm working on a web based double entry accounting application that had the following basic models;
class Account < ApplicationRecord
has_many :splits
has_many :entries, through: :splits
end
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
validate :balanced?
end
class Split < ApplicationRecord
belongs_to :entry, inverse_of: :splits
belongs_to :account
validates_associated :account
validates_associated :entry
end
Entries(transactions) must have at least two Splits that the sum of the Amount attribute(or Debits/Credits) in the Splits must equal 0. I though the validate :balanced? would take care of it, but an apparent Javascript error allowed an unbalance entry. I've yet to track the bug down, but since the Entry was unbalanced, I could not update it since valid? does not work (returns false) on new Splits that I tried to add.
The ledger accepts_nested_attributes_for form has quit a bit of Javascript that is not supposed to allow an unbalanced transaction to be submitted. Balanced? did not set an error on create, but its there on update. My approach to fixing it is not used validations that don't work, but to rely on a method called in conjunction with #entry.update(entry_params):
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
# validate :balanced? # took this out since its after the fact, balanced? method can still be called
accepts_nested_attributes_for :splits,
:reject_if => proc { |att| att[:amount].to_i.zero? && att['account_id'].to_i.zero?},
allow_destroy: true
def valid_params?(params)
split_sum = 0
params_hash = params.to_h
params_hash[:splits_attributes].each{|k,s| split_sum += s[:amount].to_i if s[:_destroy].to_i.zero?}
unless split_sum.zero?
errors.add(:amount, "Unbalanced: debits, credits must balance")
return false
else
return true
end
end
end
end
# update action from Entry Controller
def update
respond_to do |format|
if #entry.valid_params?(entry_params) && #entry.update(entry_params)
format.html { redirect_to account_path(session[:current_acct]), notice: 'Entry was successfully updated.' }
format.json { render :show, status: :ok, location: #entry }
else
# ... errr
end
end
end
Again, this in nothing more than validating the params verses the model validation that does not work for this condition.
This may be about same as answer 2, but not using a callback, just calling in controller

Mongoid: Querying from two collections and sorting by date

I currently have the following controller method in a Rails app:
def index
#entries = []
#entries << QuickPost.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries << Infographic.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries.flatten!.sort!{ |a,b| b.created_at <=> a.created_at }
#entries = Kaminari.paginate_array(#entries).page(params[:page]).per(10)
end
I realise this is terribly inefficient so I'm looking for a better way to achieve the same goal but I'm new to MongoDB and wondering what the best solution would be.
Is there a way to make a sorted limit() query or a MapReduce function in MongoDB across two collections? I'm guessing there isn't but it would certainly save a lot of effort in this case!
I'm currently thinking I have two options:
Create a master 'StreamEntry' type model and have both Infographic and QuickPost inherit from that so that both data types are stored on the same collection. The issue with this is that I have existing data and I don't know how to move it from the old collections to the new.
Create a separate Stream/ActivityStream model using something like Streama (https://github.com/christospappas/streama). The issues I can see here is that it would require a fair bit of upfront work and due to privacy settings and editing/removal of items the stream would need to be rebuilt often.
Are there options I have overlooked? Am I over-engineering with the above options? What sort of best practices are there for this type of situation?
Any info would be greatly appreciated, I'm really liking MongoDB so far and want to avoid falling into pitfalls like this in the future. Thanks.
The inherit solution is fine, but when the inherited models are close.
For example :
class Post < BasePost
field :body, type: String
end
class QuickPost < BasePost
end
class BasePost
field :title, type: String
field :created_at, type: Time
end
But when the models grows, or are too different, your second solution is better.
class Activity
include Mongoid::Document
paginates_per 20
field :occurred_at, :type => Time, :default => nil
validates_presence_of :occurred_at
belongs_to :user
belongs_to :quick_post
belongs_to :infographic
default_scope desc(:occurred_at)
end
and for example :
class QuickPost
include Mongoid::Document
has_one :activity, :dependent => :destroy
end
The dependant destroy make the activity destroyed when the QuickPost is destroyed. You can use has_many and adapt.
And to create the activities, you can create an observer :
class ActivityObserver < Mongoid::Observer
observe :quick_post, :infographic
def after_save(record)
if record.is_a? QuickPost
if record.new_record?
activity = record.build_activity
activity.user = record.user
# stuff when it is new
else
activity = record.activity
end
activity.occurred_at = record.occurred_at
# common stuff
activity.save
end
end
end

Rails counter_cache not updating correctly

Using Rails 3.1.3 and I'm trying to figure out why our counter caches aren't being updated correctly when changing the parent record id via update_attributes.
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
end
class Event < ActiveRecord::Base
has_many :exhibitor_registrations, :dependent => :destroy
end
describe ExhibitorRegistration do
it 'correctly maintains the counter cache on events' do
event = Factory(:event)
other_event = Factory(:event)
registration = Factory(:exhibitor_registration, :event => event)
event.reload
event.exhibitor_registrations_count.should == 1
registration.update_attributes(:event_id => other_event.id)
event.reload
event.exhibitor_registrations_count.should == 0
other_event.reload
other_event.exhibitor_registrations_count.should == 1
end
end
This spec fails indicating that the counter cache on event is not being decremented.
1) ExhibitorRegistration correctly maintains the counter cache on events
Failure/Error: event.exhibitor_registrations_count.should == 0
expected: 0
got: 1 (using ==)
Should I even expect this to work or do I need to manually track the changes and update the counter myself?
From the fine manual:
:counter_cache
Caches the number of belonging objects on the associate class through the use of increment_counter and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it’s destroyed.
There's no mention of updating the cache when an object is moved from one owner to another. Of course, the Rails documentation is often incomplete so we'll have to look at the source for confirmation. When you say :counter_cache => true, you trigger a call to the private add_counter_cache_callbacks method and add_counter_cache_callbacks does this:
Adds an after_create callback which calls increment_counter.
Adds an before_destroy callback which calls decrement_counter.
Calls attr_readonly to make the counter column readonly.
I don't think you're expecting too much, you're just expecting ActiveRecord to be more complete than it is.
All is not lost though, you can fill in the missing pieces yourself without too much effort. If you want to allow reparenting and have your counters updated, you can add a before_save callback to your ExhibitorRegistration that adjusts the counters itself, something like this (untested demo code):
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
before_save :fix_counter_cache, :if => ->(er) { !er.new_record? && er.event_id_changed? }
private
def fix_counter_cache
Event.decrement_counter(:exhibitor_registration_count, self.event_id_was)
Event.increment_counter(:exhibitor_registration_count, self.event_id)
end
end
If you were adventurous, you could patch something like that into ActiveRecord::Associations::Builder#add_counter_cache_callbacks and submit a patch. The behavior you're expecting is reasonable and I think it would make sense for ActiveRecord to support it.
If your counter has been corrupted or you've modified it directly by SQL, you can fix it.
Using:
ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)
Example 1: Re-compute the cached count on the post with id = 17.
Post.reset_counters(17, :comments)
Source
Example 2: Re-compute the cached count on all your articles.
Article.ids.each { |id| Article.reset_counters(id, :comments) }
I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.
Extend ActiveRecord::Base
Create a new file lib/fix_counters_update.rb with the following:
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil
changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.
Add to Initializers
Create a new file config/initializers/active_record_extensions.rb with the following:
require 'fix_update_counters'
Add to models
For each model you want the counter caches updated add the callback:
class Comment < ActiveRecord::Base
after_update :fix_updated_counters
....
end
A fix for this has been merged in to active record master
https://github.com/rails/rails/issues/9722
The counter_cache function is designed to work through the association name, not the underlying id column. In your test, instead of:
registration.update_attributes(:event_id => other_event.id)
try
registration.update_attributes(:event => other_event)
More information can be found here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Is validates_presence_of the preferred technique to require a has_many relationship

Basically: My model requires at least one instance of an associated model be present. Should I use validates_presence_of to assert this validation, or should I write some custom validation code?
Here are the essentials of my model:
class Event < ActiveRecord::Base
has_and_belongs_to_many :channels
validates_presence_of :channels, :message => "can't be empty"
end
(I assume things would be the same if I used has_many in place of has_and_belongs_to_many.)
Instead of the validates_presence_of line I could do this:
def validate
errors.add(:channels, "can't be empty") if channels.size < 1
end
I replaced the latter with the former in the Rails app I'm working on and am wondering if there might be any problems.
So to be more sure, I wrote the following rspec coverage, and both implementations respond the same:
describe Event do
before do
#net = Factory.create(:network)
#net_config = Factory.create(:network_config, :network => #net)
end
it "must have a channel" do
e = Factory.build(:event, :network => #net, :channels => [])
e.should have(1).error_on(:channels)
end
end
That is, if I remove the validation code, the above spec fails; if I put in either version of the validation code, the above spec passes.
So I might assume that my new implementation is ok. But I've read that validates_presence triggers a database load which, in turn, would wipe out any in-memory objects constructed from nested attributes. The proxy_target method, on the other hand, will return the in-memory objects without triggering a load. Some links on proxy_target: http://rubydoc.info/docs/rails/ActiveRecord/Associations/AssociationProxy http://withoutscope.com/2008/8/22/don-t-use-proxy_target-in-ar-association-extensions
In my particular case I'm not using ActiveRecord::Relation, but I wonder if I need to be cautious about this.

Resources