Next Instance Method - ruby-on-rails

In my blog app, I can call #article.comments.last. How do I create a next_comment method, one that always picks the next comment in line?
Update:
Also, how do I do the reverse, define a previous_comment method?
Update answer below.
Previous record:
class Comment < ActiveRecord::Base
def self.previous(comment, key = :id)
self.where("#{key} < ?", commend.send(key)).first
end
end

In order to define a "next", you must declare a sorting rule. There is no "next" without an order.
The order can be as simple as by primary key, or another field (e.g. a name). The following method should support both cases, and default to id"
class Comment < ActiveRecord::Base
def self.next(comment, key = :id)
self.where("#{key} > ?", commend.send(key)).first
end
end
You should call it on the chain and passing the comment instance, so that it can use the same relation you used to load the original comment
scope = #article.comments
last = scope.last
next = scope.next(last)
Another (maybe simpler) solution is to simply load two objects
current, next = #article.comments.take(2)
You can also make it a method
class Comment < ActiveRecord::Base
def self.first_and_next
# use all to create a scope in case you call
# the method directly on the Comment class
all.take(2)
end
end
current, next = #article.comments.first_and_next(2)

Given that you have a pagination gem, like will_paginate, this will work
# Article model
def next_comment
#page ||= 0
#page += 1
comments.page(#page).per(1).first
end
Or if you don't want to store the state
# Comment model
def next_comment
article.comments.where("id > ?", id).first
end

Dirty solution:
class Comment < ActiveRecord::Base
def next_comment
article.comments.where('id > ?', id).first
end
end

Related

Recommended practice for passing current user to model

Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append

Set activerecord model defaults before mass assignment initialize

I need defaults (from a remote service) in ModelA to be set before the object is passed to the view from ModelAController#new. I have used after_initialize for this. However, in #create I have a problem. If I use model_b.create_model_a(some_attributes), the attributes are passed in during initialization and then overwritten by the after_initialize call:
class ModelA < ActiveRecord::Base
after_initialize :set_defaults, if: :new_record?
def set_defaults
self.c = "default"
#actually a remote call, not something can be set as database default
end
end
class ModelB < ActiveRecord::Base
belongs_to :model_a
end
class ModelAController < ApplicationController
#ModelA is nested under ModelB in routes.rb
#GET /model_bs/:model_b_id/model_as/new
def new
model_b = ModelB.find(params[:model_b_id])
#no problem
respond_with model_b.build_model_a
end
#POST /model_bs/:model_b_id/model_as
def create
model_b = ModelB.find(params[:id])
#problem:
respond_with model_b.create_model_a({c: "not default"})
#at this point the model_a instance still has attribute c set to "default"
end
...
end
I could separate the create steps:
model_b = ModelB.find(params[:id])
model_a = model_b.build_model_a #should fire after_initialize
model_a.update_attributes({c: "not default"}) #overwrite default c value
But I feel like this makes the lifecycle of ModelA a bit of a trap for other programmers. This looks like an obvious candidate for refactoring the last two lines into one, but that would create this problem again. Is there a neater solution?
Make a conditional assignment:
def set_defaults
self.c ||= "default"
end
Alternatively instead of after_initialize hook set a default in attribute reader. That way you only set the default when you actually need attribute value, so it saves you a remote call if you don't need it:
def c
super || self.c = 'default'
end

What is the best practice for model filtering in Rails?

I have a use case where I want to be able to filter a collection in the index action of a controller
1) scoped by the current_user models AND if submitted
2) params[:ids]
I use to delegate the filtering to the model with scope or class methods:
class Zombie < ActiveRecord::Base
has_many :weapons
def self.filter_by_weapons(weapons)
where(weapons: weapons)
end
end
And in the controller
def index
#zombies = Zombie.filter_by_weapons(current_user.weapons)
end
it works well. However if I want my application to be able to filter the zombiesbased on a list of ids, I will do:
def index
#zombies = Zombie.filter_by_weapons(current_user.weapons)
if params[:weapons_ids]
#zombies = #zombies.filter_by_weapons(Weapon.where(id: params[:weapon_ids]))
end
end
However this breaks the rule that only one model should be called in the controller action.
I tried to scope by ids instead of model instances but the problem is reverse and I have to map the ids of current_user.weapons
So, what is the best way to do that ?
You can refactor your method to the following:
def self.filter_by_weapons(weapons)
weapon_ids = weapons.first.kind_of?(Weapon) ? weapons.map(&:id) : weapons
where(weapons: { id: weapon_ids.presence || -1 })
end
And use it like this:
def index
#zombies = Zombie.filter_by_weapons(current_user.weapons)
#zombies = #zombies.filter_by_weapons(params[:weapon_ids]) if params[:weapons_ids].present?
end

