passing argument to model method with grape-entity - ruby-on-rails

How can I pass an argument to a model method using grape entity ?
I'd like to check wheter the current_user likes an item when presenting the item, so I built a model user_likes? method:
class Item
include Mongoid::Document
#some attributes here...
has_and_belongs_to_many :likers
def user_likes?(user)
likers.include?(user)
end
end
but I don't know how to send current_user to the grape-entity model :
module FancyApp
module Entities
class Item < Grape::Entity
expose :name #easy
expose :user_likes # <= How can I send an argument to this guy ?
end
end
end
in the grape api:
get :id do
item = Item.find(.....)
present item, with: FancyApp::Entities::Item # I should probably send current_user here, but how ?
end
I feel like the current_user should probably be sent from this last piece of code, but I can't figure how to do it :(
Any thoughts ?
Thanks !

Well, I found I could pass current as a parameters, and use it in a block. So :
present item, with: FancyApp::Entities::Item, :current_user => current_user
and in the entity definition:
expose :user_likes do |item,options|
item.user_likes?(options[:current_user])
end

#aherve, for some reason your syntax won't work in my case. The syntax in Grape Entity docs is a little different
your example, the syntax should be:
expose(:user_likes) { |item, options| item.user_likes?(options[:current_user]) }

Another way would be to store the current user in the item temporarily by defining an attribute accessor:
class Item
include Mongoid::Document
#some attributes here...
has_and_belongs_to_many :likers
attr_accessor :current_user
def user_likes
likers.include?(current_user)
end
end
and in the grape api set the current user:
get :id do
item = Item.find(.....)
item.current_user = current_user
present item, with: FancyApp::Entities::Item
end
no changes to the grape-entity model is needed.
There is no database field current_user_id or so. No writes to database.

Related

Using Rails helpers in model

