Memoize current user object before_create - ruby-on-rails

Using Rails 3.2. Let's say I have 10 new Photos to be uploaded, I need to associate my current_user.id to each new record. For some reasons, the photos_controller.rb is blank because it's a nested with another model Shop. Here is my code:
class Photo < ActiveRecord::Base
belongs_to :attachable, :polymorphic => true, :counter_cache => true
belongs_to :user, :counter_cache => true
before_create :current_user_id
before_create :associate_current_user
def current_user_id
#current_user_id ||= UserSession.find.user.id
end
private
def associate_current_user
self.user_id = #current_user_id
end
end
It is clear that if there are 10 new records to be created, I want the model to find the current_user once, and then take it from the cache (a memoization technique), but because I am using before_create, the current_user is queried 10 times instead of getting it from cache.
What can I do to cache the #current_user_id?
Thanks.

The answer is simple : you should not do anything related to the session in your model, it breaks the MVC pattern.
Instead, do this in your controller, so you only have to get current_user.id once, and assign it to your records.

This sort of logic belongs in the controller. Move your current_user_id method to your PhotosController (or to ApplicationController if you plan to use this logic in other controllers too). That way it will assign #current_user only once per upload action.
Be sure to make it private too.

Related

How to hide records, rather than delete them (soft delete from scratch)

Let's keep this simple. Let's say I have a User model and a Post model:
class User < ActiveRecord::Base
# id:integer name:string deleted:boolean
has_many :posts
end
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
end
Now, let's say an admin wants to "delete" (hide) a post. So basically he, through the system, sets a post's deleted attribute to 1. How should I now display this post in the view? Should I create a virtual attribute on the post like this:
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
def administrated_content
if !self.deleted
self.content
else
"This post has been removed"
end
end
end
While that would work, I want to implement the above in a large number of models, and I can't help feeling that copy+pasting the above comparative into all of my models could be DRYer. A lot dryer.
I also think putting a deleted column in every single deletable model in my app feels a bit cumbersome too. I feel I should have a 'state' table. What are your thoughts on this:
class State
#id:integer #deleted:boolean #deleted_by:integer
belongs_to :user
belongs_to :post
end
and then querying self.state.deleted in the comparator? Would this require a polymorphic table? I've only attempted polymorphic once and I couldn't get it to work. (it was on a pretty complex self-referential model, mind). And this still doesn't address the problem of having a very, very similar class method in my models to check if an instance is deleted or not before displaying content.
In the deleted_by attribute, I'm thinking of placing the admin's id who deleted it. But what about when an admin undelete a post? Maybe I should just have an edited_by id.
How do I set up a dependent: :destroy type relationship between the user and his posts? Because now I want to do this: dependent: :set_deleted_to_0 and I'm not sure how to do this.
Also, we don't simply want to set the post's deleted attributes to 1, because we actually want to change the message our administrated_content gives out. We now want it to say, This post has been removed because of its user has been deleted. I'm sure I could jump in and do something hacky, but I want to do it properly from the start.
I also try to avoid gems when I can because I feel I'm missing out on learning.
I usually use a field named deleted_at for this case:
class Post < ActiveRecord::Base
scope :not_deleted, lambda { where(deleted_at: nil) }
scope :deleted, lambda { where("#{self.table_name}.deleted_at IS NOT NULL") }
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
destroy
end
def deleted?
self.deleted_at.present?
end
# ...
Want to share this functionnality between multiple models?
=> Make an extension of it!
# lib/extensions/act_as_fake_deletable.rb
module ActAsFakeDeletable
# override the model actions
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
self.destroy
end
def undestroy # to "restore" the file
self.update(deleted_at: nil)
end
def undelete
self.undestroy
end
# define new scopes
def self.included(base)
base.class_eval do
scope :destroyed, where("#{self.table_name}.deleted_at IS NOT NULL")
scope :not_destroyed, where(deleted_at: nil)
scope :deleted, lambda { destroyed }
scope :not_deleted, lambda { not_destroyed }
end
end
end
class ActiveRecord::Base
def self.act_as_fake_deletable(options = {})
alias_method :destroy!, :destroy
alias_method :delete!, :delete
include ActAsFakeDeletable
options = { field_to_hide: :content, message_to_show_instead: "This content has been deleted" }.merge!(options)
define_method options[:field_to_hide].to_sym do
return options[:message_to_show_instead] if self.deleted_at.present?
self.read_attribute options[:field_to_hide].to_sym
end
end
end
Usage:
class Post < ActiveRecord::Base
act_as_fake_deletable
Overwriting the defaults:
class Book < ActiveRecord::Base
act_as_fake_deletable field_to_hide: :title, message_to_show_instead: "This book has been deleted man, sorry!"
Boom! Done.
Warning: This module overwrite the ActiveRecord's destroy and delete methods, which means you won't be able to destroy your record using those methods anymore. Instead of overwriting you could create a new method, named soft_destroy for example. So in your app (or console), you would use soft_destroy when relevant and use the destroy/delete methods when you really want to "hard destroy" the record.

