ActiveResource models + Sweepers - ruby-on-rails

I've just started working with ActiveResource, and decided to cache a few bits of the model so I'm not hitting the api incessantly. Ok, fine.
I've looked into expiring caches, and decided to implement a sweeper (which I've not had to do yet). This isn't working.
AR model:
class Myresource < ActiveResource::Base
extend ActiveModel::Callbacks
define_model_callbacks :update
"stuff"
def current
Rails.cache.fetch("/key/#{self.id}", :expires_in => 5.minutes) do
Myresource.find(ID)
end
end
end
Sweeper:
class MyresourceSweeper < ActionController::Caching::Sweeper
observe Myresource
def after_update(myresource)
expire_cache_for_myresource
end
private
def expire_cache_for_myresource
Rails.cache.delete '/key/myresource.id'
end
end
Controller:
cache_sweeper :myresource_sweeper
So having worked with AR and caches and Sweepers only a bit, I can't figure out where to look after trying various combos of things. I can set and expire from the console for a resource, but within the app, the cache gets set, but nothing I've done is triggering a delete.
Suggestions?

The code that you posted has a typo. I kinda doubt this is your real problem, but for what it's worth, I think you meant to write your sweeper as follows:
class MyresourceSweeper < ActionController::Caching::Sweeper
observe Myresource
def after_update(myresource)
expire_cache_for_myresource(myresource)
end
private
def expire_cache_for_myresource(myresource)
Rails.cache.delete "/key/#{myresource.id}"
end
end
E.g. in the original code you posted, you weren't passing the resource to the expire_cache_for_my_resource method, and therefore were expiring the same static key over and over again.

Related

Error while generating sweeper in rails

Everyone is like this is sweeper. But can anyone please tell me how to create sweeper file for a particular controller. I copy pasted the code but its not working.
Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change. They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
class ImageSweeper < ActionController::Caching::Sweeper
observe Image
def after_save(record)
def after_save(image)
expire_cache(image)
end
def after_destroy(image)
expire_cache(image)
end
def expire_cache(image)
##expire_fragment #'image'
expire_cache(image)
end
end
The sweeper is assigned in the controllers that wish to have its job performed using the cache_sweeper class method:
class ImageController < ApplicationController
cache_sweeper :image_sweeper, :only => [ :edit, :destroy, :share ]
end
In the example above, three actions are responsible for expiring those caches
Above will only work if you have config.action_controller.perform_caching = true in development.rb
I got it working. Sweeper file will be in app/sweepers/controllername(without s)_sweeper.rb

Accessing current_user in a model in a Rails 3.2 app

I have a Rails 3.2 app. It is a publishing app where we kick off several Sidekiq jobs in response to changes in content. I was calling this from the controller but there's now getting to be multiple points of entry and are now duplicating logic in multiple controllers. The proper place for this to be is in a callback in the model. However, accessing current_user is frowned upon in the model but for things like logging changes or app events, it is critical.
So I have two questions (1) Is there something I'm missing regarding the argument about accessing current_user when you want to be logging changes across complex model structures? and (2) Is the proposed solution here an effective one with last update over 2 years ago in terms of thread-safety? I use a three Unicorn processes on Heroku. https://stackoverflow.com/a/2513456/152825
Edit 1
Thinking through this, wondering if I should just do something like this in my application.rb
class ArcCurrentUser
#current_user_id
def self.id
return #current_user_id
end
def self.id=id_val
#current_user_id=id_val
end
end
and then in my current_user method in application_controller, just update ArcCurrentUser.id to #current_user.id? I will only be using it for this logging functionality.
You're correct in that you can't access current_user from a model.
As for the answer you linked, I'm not entirely sure but I think it's not fully thread-safe. From the same question, I like this answer https://stackoverflow.com/a/12713768/4035338 more.
Say we have a controller with this action
...
def update
#my_object = MyModel.find(params[:id])
#my_object.current_user = current_user
#my_object.assign_attributes params[:my_model]
#my_object.save
end
...
and this model
class MyModel < ActiveRecord::Base
attr_accessor :current_user
before_save :log_who_did_it
private
def log_who_did_it
return unless current_user.present?
puts "It was #{current_user}!"
end
end
Or my favourite
...
def update
#my_object = MyModel.find(params[:id])
#my_object.update_and_log_user(params[:my_model], current_user)
end
...
class MyModel < ActiveRecord::Base
...
def update_and_log_user(params, user)
update_attributes(params)
puts "It was #{user}!" if user.present?
end
end

Friendly_id and Active Admin conflict - possibly because of revert_freindly_id