rails callback before 'new' of a model?

I have a model base_table, and I have a extended_table which has extra properties to further extend my base_table. (I would have different extended_tables, to add different properties to my base_table, but that's non-related to the question I'm asking here).
The model definition for my base_table is like:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def self.included(base)
base.belongs_to :base_table, autosave:true, dependent: :destroy
# do something more when this module is included
end
end
end
And the model definition for my extended_table is like:
class TennisQuestionaire < ActiveRecord::Base
include BaseTable::BaseTableInclude
end
Now I what I want is the code below:
params = {base_table: {name:"Songyy",age:19},tennis_ball_num:3}
t = TennisQuestionaire.new(params)
When I created my t, I want the base_table to be instantiated as well.
One fix I can come up with, is to parse the params to create the base_table object, before TennisQuestionaire.new was called upon the params. It's something like having a "before_new" filter here. But I cannot find such kind of filter when I was reading the documentation.
Additionally, I think another way is to override the 'new' method. But this is not so clean.
NOTE: There's one method called accepts_nested_attributes_for, seems to do what I want, but it doesn't work upon a belongs_to relation.
Any suggestions would be appreciated. Thanks :)
After some trails&error, the solution is something like this:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def initialize(*args,&block)
handle_res = handle_param_args(args) { |params| params[:base_table] = BaseTable.new(params[:base_table]) }
super(*args,&block)
end
private
def handle_param_args(args)
return unless block_given?
if args.length > 0
params = args[0]
if (params.is_a? Hash) and params[:base_table].is_a? Hash
yield params
end
end
end
end
end

Ruby attr_accessor setter does not get called during update_attributes on child object

I'm trying to add functionality to a project done in ruby, I'm unfamiliar with Ruby, but have managed to create a project review page that allows updates on the project task codes for a given monthly review.
My issue is that the client (my brother) has asked me to allow him to edit the scheduled hours for the next few months on "this" month's project review.
I've been able to show those values that don't belong to the child on the page, and I can get the usual child elements to update, but I cannot get the update to happen on the value I'm borrowing from the future month(s).
To get the page to show and not fail on update, I've added that attr_accessor (otherwise I had failures on update because the value didn't exist in the model.
an excerpt from my code is shown below. There are no errors, but there are also no updates to the variable reflected in the attr_accessor, I have tried testing with changes to the usual elements in the child object, those will get updated, but still no call to the attr_accessor "setter".
suggestions?
Thanks much,
Camille..
class Projectreview < ActiveRecord::Base
has_many :reviewcostelements
accepts_nested_attributes_for :reviewcostelements
end
class ProjectreviewsController < ApplicationController
def update
#projectreview = Projectreview.find(params[:id])
respond_to do |format|
if #projectreview.update_attributes(params[:projectreview])
format.html { redirect_to(#projectreview) }
end
end
end
end
class Reviewcostelement < ActiveRecord::Base
belongs_to :projectreview
attr_accessor :monthahead_hours1
def monthahead_hours1(newvalue) #this is the setter
#why do I never see this log message ??
logger.info('SETTER 1')
set_monthahead_hours(1, newvalue)
end
def monthahead_hours1 #this is the getter
get_monthahead_hours(1)
end
def update_attributes(attributes)
#never gets called!!!
logger.info('update_attributes values rce')
super(attributes)
end
def get_monthahead_hours(p_monthsahead)
#this works and returns the next month's scheduled_hours_this_month value
rce = Reviewcostelement.first(:joins => :projectreview,
:conditions => ["projectreviews.project_id = ?
and reviewcostelements.projecttaskcode_id =?
and projectreviews.month_start_at = ?", projectreview.project_id ,
projecttaskcode_id ,
projectreview.month_start_at.months_since(p_monthsahead)])
if rce
return rce.scheduled_hours_this_month
else
return 0
end
end
def set_monthahead_hours(p_monthsahead, newvalue)
#this never seems to get called
logger.info("set the month ahead hours")
rce = Reviewcostelement.first(:joins => :projectreview,
:conditions => ["projectreviews.project_id = ?
and reviewcostelements.projecttaskcode_id =?
and projectreviews.month_start_at = ?",
projectreview.project_id ,
projecttaskcode_id ,
projectreview.month_start_at.months_since(p_monthsahead)])
if rce
rce.scheduled_hours_this_month = newvalue
rce.save
end
end
end
The setter of the accessor looks like this:
def monthahead_hours1=(newvalue)
...
end
Note the equals (=) symbol.

Resources