expire application_controller cached method from API controller - ruby-on-rails

i have the following method on application_controller:
controllers/application_controller.rb
def set_footer_sponsored_links
#sponsored_links = Rails.cache.fetch("sponsored_links", expires_in: 240.hours) {
ContentGroup.sponsored_links
}
end
This sponsored_links (ContentGroup.sponsored_links) are created and updated via API:
controllers/api/v1/sponsored_links_controller.rb:
class Api::V1::FooterSponsoredLinksController < Api::V1::ApiController
before_filter :set_sponsored_link, only: [:destroy, :update, :show]
def create
sponsored_link = SponsoredLink.create(sponsored_link_params)
respond_with :api, sponsored_link
end
def update
#sponsored_link.update(sponsored_link_params)
respond_with :api, #sponsored_link
end
What i need is to set an expire cache method or similar that, from the API, expires cache on the method on application_controller whenever :create, :update or :destroy are called.
Any idea how to accomplish so?
Thanks in advance

First, create a private method in your Api::V1::FooterSponsoredLinksController called expire_links:
...
private
def expire_links
Rails.cache.delete('sponsored_links')
end
Then add a before_filter for that method like this:
before_filter :expire_links, only: [:create, :update, :destroy]

You should probably use a cache key that includes the maximum updated_at DateTime like so:
def set_footer_sponsored_links
#sponsored_links = Rails.cache.fetch("sponsored_links-#{SponsoredLink.maximum(:updated_at).try(:utc).try(:to_s, :number)}", expires_in: 240.hours) {
ContentGroup.sponsored_links
}
end

Related

Restrict ruby routes based on database parameter

I have a Users model that has an admin field. This is boolean. I only want to a User to create/modify/update a "Season" if the admin field is equal to true. How would I go about doing this in rails? Do i put this logic in the "Season" controller? Something to do with my routes config?
You can use single table inheritance like create a table User and create 2 more model like
Admin < User
{
default_scope { where(user_type: :college) }
....
}
and
Supplier < User
{
default_scope { where(user_type: :supplier) }
....
}
Put the logic in the SeasonsController in a before_action, that performs your desired error action unless they are an admin.
class SeasonsController < ApplicationController
before_action :authenticate_user!
before_action :ensure_admin_user!, except: [:show, :index]
...
private
def ensure_admin_user!
render_error unless current_user.admin?
end
def render_error
# whatever
end
end
Action Controller Docs: filters
I suggest you add a before_action filter in your controller:
class SessionsController < ApplicationController
before_action :check_permission!, only: [:create, :update]
# Your create, update actions
private
def check_permission!
render status: :forbidden unless current_user.admin?
end
end
You can create before_action callback to check whether the user's admin field is true or false.
class SeasonsController
before_action :check_admin_field, only: [:edit, :new]
protected
def check_admin_field
return User.find(params[:user_id]).admin_field
# if you're using devise gem then check current_user.admin_field
# or you can redirect to other path with a notice
end
end
You can check this here
If you are not passing the user_id parameter in your URL then you have to change the routes.rb like:
resources :users do
resources :seasons
end
This will create your new season route url like: root_url/users/1/seasons/new
put 'seasons/:user_id/:id' => 'home#update', :constraints => {user_id: User.where(admin: true).pluck(:id) }
post 'seasons/:user_id/:id' => 'home#create', :constraints => {user_id: User.where(admin: true).pluck(:id) }
resources :seasons, except: [:create, :update]
Change your route.rb file as per above.

How to get the name of the action in an after_action filter for ActionMailer

In the class below, how do I get the current action name (i.e. email_confirmation, password_reset) in side the after_action callback add_mandril_headers?
class UserMailer < ActionMailer::Base
after_action :add_mandril_headers
def email_confirmation(user)
mail(..)
end
def password_reset(user)
mail(..)
end
private
# how to get the action name?
def add_mandrill_headers
headers['X-MC-Tags'] = [mailer_name, action_name].join('_');
end
end
Turns out action_name returns the current mailer action name. I tried it based on the fact that ActionController has a similar method.
Thanks #HarishShetty!
As you mentioned, the action_name is good for all Controllers, as it is inherited from ApplicationController.
For example, I was using public_activity and wanted some simplification in my controllers:
class SiteDetailsController < ApplicationController
after_action :track_activity, only: [:create, :update, :destroy]
# ...
private
def track_activity
#site_detail.create_activity action_name, owner: current_user
end
end