first time asking a question on stack overflow :)
I'm having a conflict between friendly_id and active admin (it's an assumption), as discussed in many threads here. I've looked at all those threads, but I'm not entirely sure they solve my problem. Sorry for the really long post!
I'm trying to create friendly links to products on my website. I've added the friendly_id gem and everything works fine in my dev and staging environments, but friendly links fail on production. Here is all my code:
Model:
class Product < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: :slugged
...
end
Controller:
class ProductsController < ApplicationController
before_filter :get_product, only: [:show]
...
private
def get_product
#product = Product.friendly.find(params[:id])
end
end
All my product records have a completed slug field at this point. I don't want to use slugs in my admin interface, so when I came across a solution here, I went ahead and modified it a bit to get active admin to work together with friendly_id.
config/initializers/active_admin.rb:
ActiveAdmin.setup do |config|
...
config.before_filter :revert_friendly_id
end
I've defined revert_friendly_id in the application controller:
class ApplicationController < ActionController::Base
...
protected
def revert_friendly_id
model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize
# Will throw a NameError if the class does not exist
Module.const_get model_name
eval(model_name).class_eval do
def to_param
id.to_s
end
end
rescue NameError
end
end
I've noticed that when I first deploy to production via capistrano, the friendly links work as expected. So my product links are accessible with: http://website.com/products/my-product-slug. But the minute I access the admin interface on production, the links immediately switch back to product ids instead: http://website.com/products/12345. I'm not entirely sure how to resolve this problem, though I understand why it might be happening, can someone help me please?
Here is how I solved the problem. Based on armstrjare's fix at this link.
I removed the revert_friendly_id function from my application controller and the before_filter from my config. Then just added the following to app/admin/product.rb:
ActiveAdmin.register Product do
around_filter do |controller, action|
Product.class_eval do
alias :__active_admin_to_param :to_param
def to_param() id.to_s end
end
begin
action.call
ensure
Product.class_eval do
alias :to_param :__active_admin_to_param
end
end
end
...
end
And everything worked as expected. Hope this helps someone else!
I found a very simple solution: Just overwrite the to_param in your model and check if it is called from active_admin.
app/models/product.rb:
class Product < ActiveRecord::Base
def to_param
if caller.to_s.include?"active_admin"
id && id.to_s
else
slug
end
end
end
When you set the to_param method, it will be set on the entire application. So you have to check if the requested controller is in the Admin namespace or not. Based on that you have to switch back the return of the to_param method.
You can redefine find_resource method in controller:
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
For the active_admin belongs_to association you can to use the finder: option (from https://github.com/activeadmin/inherited_resources/blob/master/lib/inherited_resources/belongs_to_helpers.rb#L17)
For example:
belongs_to :content, finder: :find_by_slug!

Owner-filtered model objects on Rails 3

I need to do some filtering on my ActiveRecord models, I want to filter all my model objects by owner_id. The thing I need is basically the default_scope for ActiveRecord.
But I need to filter by a session variable, which is not accessible from the model. I've read some solutions, but none works, basically any of them says that you can use session when declaring default_scope.
This is my declaration for the scope:
class MyModel < ActiveRecord::Base
default_scope { where(:owner_id => session[:user_id]) }
...
end
Simple, right?. But it fails saying that method session does not exists.
Hope you can help
Session objects in the Model are considered bad practice, instead you should add a class attribute to the User class, which you set in an around_filter in your ApplicationController, based on the current_user
class User < ActiveRecord::Base
#same as below, but not thread safe
cattr_accessible :current_id
#OR
#this is thread safe
def self.current_id=(id)
Thread.current[:client_id] = id
end
def self.current_id
Thread.current[:client_id]
end
end
and in your ApplicationController do:
class ApplicationController < ActionController::Base
around_filter :scope_current_user
def :scope_current_user
User.current_id = current_user.id
yield
ensure
#avoids issues when an exception is raised, to clear the current_id
User.current_id = nil
end
end
And now in your MyModel you can do the following:
default_scope where( owner_id: User.current_id ) #notice you access the current_id as a class attribute
You will not be able to incorporate this into a default_scope. This would break every usage within (e.g.) the console as there is no session.
What you could do: Add a method do your ApplicationController like this
class ApplicationController
...
def my_models
Model.where(:owner_id => session[:user_id])
end
...
# Optional, for usage within your views:
helper_method :my_models
end
This method will return a scope anyhow.
Session related filtering is a UI task, so it has its place in the controller. (The model classes do not have access to the request cycle, session, cookies, etc).
What you want is
# my_model_controller.rb
before_filter :retrieve_owner_my_models, only => [:index] # action names which need this filtered retrieval
def retrieve_owner_my_models
#my_models ||= MyModel.where(:owner_id => session[:user_id])
end
Since filtering by ownership of current user is a typical scenario, maybe you could consider using standard solutions, like search 'cancan gem, accessible_by'
Also be aware of the evils of default_scope. rails3 default_scope, and default column value in migration

Can Rails sweepers work across different controllers?

I have action caching working on my Sites index, and set up a SiteSweeper that works fine:
# app/controllers/admin/sites_controller.rb
class Admin::SitesController < Admin::BaseController
cache_sweeper :site_sweeper, :only => [:create, :update, :destroy]
caches_action :index, :cache_path => '/admin/sites'
...
# app/sweepers/site_sweeper.rb
class SiteSweeper < ActionController::Caching::Sweeper
observe Site
def after_save(site)
expire_cache(site)
end
def after_destroy(site)
expire_cache(site)
end
def expire_cache(site)
expire_action '/admin/sites'
end
end
But I also want to expire /admin/sites whenever any Publishers are saved or destroyed. Is it possible to have a PublisherSweeper expire the Sites index with something like this?
# app/sweepers/publisher_sweeper.rb
class PublisherSweeper < ActionController::Caching::Sweeper
observe Publisher
def after_save(publisher)
expire_cache(publisher)
end
def after_destroy(publisher)
expire_cache(publisher)
end
def expire_cache(publisher)
expire_action '/admin/sites'
end
end
I know I can just call expire_action '/admin/sites' within the various Publisher actions. I'm just wondering if sweepers have this capability (to keep my controllers a bit cleaner).
One sweeper can observe many Models, and any controller can have multiple sweepers.
I think you should change your logic to use something like that:
class SiteSweeper < ActionController::Caching::Sweeper
observe Site, Publisher
(…)
end
On PublishersController
cache_sweeper :site_sweeper, :admin_sweeper
So you don't repeat the logic of cleaning the /admin/site. Call it AdminSweeper, so when something goes wrong you know the only one place that expired the "/admin/sites" action.

Resources