In my model I have a method that marks a record as pending by changing its status to 2. After which it calls another method in another controller to create a notification containing details of the record that was changed.
e.g.
class Page < ActiveRecord::Base
def pend_page
self.update(status: 2)
Notification.create_notification("#{link_to self.title, pages_path(:status => 2)} marked as pending", #current_user)
end
end
However it seems Rails doesn't pass helpers for link_to and the routes to the models... as I get the error: undefined method 'pages_path' for #<Page:0x007fd15c996c88>.
How can I make it so that the link_to and pages_path work?
I'm using Rails 4.2.5.1
edit: here is what create_notification looks like:
class Notification < ActiveRecord::Base
belongs_to :user
def self.create_notification(content, user)
notification = Notification.new
notification.content = content
notification.user_id = user.id
notification.status = 0
notification.save
end
end
This should go in either a service object or in a PORO (plain old ruby object). A model's concerns should begin and end with database related functionality, anything else is in the wrong place.

How to call the model's where(...) method from ApplicationController

Let's say I have two models: Client and Product
The "username" and "email" of Client should be "unique index", as "serialnumber" of Product
When the user is typing on the form field that is unique index, I have an onblur function that sends a request to the controller with the attribute name and attribute value. If a value exists, the user is immediately informed.
In ClientController, I wrote a function that checks if it's unique or not and returns -2 for error, -1 for not exists or a positive number (the id) if exists.
def unique
if params[:attrName].blank? or params[:attrValue].blank?
id = "-2"
else
cli = Client.where("#{params[:attrName]} = '#{params[:attrValue]}'").first
if cli != nil
id = cli["id"]
else
id = "-1"
end
end
render :json => {
:id => id
}
end
This is not good for many reasons (SQL Injection vulnerability, violation of DRY, as each controller would have basically the same method.
I'm thinking of writing the "unique" function inside ApplicationController, but as you saw above, I should be able to call "Client.where" if it's a client, or "Product.where" if it's a product. How can I build this function the most "generically" possible and the most securily? I'm thinking of raw SQL but I think this is a naive approach.
It would be wise to avoid raw SQL for this.
Would this work?
class ApplicationController < ActionController::Base
def unique
id = if params[:attrName].blank? || params[:attrValue].blank?
-2
elsif found = model_name.where(params[:attrName] => params[:attrValue]).take
found.id
else
-1
end
render json: { id: id }
end
end
You could put that in application_controller.rb then in both of your ClientsController and ProductsController you would define the model_name method:
class ClientsController < ApplicationController
def model_name
Client
end
end
class ProductsController < ApplicationController
def model_name
Product
end
end
This will work but might not be ideal. You might want to let Rails do more of the work by using find to raise if a model exists or not and strong params for validating that the params you need are present.
You can move this to a module and make it return an ActiveRecord relation. the advantage is later you can chain this with other ActiveRecord relations if you wish to, Something like (and note I have used ? in my sql condition, instead of directing giving the param )
#module
module UniqueRecord
module ClassMethods
def unique(params)
where(params)
end
end
def self.included(receiver)
receiver.extend ClassMethods
end
end
and use it in your class
#client.rb
class Client < ActiveRecord::Base
include UniqueRecord
end
#product.rb
class Product < ActiveRecord::Base
include UniqueRecord
end
So now both of your classes has the method unique available.
you can create a hash from the keys and values you get, Ex: you could dynamically create a hash to search email like
hash = {email: 'same#email.com'}
and then call the method
Client.unique(hash)
and if you want to , you can call it by the class name string
'Client'.constantize.unique(hash)
one more thing, it better to return an array of objects (if found) or blank array (if not found) instead of -1, -2. that will make your api consistent. like
Client.unique(hash).to_json

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

Rails: Adding methods to models to perform checks based on current_user?

I have a model that looks something like this:
class Comment < ActiveRecord::Base
...
#allow editing comment if it is moderated and the user passed-in
#is the one that owns the comment
def can_edit?(user)
moderated? and user.Type == User and user.id == self.user_id
end
...
end
And a call in a view:
<%= link_to 'Show Comment', #comment if #comment.can_show?(current_user) %>
I need to write many such methods in many different models - sort of validation checks to see if current_user is allowed to
do something on a model.
But it feels cumbersome - especially the need to check that the passed-in user is indeed a object of type User.
What's a clean, best-practice way to do this sort of thing? Am I on the right track? (i.e. should I be adding such methods to a model or somewhere else)
Note
I am using scoped queries to get the comments and other models, but in some cases I cannot scope the query so I have to use the can_xxxx? methods
Ps. Is what I'm doing considered a "fat model"?
Create a module containing all the authorization methods and include the module to all the classes requiring authorization.
Add a file called authorization.rb to app/models directory.
module Authorization
def can_edit?(user)
moderated? and user.is_a?(User) and user.id == self.user_id
end
def self.included(base)
base.send(:extend, ClassMethods)
end
module ClassMethods
# add your class methods here.
end
end
Add a file called authorization.rb to config/initializers directory.
%w(
Comment
Post
).each do |klass|
klass.constantize.include(Authorization)
end
Now Comment and Post models will have all the authorization methods.
Other approach is to use your current named_scope.
class Post
named_scope :accessible, lambda { |user|
{
:conditions => { :user_id => user.id, :moderated => true}
}
}
end
Post controller actions
class PostsController
def index
#posts = Post.acessible(current_user)
# process data
end
def show
# throws record not found when the record is not accessible.
#post = Post.acessible(current_user).find(params[:id])
# process data
end
end
I like this approach as it uses the same logic for accessing an array of objects or a single object.
You can add the named_scope to the module to avoid repeated definitions:
module Authorization
def self.included(base)
base.named_scope :accessible, lambda { |user|
{
:conditions => { :user_id => user.id, :moderated => true}
}
}
end
module ClassMethods
# add your class methods here.
end
end
Make sure to include the module in required classes as suggested earlier.
I don't think what you're doing is necessarily wrong. I see three ways to simplify, though:
1) track self.user as well as self.user_id. Then you can say:
def can_show?(user)
moderated ? and user == self.user
end
Note, this might add overhead either with DB lookup times and/or memory footprint.
2) Use #is_a? in order to check ancestry and not just class equality:
def can_show?(user)
moderated ? and user.is_a?( User ) and user.id == self.user_id
end
3) If passing in a non-user is wrong, you might want to raise an error when this happens:
def can_show?(user)
raise "expected User, not #{ user.class.to_s }" unless user.is_a?(User)
moderated ? and user.id == self.user_id
end
As for Q2, I haven't heard the terminology "fat model." Is it referenced anywhere in particular?
Re: fat model and skinny controller
This is the idea of pushing logic into the model rather than having it in the controller (or worse, the view).
A big benefit is to help with testing; also the focus of placing more logic in the model rather than in the controller. Remember that it is not uncommon to have controllers work with multiple models.
Putting the logic into a model rather than a controller often means that the business rules are being baked into the model--which is exactly where they belong.
A possible downside is that any information available to the controller that is not available in the model needs to be explicitly passed into the model's methods or "set" using a model's instance variables.
Your example of needing to pass the current user into the model illustrates the issue.
Overall though, I and many others have found that fat models tend to work out better than not.

