I am relatively new to the rails framework and I am finding some difficulties in the project I'm working on using cancancan as an authorization mechanism. The following code is attached below.
def line_list
authorize! :read, ContainerTag
#activities = ContainerTag.includes(:surveillance_activity, :mobile_user).accessible_by(current_ability, :read).filters(params).ascending.paginate(:page => params[:page], :per_page => #per_page)
end
def legacy_data
authorize! :read, LegacyActivity
end
The understanding I get from following some articles they use the following syntax.
def custom_action
#blog = Blog.find(params[:blog])
authorize! :custom_action, #blog
end
In the last code snippet, they are actually authorizing by simply writing the function name and providing the blog class object that is handled by the ability class. This flow is simple to understand, but what I dont get is the above two functions working as they have given just class names instead of objects. There are some lines where they also gave function name along with the class name.
Related
I'm very new to Rails, and I'm a little overwhelmed where I do simple things like create an API call. I've set up a route at /reports which has this controller:
class ReportsController < ApplicationController
#client = # Api-accessing gem
#all_reports = []
def self.request_report
begin
puts "Step 1:"
step1 = #client.request_report(opts = {"max_count" => 1})
step1_result = step1.parse
puts "Done!"
puts step1_result
rescue Excon::Errors::ServiceUnavailable => e
puts "Didn't work"
logger.warn e.response.message
retry
end
end # End request_report
request_report
end
This correctly calls the external API when I first load the /reports route, but when I refresh the page the code isn't re-run.
Perhaps I'm misunderstanding what controllers are used for? Am I meant to be putting this code somewhere else? Or is there a caching issue?
The only public API of controller are the actions which respond to a HTTP request. In your case get "/reports" => "reports#request_report" is a route which corresponds to the action request_report.
However actions are instance methods, not class methods:
class ReportsController
def request_report # self.request_report would make this a class method!
# #todo get reports from somewhere and
# return some sort of response.
end
# any method call here happens when the class is evaluated.
end
You are declaring the action as a class method and then calling it when the ReportsController class is evaluated. Sorry to say but just about everything about your controller is wrong.
The Rails convention would be to call the action index.
Controllers in Rails should only be instantiated by the router (or your test framework). So they are definatly the wrong place to put resuable bits and bobs. If you ever see someone doing ReportsController.new.foo or ReportsController.foo - fire them on the spot.
So where do you put external API calls?
If its a pretty trivial one-off you can place it in private method in your controller.
Some place API calls on the model layer - however that is debatable since ActiveRecord models already are supercharged to the gills with powers and responsibilities.
One solution that has worked well for me is Service Objects. They are easy to test and have a clear single responsibility.
class RequestReportService
def initalize(client)
#client = client
end
def call(opts = {})
begin
return #client.request_report(opts.merge("max_count" => 1))
rescue Excon::Errors::ServiceUnavailable => e
nil
end
end
end
class ReportsController
def index
#reports = RequestReportService.new(#client).call
end
end
To add to #max's excellent answer, you need to appreciate that Rails is based on a stateless protocol (HTTP)...
each request message can [only] be understood in isolation.
This means that if you want to create a set of controller actions, you have to appreciate that each call is going to create a new instance of your classes etc. This, coupled with the idea of a RESTful set of actions, should give you a basis from which to build your functionality.
--
#config/routes
scope constraints: { subdomain: "api" } do
resources :reports #-> http://api.url.com/reports
end
#app/controllers/reports_controller.rb
class ReportsController < ApplicationController
respond_to :json #-> requires "responders" gem
def index #-> instance method
#reports = Report.all
respond_with #reports #-> all reports
end
def show
#report = Report.find params[:id]
respond_with #report
end
end
I'll leave the service object stuff as I have no experience with it.
--
If you're pulling from an external API, you have several considerations:
Calls ideally need to be asynchronous (unless you use multi-threading)
Calls need to be made in the instance method
Your current pattern calls the API on the class, which is why you can't refresh it:
class ReportsController < ApplicationController
#client = # Api-accessing gem
#client is only invoked (I don't know why it works, as it should be a class variable) with the class.
So if you send a new request (which creates an instance of ReportsController), #client is going to be declared that one time.
To get it working correctly, #client needs to be defined with each instance method:
class ReportsController < ApplicationController
def index
#client = # Api-accessing gem
This way, each time you invoke ReportsController#index, a new API call will be made. Might seem trivial, but the data scope is massive.
Finally, you need to read up about MVC (Model View Controller):
This will show you how controllers are meant to be used in Rails applications etc.
Well I actually never seen anyone code like this in a rails controller. Rails is a mvp framework. Controller are use to negotiate between your model and the views. First of all, if you routed correctly to your controller like
get "/reports" => "request_report#reports"
your controller should have a method like the following
def request_report
#client = Client.find(params[:id])
end
And then the controller will render and display the view in your app/views/reports/request_report.html.erb with access to the #client variable you just search from your database.
I am not sure why you are calling the block request_report at the bottom of the page, it just doesn't make sense in a controller. And you certainly don't really need to write self in front of a controller method.
def self.request_report
your code
end
As for where to put your api controller, usually for an api controller, we can create new folders under controllers, so the structure will be like
app/controllers/api/v1/your_api_controller.rb
Then in your_api_controller.rb you will need to add namespace infront of your controller like this.
class Api::V1::ReportsController < ActionController::Base
end
It is the same with your routes, you will add namespace in your route.rb
namespace :api do
namespace :v1 do
get "/reports" => "request_report#reports"
end
end
Ok, so my main issue is I have implemented Mailboxer into our project to handle messaging and I am trying to write tests for it. However, I keep stumbling over and over again. I have attempted several different stub/mocks but have made no progress.
We have a conversations_controller.rb that relies on before_filters for setting all the instance variables necessary for doing each action. Then in the controller actions, the instance variables are referenced directly to do any sort of action or to return specific data.
Here is an example of our index action which returns all conversations in the "box" that is specified in the before_filter, of the mailbox also specified in another before_filter:
class ConversationsController < ::ApplicationController
before_filter :get_user_mailbox, only: [:index, :new_message, :show_message, :mark_as_read, :mark_as_unread, :create_message, :reply_message, :update, :destroy_message, :untrash]
before_filter :get_box
def index
if #box.eql? "inbox"
#conversations = #mailbox.inbox
elsif #box.eql? "sentbox"
#conversations = #mailbox.sentbox
else
#conversations = #mailbox.trash
end
end
And before filters:
private
def get_user_mailbox
#user = User.where(:user_name => user.user_name.downcase).where(:email => user.email.downcase).first_or_create
#mailbox = #user.mailbox if #user
end
def get_box
if params[:box].blank? or !["inbox","sentbox","trash"].include?params[:box]
params[:box] = 'inbox'
end
#box = params[:box]
end
So I guess I have 2 questions in one. First, how to I get my tests to generate the correct data #mailbox, #user, and #box that is needed for the index action. Next, how do I pass the fake parameter to set #box to different "inbox/sentbox/trash". I have tried controller.index({box: "inbox"}) but always get "wrong arguments 1 for 0" messages.
I have tried the following in various different ways, but always get nil:class errors which means that my instance variables are definitely not being set properly.
describe "GET 'index' returns correct mailbox box" do
before :each do
#user = User.where(:user_name => 'test').where(:email => 'test#test.com').first_or_create
#mailbox = #user.mailbox
end
it "#index returns inbox when box = 'inbox'" do
mock_model User
User.stub_chain(:where, :where).and_return(#user)
controller.index.should == #mailbox.inbox
end
end
Filters and callbacks are hard to test and debug. Avoid them when possible.
In this case I don't think your before_filter is necessary, thus no need to test it. The better home for the methods is model.
Check my refacoring:
class User < ActiveRecord::Base
delegate :inbox, :sentbox, :trash, to: :mailbox
end
class ConversationsController < ::ApplicationController
def index
#conversations = current_user.send get_box
end
private
def get_box
# your code
end
end
That's all. Should be enough.
You can then test regularly.
First of all, read the oficial documentation for rails testing: using data for testing and passing parameters to controlers is explained there.
To generate data for your tests you can:
fill your test database with some mailbox and users using rails fixtures or use something like factory girl
use mock objects to fake data. I personally use the mocha gem but there are others
I tend to use a combination of both, prefering mock objects when possible and falling back to factory girl when mocking needs too much code.
my problem is related to customization in ActiveAdmin.
First of all I can't get how to override index action. Everything looks simple according to the documentation but very few things work as expected. Eventually I came up with these two alternatives. First one is a blogpost which presents the following solution (which appear to work).
scope_to do
Class.new do
def self.projects
Project.where(:id => 1)
end
end
end
While this one, which is the solution for issue#511 does not work. Can anyone tell why??
scope_to :current_project
controller do
private
def current_project
Project.where(:id => 1)
end
end
What's your experience? How do you achieve index action customization?
I'm an experienced web developer but I'm new to Ruby world in general.
Do you think it's a good idea to use ActiveAdmin for a production project? What's you pick when it comes to Admin interface?
I've read about Rails Admin but looks like it's not easy to customize.
My biggest concern at the moment is about active admin not easy to customize to achieve UI or behavior which are very different from the ones that it offers by default.
What do you think?
If you want to customize controller see https://github.com/josevalim/inherited_resources. For example:
controller do
def index
# something
index! do |format|
format.html { redirect_to some_url }
end
end
protected
def collection
#projects ||= end_of_association_chain.paginate(:page => params[:page])
end
end
I've been struggling with this for a while now, so I thought I'd ask the experts.
I am trying to make it so that Users can only edit/view Items that they have created using Devise.
I have Users set up and working well. Items are being created with a user associated with them and I can verify this via rails console.
def create
#item = Item.new(params[:item])
#item.user = current_user
end
What I am trying to do now is to make it so that once logged in, users can only see the items that they have created, and no others.
In my Items controller have tried replacing:
def index
#items = Items.all
end
with
def index
#items = current_user.Items.find(params[:id])
end
but this doens't seem to work for me and I get
undefined method `Items' for #<User:0x007fdf3ea847e0>
Can anyone offer any advice as to what to try next?
Thanks so much.
Maybe I`m old school but I would not use current_user to find records, only to verify permissions. I would use the primary key relationships directly (they don't change):
#items = Item.find(:all, :conditions => { :user_id => current_user[:id] }
or
#items = Item.find_all_by_user_id current_user[:id]
As for setting permissions, devise actually doesn`t let you do that BUT there is the excellent supplement called Cancan, you should definitely look into it. With Cancan, you will have an ability.rb class that will define your permissions. What you are looking for then becomes:
class Ability
can [:read, :show, :edit, :update, :delete, :destroy], Item do |item|
item.user_id == user.id
end
# or
can :manage, Item do |item|
item.user_id == user.id
end
end
reading the Cancan docs would clarify the code above.
What you're trying to do is really close…
current_user is an "instance" of the User class.
What you want to do is use the association from the user instance, which is a special method applied to every user—"items". (If the Item class also has a belongs_to :user it'll have a method called user as well)
You want current_user.items.find(params[:id])
Also, when you create it, you could also use current_user.items.create(params[:item])
If I'm understanding your question, I think you might want to check out an authorization library - like CanCan to do this.
https://github.com/ryanb/cancan
It works pretty slick to handle permission type things like this. Many people use this library in conjunction with Devise.
In my Ruby on Rails app, I've got:
class AdminController < ApplicationController
def create
if request.post? and params[:role_data]
parse_role_data(params[:role_data])
end
end
end
and also
module AdminHelper
def parse_role_data(roledata)
...
end
end
Yet I get an error saying parse_role_data is not defined. What am I doing wrong?
Helpers are mostly used for complex output-related tasks, like making a HTML table for calendar out of a list of dates. Anything related to the business rules like parsing a file should go in the associated model, a possible example below:
class Admin < ActiveRecord::Base
def self.parse_role_data(roledata)
...
end
end
#Call in your controller like this
Admin.parse_role_data(roledata)
Also look into using (RESTful routes or the :conditions option)[http://api.rubyonrails.org/classes/ActionController/Routing.html] when making routes, instead of checking for request.post? in your controller.
Shouldn't you be accessing the parse_role_data through the AdminHelper?
Update 1: check this
http://www.johnyerhot.com/2008/01/10/rails-using-helpers-in-you-controller/
From the looks of if you're trying to create a UI for adding roles to users. I'm going to assume you have a UsersController already, so I would suggest adding a Role model and a RolesController. In your routes.rb you'd do something like:
map.resources :users do |u|
u.resources :roles
end
This will allow you to have a route like:
/users/3/roles
In your RolesController you'd do something like:
def create
#user = User.find_by_username(params[:user_id])
#role = #user.roles.build(params[:role])
if #role.valid?
#role.save!
redirect_to #user
else
render :action => 'new'
end
end
This will take the role params data from the form displayed in the new action and create a new role model for this user. Hopefully this is a good starting point for you.