Child not being created from Parent model?

I have a checkbox that if checked allows my child resource called Engineer to be created. I'm trying to create it through my model since that's where I can call the after_save method.
Here is my code:
models/user.rb
class User < ActiveRecord::Base
has_many :armies
has_many :engineers
end
models/army.rb
class Army < ActiveRecord::Base
has_many :engineers
attr_reader :siege
after_save :if_siege
private
def if_siege
if self.siege
Engineer.create!( :user_id => current_user.id, :army_id => self.id )
end
end
end
models/engineer.rb
class Engineer < ActiveRecord::Base
belongs_to :user
belongs_to :army
end
controllers/armies_controller.rb
def new
#army = Army.new
end
def create
#army = current_user.armies.build(params[:army])
if #army.save
redirect_to new_army_path
else
render :new
end
end
end
This gives me an error though for my if_siege method:
undefined local variable or method `current_user'
How can I fix this or is there another way to do this? Not sure if this should go in the controller or model but I only can wrap my head around putting this in the model.
Thanks.
Add belongs_to :user to the Army model
In Army#if_siege, update Engineer.create! as follows
Engineer.create!( :user_id => self.user.id, :army_id => self.id )
First, the current_user object won't exist within the context of the Model layer unless your authentication is doing something to make it available. This is usually a non Threadsafe approach though. Maybe for you this isn't the issue.
Current User Instantiation
Having said that, one way (perhaps not the ideal way) to address this is by creating an attr_accessor in the model on the object called Army. Then set the current_user to this in the Army new action in the controller where the current_user instance is available.
# in the Army model
attr_accessor :the_user
# in the Army Controller
#army = Army.new(:the_user => current_user.id)
You will also have to add a hidden field to store this value in your view to carry this through to the create action.
Just an observation, but I'm fairly sure in the "if_seige" method the self calls are redundant. self should already be scoped to the Army object in that method.

Rails after_initialize only on "new"

I have the following 2 models
class Sport < ActiveRecord::Base
has_many :charts, order: "sortWeight ASC"
has_one :product, :as => :productable
accepts_nested_attributes_for :product, :allow_destroy => true
end
class Product < ActiveRecord::Base
belongs_to :category
belongs_to :productable, :polymorphic => true
end
A sport can't exist without the product, so in my sports_controller.rb I had:
def new
#sport = Sport.new
#sport.product = Product.new
...
end
I tried to move the creation of the product to the sport model, using after_initialize:
after_initialize :create_product
def create_product
self.product = Product.new
end
I quickly learned that after_initialize is called whenever a model is instantiated (i.e., from a find call). So that wasn't the behavior I was looking for.
Whats the way I should be modeling the requirement that all sport have a product?
Thanks
Putting the logic in the controller could be the best answer as you stated, but you could get the after_initialize to work by doing the following:
after_initialize :add_product
def add_product
self.product ||= Product.new
end
That way, it only sets product if no product exists. It may not be worth the overhead and/or be less clear than having the logic in the controller.
Edit: Per Ryan's answer, performance-wise the following would likely be better:
after_initialize :add_product
def add_product
self.product ||= Product.new if self.new_record?
end
Surely after_initialize :add_product, if: :new_record? is the cleanest way here.
Keep the conditional out of the add_product function
If you do self.product ||= Product.new it will still search for a product every time you do a find because it needs to check to see if it is nil or not. As a result it will not do any eager loading. In order to do this only when a new record is created you could simply check if it is a new record before setting the product.
after_initialize :add_product
def add_product
self.product ||= Product.new if self.new_record?
end
I did some basic benchmarking and checking if self.new_record? doesn't seem to affect performance in any noticeable way.
Instead of using after_initialize, how about after_create?
after_create :create_product
def create_product
self.product = Product.new
save
end
Does that look like it would solve your issue?
It looks like you are very close. You should be able to do away with the after_initialize call altogether, but first I believe if your Sport model has a "has_one" relationship with :product as you've indicated, then your Product model should also "belong_to" sport. Add this to your Product model
belongs_to: :sport
Next step, you should now be able to instantiate a Sport model like so
#sport = #product.sport.create( ... )
This is based off the information from Association Basics from Ruby on Rails Guides, which you could have a read through if I am not exactly correct
after_initialize :add_product, unless: :persisted?
You should just override initialize method like
class Sport < ActiveRecord::Base
# ...
def initialize(attributes = {})
super
self.build_product
self.attributes = attributes
end
# ...
end
Initialize method is never called when record is loaded from database.
Notice that in the code above attributes are assigned after product is build.
In such setting attribute assignment can affect created product instance.

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

Rails accepts_nested_attributes_for callbacks

I have two models Ticket and TicketComment, the TicketComment is a child of Ticket.
ticket.rb
class Ticket < ActiveRecord::Base
has_many :ticket_comments, :dependent => :destroy, :order => 'created_at DESC'
# allow the ticket comments to be created from within a ticket form
accepts_nested_attributes_for :ticket_comments, :reject_if => proc { |attributes| attributes['comment'].blank? }
end
ticket_comment.rb
class TicketComment < ActiveRecord::Base
belongs_to :ticket
validates_presence_of :comment
end
What I want to do is mimic the functionality in Trac, where if a user makes a change to the ticket, and/or adds a comment, an email is sent to the people assigned to the ticket.
I want to use an after_update or after_save callback, so that I know the information was all saved before I send out emails.
How can I detect changes to the model (ticket.changes) as well as whether a new comment was created or not (ticket.comments) and send this update (x changes to y, user added comment 'text') in ONE email in a callback method?
you could use the ActiveRecord::Dirty module, which allows you to track unsaved changes.
E.g.
t1 = Ticket.first
t1.some_attribute = some_new_value
t1.changed? => true
t1.some_attribute_changed? => true
t1.some_attribute_was => old_value
So inside a before_update of before_create you should those (you can only check before the save!).
A very nice place to gather all these methods is in a Observer-class TicketObserver, so you can seperate your "observer"-code from your actual model.
E.g.
class TicketObserver < ActiveRecord::Observer
def before_update
.. do some checking here ..
end
end
to enable the observer-class, you need to add this in your environment.rb:
config.active_record.observers = :ticket_observer
This should get you started :)
What concerns the linked comments. If you do this:
new_comment = ticket.ticket_comments.build
new_comment.new_record? => true
ticket.comments.changed => true
So that would be exactly what you would need. Does that not work for you?
Note again: you need to check this before saving, of course :)
I imagine that you have to collect the data that has changed in a before_create or before_update, and in an after_update/create actually send the mail (because then you are sure it succeeded).
Apparently it still is not clear. I will make it a bit more explicit. I would recommend using the TicketObserver class. But if you want to use the callback, it would be like this:
class Ticked
before_save :check_state
after_save :send_mail_if_needed
def check_state
#logmsg=""
if ticket_comments.changed
# find the comment
ticket_comments.each do |c|
#logmsg << "comment changed" if c.changed?
#logmsg << "comment added" if c.new_record?
end
end
end
end
def send_mail_if_needed
if #logmsg.size > 0
..send mail..
end
end

Resources