Set a before action to all member routes?

Playing around with Rails 4, I noticed that it defines a before_action filter set_[model], that is called for the actions show, edit, update and destroy.
It is in fact very useful, but I don't see much sense in not having it to all member actions. After all, if you are using a member action, you want to act on that member, thus you need to recover it from database at some point.
Note that by member actions, I also means the ones configured on routes.rb in the members block.
Is there a straight-forward way to do this, without list all member actions on the before_action filter?
Edit: To clarify, the whole point is use some rails magic to get all member routes and generate the array that would be pass in the :only. So I can do something like
before_action set_model only: all_member_routes
where all_member_routes is a piece of code that returns all member routes for my model.
Or even better,
before_action set_model, only_member_actions: true
The show, edit, update, destroy actions are precisely all member actions. They are the only ones that require to find the model.
If you have your own member action, then you'll have to add it to the list yourself.
before_action :set_item, only: [:edit, ..., :my_member_action]
Or, you can use the :except option to exclude all the collection actions that do not need it:
before_action :set_item, except: [:index, :create]
This way, if you add other member actions, you wont have to change anything.
Personally, I prefer to be explicit and use :only.
I'm pretty sure there is no easier way to do it, you can't detect all member actions automatically.
edit:
I really don't think you should do that but...
You can access the name of your controller with the controller_name method.
Getting the routes related to the controller:
routes = Rails.application.routes.routes.select { |r| r.defaults[:controller] == controller_name }
Then, I think the best way to see if a route is a member route is that the #parts array includes :id. Maybe you can find a more robust way.
So I would do:
routes.select { |r| r.parts.include?(:id) }.map { |r| r.defaults[:action] }.map &:to_sym
That would give you: [:show, :preview, :my_challenges] for
resources :users, only: [:index, :show], controller: 'accounts/users' do
member do
get :preview
get :my_challenges
end
end
class ApplicationController < ActionController::Base
def member_routes
Rails.application.routes.routes
.select { |r| r.defaults[:controller] == controller_name && r.parts.include?(:id) }
.map { |r| r.defaults[:action] }
.map(&:to_sym)
end
end
class UsersController < ApplicationController
before_action set_model, only: member_routes
end
If you ask for a before_action without :only or :except options, it will apply to all member actions:
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
In this particular case, it will require login from all actions on all controllers, since controllers will inherit from ApplicationController.
You can skip a before_action if you need it (for example, you need to skip require_login if you want to login into the system or sign up) like this:
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
Source: Rails Guides
So, in your particular case:
You could have one usual UserController:
class UserController < ApplicationController
def index
end
...
/* you define here `index`, `create`, `update`, ... */
def destroy
...
end
end
And you could have a separate controller with all your member actions:
class UserCustomController < ApplicationController
before_action :set_model
def profile
...
end
def preview
end
def custom_member_action
end
...
/* all your member actions */
end
This would be actually better than having a single controller with lots of methods.

How to execute ApplicationController filters before than resource_from_invitation_token?

I have a multitenancy app and I'm setting the current tenancy like this:
class ApplicationController < ActionController::Base
around_filter :scope_current_tenancy
def scope_current_tenancy
Tenancy.current_id = current_tenancy.id if request.subdomain != 'www'
yield
ensure
Tenancy.current_id = nil
end
end
Then in my user model I have a default_scope defined to access only to users within my tenancy:
class Postulant < ActiveRecord::Base
default_scope ->{ where("enlistments.tenancy_id = ?", Tenancy.current_id).includes(:enlistments).references(:enlistments) }
This works so far, but now using devise_invitable and trying to accept an invitation I'm receiving a Filter chain halted as :resource_from_invitation_token rendered or redirected message. The problem is because my scope_current_tenancy filter is being executed after resource_from_invitation_token, so resource is not loading correctly.
class Devise::InvitationsController < DeviseController
prepend_before_filter :resource_from_invitation_token, :only => [:edit, :destroy]
def resource_from_invitation_token
# Here 'resource_class' is my Postulant model, so when I call
# 'find_by_invitation_token' applies the defined default_scope
# which doesn't word without 'scope_current_tenancy'
unless params[:invitation_token] && self.resource = resource_class.find_by_invitation_token(params[:invitation_token], true)
set_flash_message(:alert, :invitation_token_invalid)
redirect_to after_sign_out_path_for(resource_name)
end
end
end
So my question is, is there a way to run :scope_current_tenancy before than :resource_from_invitation_token?
I've tried to change around_filter :scope_current_tenancy to prepend_around_filter :scope_current_tenancy but I had no luck. Any thoughts?
Because the prepend_before_filter :resource_from_invitation_token comes after your ApplicationController, this filter will be prepended to the front of the filter chain, even if you use prepend_before_filter for scope_current_tenancy. One option might be to try something like:
skip_around_filter :scope_current_tenancy
prepend_before_filter :resource_from_invitation_token, :only => [:edit, :destroy]
prepend_around_filter :scope_current_tenancy
in your Devise::InvitationsController
Not sure if this will work, but seems worth a shot.
Alternately, you could just drop the 'skip_around_filter' line, assuming the scope_current_tenancy is idempotent, which seems to be the case.