Is there a way to validate a specific attribute on an ActiveRecord without instantiating an object first?

For example, if I have a user model and I need to validate login only (which can happen when validating a form via ajax), it would be great if I use the same model validations defined in the User model without actually instantiating a User instance.
So in the controller I'd be able to write code like
User.valid_attribute?(:login, "login value")
Is there anyway I can do this?
Since validations operate on instances (and they use the errors attribute of an instance as a container for error messages), you can't use them without having the object instantiated. Having said that, you can hide this needed behaviour into a class method:
class User < ActiveRecord::Base
def self.valid_attribute?(attr, value)
mock = self.new(attr => value)
unless mock.valid?
return mock.errors.has_key?(attr)
end
true
end
end
Now, you can call
User.valid_attribute?(:login, "login value")
just as you intended.
(Ideally, you'd include that class method directly into the ActiveRecord::Base so it would be available to every model.)
Thank you Milan for your suggestion. Inspired by it I created a simple module one can use to add this functionality to any class. Note that the original Milans suggestion has a logic error as line:
return mock.errors.has_key?(attr)
should clearly be:
return (not mock.errors.has_key?(attr))
I've tested my solution and it should work, but ofc I give no guarantees. And here's my glorious solution. Basically a 2-liner if you take away the module stuff.. It accepts method names as stings or symbols.
module SingleAttributeValidation
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def valid_attribute?(attr, value)
mock = self.new(attr => value)
(not mock.valid?) && (not mock.errors.has_key?(attr.class == Symbol ? attr : attr.to_sym))
end
end
end
To use your standard validation routines:
User.new(:login => 'login_value').valid?
If that does not work for you, build a custom class method for this:
class User < ActiveRecord::Base
validate do |user|
user.errors.add('existing') unless User.valid_login?(user.login)
end
def self.valid_login?(login)
# your validation here
!User.exist?(:login=> login)
end
end
I had a hell of a time getting this to work in Rails 3.1. This finally worked. (Not sure if it's the best way to do it, I'm kind of a newb.). The problem I was having was that value was being set to type ActiveSupport::SafeBuffer, and was failing validation.
def self.valid_attribute?(attr, value)
mock = User.new(attr => "#{value}") # Rails3 SafeBuffer messes up validation
unless mock.valid?
return (not mock.errors.messages.has_key?(attr))
end
return true
end
I have gone with the custom class solution but I just wanted to make sure there was no better way
class ModelValidator
def self.validate_atrribute(klass, attribute, value)
obj = Klass.new
obj.send("#{attribute}=", value)
obj.valid?
errors = obj.errors.on(attribute).to_a
return (errors.length > 0), errors
end
end
and I can use it like
valid, errors = ModelValidator.validate_attribute(User, "login", "humanzz")
class User < ActiveRecord::Base
validates_each :login do |record, attr, value|
record.errors.add attr, 'error message here' unless User.valid_login?(value)
end
def self.valid_login?(login)
# do validation
end
end
Just call User.valid_login?(login) to see if login itself is valid
An implementation of the 'valid_attribute' method you are suggesting:
class ActiveRecord:Base
def self.valid_attribute?(attribute, value)
instance = new
instance[attribute] = value
instance.valid?
list_of_errors = instance.errors.instance_variable_get('#errors')[attribute]
list_of_errors && list_of_errors.size == 0
end
end
How about:
User.columns_hash.has_key?('login')

Resources