Rails / Redis / Soulmate - How to limit search results by user role? - ruby-on-rails

I have set up my app to use Soulmate and soulmate.js for autocomplete. It searches two models - Users and Books. But, I have multiple roles for users - some are authors and some are readers. I only want authors to show up in the results.
Is there a way to limit what is added to Redis as new users sign up, based on their role?
in user.rb (using the "if role = "author") did not work
def load_into_soulmate
loader = Soulmate::Loader.new("authors")
loader.add("term" => fullname, "id" => self.id, "data" => {
"link" => Rails.application.routes.url_helpers.user_path(self)
}) if self.role = "author"
end
def remove_from_soulmate
loader = Soulmate::Loader.new("authors")
loader.remove("id" => self.id)
end
in pages.js
$('#search').soulmate({
url: '/search/search',
types: ['books','authors'],
renderCallback : render,
selectCallback : select,
minQueryLength : 2,
maxResults : 5
})
Alternatively, could I add the role to what Redis stores and then tell the js to only serve users where role = "author"?
Thanks!!

I solved it with a conditional in the after_save in user.rb:
after_save :load_into_soulmate, :if => :is_author
and
def is_author
if self.role == "author"
true
else
false
end
end

Related

Ruby on Rails search with multiple parameters

For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.

how to join a subquery with conditions in Squeel

Prologue
I've embraced Squeel – and enjoying every step! Thank you so much for sharing, Ernie Miller!
I'm developing with ruby 1.9.2 and Squeel 1.0.2 and Rails 3.2.5
(I'll confess to having restructured the question entirely - hoping to increase the readability and better my chances of getting an answer) <:)
Use case
I'd like a (super)user to be able to assign authorizations and permissions like this
a user_group should be able to have multiple authorizations
an authorization should be able to have multiple permissions
a permission should be able to control access to (manipulating) data
via the controller (the request path)
on instances of a Class
on any particular instance
The ACL system should be lazy – ie if no roles/authorizations are given, the users obviously does not concern themselves with ACL at all.
Migrations
I identified role and (a polymorphic) roleable entities from the use case an thus I have
a Role right out of the ordinary
create_table :roles do |t|
t.references :ox
t.string :name
t.boolean :active, default: true
t.timestamps
end
and a Roleable a bit more descriptive
create_table :roleables do |t|
t.references :ox
t.references :role
t.references :roleable, polymorphic: true
t.string :authorization
t.string :controller
t.boolean :active, default: true
t.timestamps
end
Classes
The system has a generic class - AbstractActionBase - which inherits from ActiveRecord:Base, and which all classes inherits from (allowing me to add systemwide attributes and methods in one place)
So - in part - my AbstractActionBase looks like
class AbstractActionBase < ActiveRecord::Base
self.abstract_class=true
require 'tempfile'
belongs_to :ox
has_many :roleables, as: :roleable
attr_accessible :ox_id
validates_presence_of :ox_id
#
# all models inheriting from this will have versions
has_paper_trail
#
#
#
# Class method to providing for index SELECT's being married with roleables (permissions)
# used from abstraction_actions_controller where build_collection calls this method
# the result 'should' be an ActiveRelation - used for the Kamanari 'result' call to readying pagination
#
def self.with_authorizations
#
# SELECT * FROM any_table at
# left join (
# select r.roleable_id, r.roleable_type, group_concat( r.authorization )
# from roleables r
# where r.authorization is not null
# and r.roleable_id=at.id
# and r.roleable_type=at.base_class
# and r.role_id not in (1,2,3) <--- ID's are current_user.roles
# ) rm on rm.roleable_id=at.id and rm.roleable_type=at.base_class
#
# which will provide for this:
#
# |.......| last column in table 'at' | roleable_id | roleable_type | authorizations |
# |.......| some value | 1 | 'UserGroup' | 'insert,create'|
# |.......| yet another value | 92 | 'UserGroup' | 'read' |
#
#
self.where{ active==true }
end
# compile a collection of records - regard search using Ransack
def base.collection( params, resource_set )
#
# kaminari (and continous scrolling)
#
params[:page] ||= 1
params[:per_page] ||= self.per_page
params[:o] ||= self.resource_order_by
distinct = params[:distinct].nil? ? false : params[:distinct].to_i.zero?
resource_set = (resource_set.respond_to?( "result")) ? resource_set.result(:distinct => distinct) : resource_set
(resource_set.respond_to?( "page")) ? resource_set.order(params[:o]).page( params[:page] ).per( params[:per_page] ) : resource_set.order(params[:o])
end
end
Part of the Role class looks like this
class Role < AbstractActionBase
has_many :roleables
scope :active, where{ active.eq true }
#
# what does this role allow
def permissions
roleables.permissions.scoped
end
#
# to whom does this role allow
def authorizations
roleables.authorizations.scoped
end
# returns true if the roleables (permissions) authorizes the options
# options are { controller: "", action: "", record: Instance, is_class: boolean }
def authorizes?( options={} )
coll = permissions
coll = coll.on_action(options.delete(:action)) if options.keys.include? :action
coll = coll.on_entity( options.delete(:record), options.delete(:is_class) || false ) if options.keys.include? :record
coll = coll.on_controller(options.delete(:controller)) if options.keys.include? :controller
(coll.count>0) === true
end
end
The Roleable class looks like this
class Roleable < AbstractActionBase
belongs_to :role
belongs_to :roleable, polymorphic: true
# roleables authorizes users through user_groups
# (in which case the authorization is "-")
# providing them permissions on controllers, actions and instances
scope :authorizations, where{ authorization == nil }
scope :permissions, where{ authorization != nil }
# using Squeel, find roleables on a particular controller or any controller
def self.on_controller(ctrl)
where{ (controller==ctrl) | (controller==nil) }
end
# using Squeel, find roleables on a particular authorization or allowed 'all'
def self.on_action(action)
where{ (authorization=~ "%#{action}%") | (authorization=="all") }
end
# using Squeel, find roleables on a particular instance/record or class
def self.on_entity(entity, is_class=false)
if is_class
where{ ((roleable_type==entity.base_class.to_s ) & ( roleable_id==nil)) | ((roleable_type==nil) & (roleable_id==nil)) }
else
where{ ((roleable_type==entity.class.to_s ) & ( roleable_id==entity.id)) | ((roleable_type==nil) & (roleable_id==nil)) }
end
end
end
Logic
Creating
This allows me authorizations - assigning roles to someone/something - in which case the authorization string is nil, like
The user_group sales is assigned the role sales
with Roleable.create({ role: #sales, roleable: #user_group })
At the same time I can do permissions - describing the particulars of any role - like
The role sales has index, create, edit and delete permissions
on the OrderHead and OrderDetail tables with
Roleable.create({ role: #sales, authorization: "index,create,edit,delete", roleable: #user_group, controller: "order_heads" })
Roleable.create({ role: #sales, authorization: "index,create,edit,delete", roleable: #user_group, controller: "order_details" })
these 'particulars' can be ethereal like
Roleable.create({ role: #sales, authorization: "index" })
somewhat real
Roleable.create({ role: #sales, authorization: "index", roleable_type: 'OrderHead' })
or very expressed
Roleable.create({ role: #sales, authorization: "index", roleable: OrderHead.first })
Selecting
Most every controller inherits from AbstractActionsController where the index (and other actions) are defined. That controller it self inherits from InheritedResources:Base like this
class AbstractActionsController < InheritedResources::Base # < ApplicationController
append_view_path ViewTemplate::Resolver.instance
respond_to :html, :xml, :json, :js, :pdf
belongs_to :ox, :optional => true
before_filter :authorize!
before_filter :authenticate!
before_filter :warn_unless_confirmed!
before_filter :fix_money_params, :only => [:create,:update]
# GET /collection - printers
def index
# session[:params] = params
#
# preparing for Ransack
unless params[:q].nil?
params[:q]= { :"#{params[:q_fields]}" => params[:q] }
end
super do |format|
format.html
format.js { render layout: false }
format.pdf{ render :pdf => generate_pdf(false) and return }
format.xml { render layout: false }
format.json do
# field lookup request?
unless params[:lookup].nil?
render layout: false, :json => collection.map(&:select_mapping)
else
render json: collection.map { |p| view_context.grow_mustache_for_index(p, collection, (parent? ? collection : resource_class.order(:id)), #selected ) }
end
end
end
end
# the collection method on inherited_resources
# gets overloaded with Ransack search and Kaminari pagination (on the model)
def collection
# #collection ||= build_collection
# TODO - test whether caching the collection is possible
build_collection
end
def build_collection
unless params[:belongs].nil?
# debugger
parent = params[:belongs].constantize.find(params[:belongs_id])
#selected = parent.nil? ? [] : parent.send( rewrite_association(params[:assoc],parent) )
#search_resource = core_entity(params[:assoc].constantize)
#search_resource = #search_resource.search(params[:q]) unless params[:q].nil?
else
#search_resource = rewrite_end_of_association_chain(resource_class)
#search_resource = core_entity(#search_resource)
#search_resource = #search_resource.search(params[:q]) unless params[:q].nil?
end
# authorize rows
#search_resource = #search_resource.with_authorizations # left joins roleables coalescing a "authorization" field from roles ID's not owned by current_user through his user_groups
#resources ||= resource_class.collection( params, #search_resource )
end
end
Challenge
What a long story to presenting a short question <:)
How do I write the with_authorizations method to returning a ActiveRelation (and preferably using Squeel)
Walt,
You may be making this more complicated than necessary. If I'm reading this right, the primary purpose of the subquery is to get a concatenated list of the authorizations available in the results. If this is the case, you can simply eager_load authorizations and expose their names via a method on the Role model which does the concatenation for you. This has the secondary upside of being compatible with DBs other than MySQL.
Like I said - preferably using Squeel :)
It turns out that (from the horses mouth so to speak) joins are for associations in Squeel-county ;)
So - what to do? Well, I did one last tour de SO with my SQL-to-ActiveRecord lasso swinging, and lo' and behold! Someone had asked a great question - and there was an even greater answer! Perfect.
In a few short almost fever blinded moments I hacked away using the technique described - and Heureka !!
Previously, I added a pastiebin to aid possible "answerees" - so I've added the result to the pastiebin - but in short it goes like this:
Model.select("something").joins("to your hearts contend")
Cheers,
Walther

Using cancan the proper way

I have a certain requirement where the views have different content based upon the type of user. Lets say I have the index action for the users controller. Then I can use cancan to authorize the action like this
authorize! :index, #users
Further for filtering the content I have another authorization like
if can :view_all,User
Further another authorization like
if can :view_some,User will require another one.
This will result in lots of conditions. Instead of this, I could have used just simple conditions like
If the user is with view_all access show him all
else if the user is with view_some access show him some
else access denied
Cancan requires one extra query, isn't it? I might be using cancan the wrong way. So need some suggestions.
Here is the rough snippet of my ability.rb file
can :index, User do |user1|
role.accesses.include?(Access.where(:name => "access1").first) || role.accesses.include?(Access.where(:name => "access2").first)
end
can :view_all, User do |user1|
role.accesses.include?(Access.where(:name => "access1").first)
end
can :view_some, User do |user1|
role.accesses.include?(Access.where(:name => "access2").first)
end
Cancan requires one extra query?
When combining abilities, cancan will use a single query.
If you look at the specs, eg. spec/cancan/model_adapters/active_record_adapter_spec.rb, you'll find specs like this:
it "should fetch any articles which are published or secret", focus: true do
#ability.can :read, Article, :published => true
#ability.can :read, Article, :secret => true
article1 = Article.create!(:published => true, :secret => false)
article2 = Article.create!(:published => true, :secret => true)
article3 = Article.create!(:published => false, :secret => true)
article4 = Article.create!(:published => false, :secret => false)
Article.accessible_by(#ability).should == [article1, article2, article3]
end
And if you turn on SQL logging, you'll see that the query combines the conditions:
Article Load (0.2ms)
SELECT "with_model_articles_95131".*
FROM "with_model_articles_95131"
WHERE (("with_model_articles_95131"."secret" = 't')
OR ("with_model_articles_95131"."published" = 't'))

Trying to master Ruby. How can I optimize this method?

I'm learning new tricks all the time and I'm always on the lookout for better ideas.
I have this rather ugly method. How would you clean it up?
def self.likesit(user_id, params)
game_id = params[:game_id]
videolink_id = params[:videolink_id]
like_type = params[:like_type]
return false if like_type.nil?
if like_type == "videolink"
liked = Like.where(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink").first unless videolink_id.nil?
elsif like_type == "game"
liked = Like.where(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game").first unless game_id.nil?
end
if liked.present?
liked.amount = 1
liked.save
return true
else # not voted on before...create Like record
if like_type == "videolink"
Like.create(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink", :amount => 1)
elsif like_type == "game"
Like.create(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game", :amount => 1)
end
return true
end
return false
end
I would do something like:
class User < ActiveRecord::Base
has_many :likes, :dependent => :destroy
def likes_the(obj)
like = likes.find_or_initialize_by_likeable_type_and_likeable_id(obj.class.name, obj.id)
like.amount += 1
like.save
end
end
User.first.likes_the(VideoLink.first)
First, I think its wrong to deal with the "params" hash on the model level. To me its a red flag when you pass the entire params hash to a model. Thats in the scope of your controllers, your models should have no knowledge of the structure of your params hash, imo.
Second, I think its always cleaner to use objects when possible instead of class methods. What you are doing deals with an object, no reason to perform this on the class level. And finding the objects should be trivial in your controllers. After all this is the purpose of the controllers. To glue everything together.
Finally, eliminate all of the "return false" and "return true" madness. The save method takes care of that. The last "return false" in your method will never be called, because the if else clause above prevents it. In my opinion you should rarely be calling "return" in ruby, since ruby always returns the last evaluated line. In only use return if its at the very top of the method to handle an exception.
Hope this helps.
I'm not sure what the rest of your code looks like but you might consider this as a replacement:
def self.likesit(user_id, params)
return false unless params[:like_type]
query = {:user_id => user_id,
:likeable_id => eval("params[:#{params[:like_type]}_id]"),
:likeable_type => params[:like_type].capitalize}
if (liked = Like.where(query).first).present?
liked.amount = 1
liked.save
else # not voted on before...create Like record
Like.create(query.merge({:amount => 1}))
end
end
I assume liked.save and Like.create return true if they are succesful, otherwise nil is returned. And what about the unless game_id.nil? ? Do you really need that? If it's nil, it's nil and saved as nil. But you might as well check in your data model for nil's. (validations or something)

rails, `flash[:notice]` in my model?

I have a user model in which I have a method for seeing if the user has earned a "badge"
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1)
end
If they have earned a badge, the the award_badge method runs and gives the user the associated badge. Can I do something like this?
def check_if_badges_earned(user)
if user.recipes.count > 10
flash.now[:notice] = "you got a badge!"
award_badge(1)
end
Bonus Question! (lame, I know)
Where would the best place for me to keep all of these "conditions" for which my users could earn badges, similar to stackoverflows badges I suppose. I mean in terms of architecture, I already have badge and badgings models.
How can I organize the conditions in which they are earned? some of them are vary complex, like the user has logged in 100 times without commenting once. etc. so there doesn’t seem to be a simple place to put this sort of logic since it spans pretty much every model.
I'm sorry for you but the flash hash is not accessible in models, it gets created when the request is handled in your controller. You still can use implement your method storing the badge infos (flash message included) in a badge object that belongs to your users:
class Badge
# columns:
# t.string :name
# seed datas:
# Badge.create(:name => "Recipeador", :description => "Posted 10 recipes")
# Badge.create(:name => "Answering Machine", :description => "Answered 1k questions")
end
class User
#...
has_many :badges
def earn_badges
awards = []
awards << earn(Badge.find(:conditions => { :name => "Recipeador" })) if user.recipes.count > 10
awards << earn(Badge.find(:conditions => { :name => "Answering Machine" })) if user.answers.valids.count > 1000 # an example
# I would also change the finds with some id (constant) for speedup
awards
end
end
then:
class YourController
def your_action
#user = User.find(# the way you like)...
flash[:notice] = "You earned these badges: "+ #user.earn_badges.map(:&name).join(", ")
#...
end
end

Resources