Ruby on Rails pattern for setting class defaults and allowing an override on subclasses

Basically I want to implement a simple Rails extension to define the seriousness of methods in my controller so that I can restrict usage of them appropriately. For example I'd define the default restful actions as so in an abstract superclass:
view_methods :index, :show
edit_methods :new, :create, :edit, :update
destroy_methods :destroy
I'd then down in a non-abstract controller call:
edit_methods :sort
to add in the sort method on that particular controller as being an edit level method.
I could then use a before_filter to check the level of the action currently being performed, and abort it if my logic determines that the current user can't do it.
Trouble is, I'm having trouble working out how to set up this kind of structure. I've tried something like this so far:
class ApplicationController
##view_methods = Array.new
##edit_methods = Array.new
##destroy_methods = Array.new
def self.view_methods(*view_methods)
class_variable_set(:##view_methods, class_variable_get(:##view_methods) << view_methods.to_a)
end
def self.edit_methods(*edit_methods)
class_variable_set(:##edit_methods, self.class_variable_get(:##edit_methods) << edit_methods.to_a)
end
def self.destroy_methods(*destroy_methods)
##destroy_methods << destroy_methods.to_a
end
def self.testing
return ##edit_methods
end
view_methods :index, :show
edit_methods :new, :create, :edit, :update
destroy_methods :destroy
end
The three methods above are different on purpose, just to show you what I've tried. The third one works, but returns the same results no matter what controller I test. Probably because the class variables are stored in the application controller so are changed globally.
Any help would be greatly appreciated.
The problem is that your class variables are inherited, but point to the same instance of Array. If you update one, it will also be updated on all classes that inherited the Array.
ActiveSupport offers a solution to this problem by extending the Class class with several methods to define inheritable class attributes. They are used everywhere internally in Rails. An example:
class ApplicationController
class_inheritable_array :view_method_list
self.view_method_list = []
def self.view_methods(*view_methods)
self.view_method_list = view_methods # view_methods are added
end
view_methods :index, :show
end
Now you can set default values in ApplicationController and override them later.
class MyController < ApplicationController
view_method :my_method
end
ApplicationController.view_method_list #=> [:index, :show]
MyController.view_method_list #=> [:index, :show, :my_method]
You can even use the view_method_list as an instance method on the controllers (e.g. MyController.new.view_method_list).
In your example you didn't define a way to remove methods from the lists, but the idea is to do something like the following (in case you need it):
# given the code above...
class MyController
self.view_method_list.delete :show
end
MyController.view_method_list #=> [:index, :my_method]
I turned it into a plugin like so:
module ThreatLevel
def self.included(base)
base.send :extend, ClassMethods
end
module ClassMethods
def roger_that!
class_inheritable_array :view_method_list, :edit_method_list, :destroy_method_list
self.view_method_list = Array.new
self.edit_method_list = Array.new
self.destroy_method_list = Array.new
def self.view_methods(*view_methods)
self.view_method_list = view_methods
end
def self.edit_methods(*edit_methods)
self.edit_method_list = edit_methods
end
def self.destroy_methods(*destroy_methods)
self.destroy_method_list = destroy_methods
end
view_methods :index, :show
edit_methods :new, :create, :edit, :update
destroy_methods :destroy
end
end
end
ActionController::Base.send :include, ThreatLevel
Calling roger_that! on the super_controller where you want it to take effect does the trick.

Resources