Cancan Thinking Sphinx current_ability Questions - ruby-on-rails-3

trying to get cancan working with thinking sphinx but running into some issues.
Before using sphinx, I had this in my companies view:
#companies = Company.accessible_by(current_ability)
That prevented my users from seeing anyone else's companies...
After installing sphinx, I ended up with:
#companies = Company.accessible_by(current_ability).search(params[:search], :include => :order, :match_mode => :extended ).paginate(:page => params[:page])
Which now displays all my companies and isn't refining per user based on ability.
It would see ts isn't set up for cancan?

I think it's more that accessible_by is probably a scope - which is Database/SQL-driven. Sphinx has its own query interface, and so ActiveRecord scopes don't apply.
An inefficient workaround (gets all companies first):
company_ids = Company.accessible_by(current_ability).collect &:id
#companies = Company.search params[:search],
:include => :order,
:match_mode => :extended,
:page => params[:page],
:with => {:sphinx_internal_id => company_ids}
A couple of things to note: sphinx_internal_id is the indexed model's primary key - Sphinx has its own unique identifier named id, hence the distinction. Also: You don't want to call paginate on a search collection - Sphinx always paginates, so just pass the :page param through to the search call.
There'd be two better workarounds that I can think of - either have a Sphinx equivalent of accessible_by, with the relevant information added to your indices as attributes - or, simpler if not quite as ideal, just get the company ids returned in the first line of my above snippet without loading up every company as an ActiveRecord object. Both will probably mean bypassing and/or duplicating Cancan's helpers.
Although... maybe this would do the trick, taking the latter approach:
sql = Company.accessible_by(current_ability).select(:id).to_sql
company_ids = Company.connection.select_values sql
#companies = Company.search params[:search],
:include => :order,
:match_mode => :extended,
:page => params[:page],
:with => {:sphinx_internal_id => company_ids}
Avoids loading unnecessary Company objects, uses the Cancan helper (provided it is/returns a scope), and works neatly with what Sphinx/Thinking Sphinx expects. I've not used Cancan though, so this is a bit of guesswork.

Related

Making this ActiveRecord lookups more efficient

I want to get an array of all email addresses for users of certain service types.
Using a string of ActiveRecord has_many relations, I can get these like this:
affected_services = Service.where(service_type: 'black')
affected_accounts = affected_services.map {|s| s.account}
affected_emails = affected_accounts.map {|a| a.contact.email}
I know it would be a fairly simple SQL query, but I'd prefer to see if ActiveRecord can do it to keep database abstraction.
Is there a good ActiveRecord way to retrieve those results?
You could use :include to include children in the query.
Account.find(:all, :include => :contact, :conditions => {:service_id => Service.where(:service_type => 'black').map{|account| account.contact.email } })

Sphinx and friend search

I have a high-level question about Sphinx on Ruby on Rails:
Say I want to search are the articles written by my friends.
The model structure would be:
User has_many users :through relationships
Article belongs_to User
My question is: what sort of syntax would I use on Sphinx so the user searches for articles and gets only the articles written by his friends? I am having trouble finding this online and I'd like to have a handle on how this would work before I implement my solution.
NOTE: I suspect one solution is to have an array of friend IDs and then use a :condition :with => {:id => array_of_friendIDs}. But maybe there is a more effective way of doing it?
You're basically correct - you're going to need to assemble an array of friend ids and pass it to the search, using the :with option. How you fetch that list isn't particularly related to Sphinx, though.
friend_ids = current_user.users.pluck(:id)
#articles = Article.search(params['search_term'], :with => {:user_id => friend_ids})
Using .pluck will save you a good bit of time that would otherwise be devoted to instantiating a bunch of User objects - you only need their ids. Make sure that you've set user_id as an attribute for Sphinx (using has user_id in the define_index block).

An efficient way to track user login dates and IPs history

I'm trying to track user login history for stat purposes but its not clear to me what the best way to go about it would be. I could have a separate table that records users and their login stats with a date, but that table could get REALLY big. I could track some historic fields in the User model/object itself in a parse-able field and just update it (them) with some delimited string format. e.g. split on :, get the last one, if an included date code isn't today, add an item (date+count) otherwise increment, then save it back. At least with this second approach it would be easy to remove old items (e.g. only keep 30 days of daily logins, or IPs), as a separate table would require a task to delete old records.
I'm a big fan of instant changes. Tasks are useful, but can complicate things for maintenance reasons.
Anyone have any suggestions? I don't have an external data caching solution up or anything yet. Any pointers are also welcome! (I've been hunting for similar questions and answers)
Thanks!
If you have the :trackable module, I found this the easiest way. In the User model (or whichever model you're authenticating)
def update_tracked_fields!(request)
old_signin = self.last_sign_in_at
super
if self.last_sign_in_at != old_signin
Audit.create :user => self, :action => "login", :ip => self.last_sign_in_ip
end
end
(Inspired by https://github.com/plataformatec/devise/wiki/How-To:-Turn-off-trackable-for-admin-users)
There is a nice way to do that through Devise.
Warden sets up a hook called after_set_user that runs after setting a user. So, supposed you have a model Login containing an ip field, a logged_in_at field and user_id field, you can only create the record using this fields.
Warden::Manager.after_set_user :except => :fetch do |record, warden, options|
Login.create!(:ip => warden.request.ip, :logged_in_at => Time.now, :user_id => record.id)
end
Building upon #user208769's answer, the core Devise::Models::Trackable#update_tracked_fields! method now calls a helper method named update_tracked_fields prior to saving. That means you can use ActiveRecord::Dirty helpers to make it a little simpler:
def update_tracked_fields(request)
super
if last_sign_in_at_changed?
Audit.create(user: self, action: 'login', ip: last_sign_in_ip)
end
end
This can be simplified even further (and be more reliable given validations) if audits is a relationship on your model:
def update_tracked_fields(request)
super
audits.build(action: 'login', ip: last_sign_in_ip) if last_sign_in_at_changed?
end
Devise supports tracking the last signed in date and the last signed in ip address with it's :trackable module. By adding this module to your user model, and then also adding the correct fields to your database, which are:
:sign_in_count, :type => Integer, :default => 0
:current_sign_in_at, :type => Time
:last_sign_in_at, :type => Time
:current_sign_in_ip, :type => String
:last_sign_in_ip, :type => String
You could then override the Devise::SessionsController and it's create action to then save the :last_sign_in_at and :last_sign_in_ip to a separate table in a before_create callback. You should then be able to keep them as long you would like.
Here's an example (scribd_analytics)
create_table 'page_views' do |t|
t.column 'user_id', :integer
t.column 'request_url', :string, :limit => 200
t.column 'session', :string, :limit => 32
t.column 'ip_address', :string, :limit => 16
t.column 'referer', :string, :limit => 200
t.column 'user_agent', :string, :limit => 200
t.column 'created_at', :timestamp
end
Add a whole bunch of indexes, depending on queries
Create a PageView on every request
We used a hand-built SQL query to take out the ActiveRecord overhead on
this
Might try MySQL's 'insert delayed´
Analytics queries are usually hand-coded SQL
Use 'explain select´ to make sure MySQL isusing the indexes you expect
Scales pretty well
BUT analytics queries expensive, can clog upmain DB server
Our solution:
use two DB servers in a master/slave setup
move all the analytics queries to the slave
http://www.scribd.com/doc/49575/Scaling-Rails-Presentation-From-Scribd-Launch
Another option to check is Gattica with Google Analytics
I hate answering my own questions, especially given that you both gave helpful answers. I think answering my question with the approach I took might help others, in combination with your answers.
I've been playing with the Impressionist Gem (the only useful page view Gem since the abandoned RailStat) with good results so far. After setting up the basic migration, I found that the expected usage follows Rail's MVC design very closely. If you add "impressionist" to a Controller, it will go looking for the Model when logging the page view to the database. You can modify this behaviour or just call impressionist yourself in your Controller (or anywhere really) if you're like me and happen to be testing it out on a Controller that doesn't have a Model.
Anyways, I got it working with Devise to track successful logins by overriding the Devise::SessionsController and just calling the impressionist method for the #current_member: (don't forget to check if it's nil! on failed login)
class TestSessionController < Devise::SessionsController
def create
if not #current_member.nil?
impressionist(#current_member)
end
super
end
end
Adding it to other site parts later for some limited analytics is easy to do. The only other thing I had to do was update my routes to use the new TestSessionController for the Devise login route:
post 'login' => 'test_session#create', :as => :member_session
Devise works like normal without having to modify Devise in anyway, and my impressionist DB table is indexed and logging logins. I'll just need a rake task later to trim it weekly or so.
Now I just need to work out how to chart daily logins without having to write a bunch of looping, dirty queries...
There is also 'paper_trail' gem, that allows to track model changes.

Thinking sphinx not indexing belongs_to relationship properly in production but works fine in dev

I want to display the movies associated to a given author. On the author's page, I want users to be able to paginate, sort, and filter by keyword.
Everything works fine on my local machine. But, in production, the list of movies on the author's page is empty, and I can't figure out why. In production, in the console, I tested the following expressions, but no luck (always returns 0, while it returns values > 0 in dev):
ruby-1.9.2-p290 :042 > Movie.search(:with => {:author_ids => [6]}).count
=> 0
ruby-1.9.2-p290 :043 > Movie.search(:with => {:author_ids => 6}).count
=> 0
The weird thing is that I'm using a very similar code to display the movies associated to a topic on a topic's page, and it works great in development AND in production. For instance:
ruby-1.9.2-p290 :051 > Movie.search(:with => {:topic_ids => 2}, :per_page => 1000).count
=> 295
Here is how I define my Movie class:
class Movie < ActiveRecord::Base
belongs_to :author
has_many :topics
...
define_index('movie') do
...
has author(:id), :as => :author_ids,
:facet => true
has topics(:id), :as => :topic_ids,
:facet => true
...
end
...
end
And here is what my Author show controller looks like:
def show
#author = Author.find(params[:id])
keywords = params[:what] || ""
with_params[:author_ids] = [#author.id]
#movies = Movie.search(
keywords,
:with => with_params
)
end
This leads me to believe there is something wrong with the Sphinx index in production, but I'm not sure how to investigate further to find the root of the problem...
UPDATE: Following Pat's suggestion, I updated Sphinx and everything was solved (I upgraded from 0.9.8 to 0.9.10)! I was confused because Sphinx is NOT a Gem (even though a Sphinx gem exists)... So I had to go through the regular download, make, make install process.
I'll start with the obvious, but maybe this has already been tried - is the author_ids attribute something relatively new? Have you rebuilt (indexed and restarted) Sphinx since adding that attribute? rake ts:rebuild is the easy way to do that.
Update: It turns out updating Sphinx was the fix here - Alex can confirm which version, but I'm guessing 0.9.9 or better should do the trick.

Should I drop a polymorphic association?

My code is still just in development, not production, and I'm hitting a wall with generating data that I want for some views.
Without burying you guys in details, I basically want to navigate through multiple model associations to get some information at each level. The one association giving me problems is a polymorphic belongs_to. Here are the most relevant associations
Model Post
belongs_to :subtopic
has_many :flags, :as => :flaggable
Model Subtopic
has_many :flags, :as => :flaggable
Model Flag
belongs_to :flaggable, :polymorphic => true
I'd like to display multiple flags in a Flags#index view. There's information from other models that I want to display, as well, but I'm leaving out the specifics here to keep this simpler.
In my Flags_controller#index action, I'm currently using #flags = paginate_by_sql to pull everything I want from the database. I can successfully get the data, but I can't get the associated model objects eager-loaded (though the data I want is all in memory). I'm looking at a few options now:
rewrite my views to work on the SQL data in the #flags object. This should work and will prevent the 5-6 association-model-SQL queries per row on the index page, but will look very hackish. I'd like to avoid this if possible
simplify my views and create additional pages for the more detailed information, to be loaded only when viewing one individual flag
change the model hierarchy/definitions away from polymorphic associations to inheritance. Effectively make a module or class FlaggableObject that would be the parent of both Subtopic and Post.
I'm leaning towards the third option, but I'm not certain that I'll be able to cleanly pull all the information I want using Rails' ActiveRecord helpers only.
I would like insight on whether this would work and, more importantly, if you you have a better solution
EDIT: Some nit-picky include behavior I've encountered
#flags = Flag.find(:all,:conditions=> "flaggable_type = 'Post'", :include => [{:flaggable=>[:user,{:subtopic=>:category}]},:user]).paginate(:page => 1)
=> (valid response)
#flags = Flag.find(:all,:conditions=> ["flaggable_type = 'Post' AND
post.subtopic.category_id IN ?", [2,3,4,5]], :include => [{:flaggable=>
[:user, {:subtopic=>:category}]},:user]).paginate(:page => 1)
=> ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :flaggable
Don't drop the polymorphic association. Use includes(:association_name) to eager-load the associated objects. paginate_by_sql won't work, but paginate will.
#flags = Flag.includes(:flaggable).paginate(:page => 1)
It will do exactly what you want, using one query from each table.
See A Guide to Active Record Associations. You may see older examples using the :include option, but the includes method is the new interface in Rails 3.0 and 3.1.
Update from original poster:
If you're getting this error: Can not eagerly load the polymorphic association :flaggable, try something like the following:
Flag.where("flaggable_type = 'Post'").includes([{:flaggable=>[:user, {:subtopic=>:category}]}, :user]).paginate(:page => 1)
See comments for more details.
Issues: Count over a polymorphic association.
#flags = Flag.find(:all,:conditions => ["flaggable_type = 'Post' AND post.subtopic.category_id IN ?",
[2,3,4,5]], :include => [{:flaggable => [:user, {:subtopic=>:category}]},:user])
.paginate(:page => 1)
Try like the following:
#flags = Flag.find(:all,:conditions => ["flaggable_type = 'Post' AND post.subtopic.category_id IN ?",
[2,3,4,5]], :include => [{:flaggable => [:user, {:subtopic=>:category}]},:user])
.paginate(:page => 1, :total_entries => Flag.count(:conditions =>
["flaggable_type = 'Post' AND post.subtopic.category_id IN ?", [2,3,4,5]]))