An efficient way to track user login dates and IPs history - ruby-on-rails-3

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.

Related

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).

How to validate Rails Model attribute uniqueness over a virtual attribute scope

How can I validate uniqueness of an attribute with a custom or virtual scope? I thought of using a virtual attribute, but it keeps trying to query audit_year in the database. I would rather not create another database column just for the purpose of this uniqueness constraint.
Each location can only have one audit scheduled per year, so I need to extract the year from the scheduled attribute and validate uniqueness over that scope.
class Audit
attr_accessible :location_name, :scheduled_date, :completion_date ...
validates :location_name, :presence => true, :uniqueness => { :scope => :audit_year }
...
def audit_year
scheduled_date.year
end
end
I may not even be on the correct path with my virtual attribute attempts. What would be the "right" way to do this in rails?
I know this is a bit late, but I figured I'd pitch in. I'm doing this from memory so you may need to screw with this a bit.
My first thought is in your audit_year method, you could query the database for all years. Something like:
def audit_year
!self.class.select('selected_date').map{ |a| a.selected_date.year }.include?(selected_date.year)
# or using Rails 3.2 and Ruby 1.9.3:
# !self.class.pluck('created_at').map(&:year).include?(selected_date.year)
end
My assumption of the unique method is if it returns false, it will fail validation. This code selects just that one column from your class (I used self.class instead of Audit so it's more reusable), then maps out the years to an array. If it's included (true), return the opposite (!). You could probably optimize the query with uniq, but it depends on how large the table is whether it's necessary or not.
The other option would be to roll your own validator for the attribute, which really wouldn't be that difficult. The only difference is you'd add a line that conditionally checks for selected_date.present? in addition to the above. A great resource is the Rails Guides for callbacks and errors if you don't know how: http://guides.rubyonrails.org/active_record_validations_callbacks.html
Hope that helps.

Rails best practices - where should this code go?

There are a couple of places where I could do what I need, but I'm not sure where the best place is in line with good practices.
I have an Orders controller, and after a successful order is created I want to create a subscription (but only if the order is a success), and a referral (but only if the order is associated with one).
Now the obvious choice is to use after_create on the Order model... but... how can I get session data into that? (The referral ids, friend ids and voucher ids are only in the session as there's no need to store them in the Order db).
So should I just create the Subscription and Referral objects in the create action (how I have it at the mo) or is there a better way?
Here's my create action:
(#order.purchase only returns true if the payment was successful)
def create
if #order.save
if #order.purchase
Subscription.create(:order_id => #order.id, :product_id => #order.product_id)
if #order.voucher
Referral.create(:user_id => session[:friend_id], :order_id => #order.id,
:voucher_amount => #voucher_value)
end
render :action => "success"
else
render :action => "failure"
end
else
render :action => 'new'
end
end
Any help would be appreciated - I really would like to do this properly so I hope no one minds me asking what is probably a simple question.
I recently had a similar question, please take a look, i think that a simple virtual attribute in a callback will do it for you as well.
Fetch current user in after_create filter
use of callbacks will make your life easy, you need to use after_save
do all your stuff in after_save callback of order model. see rails api doc for callback here
Edit: If the session variable is not available with model, you can have a post_save method to deal with all logic which also accepts all require params like
like
class Order < ActiveRecord::Base
def post_save require_attr
#create subscriptions
# create referral
end
end

Rails ActiveRecord query

My question is twofold... Primarily, I am trying to figure out how to ask > or < when filtering this query. You can see at the end I have .where(:created_at > 2.months.ago) and that is improper syntax, but I'm not sure the correct way to call something similar.
Secondly, this is a bit of a long string and is going to get longer as the are more conditions I have to factor in. Is there a cleaner way of building this, or is a long string of conditions like this pretty standard?
class PhotosController < ApplicationController
def showcase
#photos = Photo.order(params[:sort] || 'random()').search(params[:search]).paginate(:per_page => 12, :page => params[:page]).where(:created_at > 2.months.ago)
end
Thanks.
Unfortunately you've hit a sore point in the ActiveRecord querying api. There is no standard, out of the box way to do this. You can do date ranges very easily, but < and > have no easy path. However Arel, the underlying SQL engine, can do this very easily. You could write a simple scope to handle it thusly:
scope :created_after, lambda {|date| where arel_table[:created_at].gt(date) }
And you could refactor this easily to take a column, or gt versus lt, etc.
Other people have solved this problem already, however, and you could take advantage of their work. One example is MetaWhere, which adds a bunch of syntactic sugar to your queries. For example, using it you might write:
Article.where(:title.matches => 'Hello%', :created_at.gt => 3.days.ago)
On #2, scopes do tend to get long. You might look into the gem has_scope, which helps to alleviate this by defining scopes on the controller in an analogous way to how they are defined on the model. An example from the site:
# The model
# Note it's using old Rails 2 named_scope, but Rails 3 scope works just as well.
class Graduation < ActiveRecord::Base
named_scope :featured, :conditions => { :featured => true }
named_scope :by_degree, proc {|degree| { :conditions => { :degree => degree } } }
end
# The controller
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
def index
#graduations = apply_scopes(Graduation).all
end
end
You can do where(["created_at > ?", 2.months.ago]) for your first question.
For your second question there are several solutions :
You can use scopes to embed the conditions in them and then combine them.
You can break the line in multiple lines.
You can keep it like this if you have a large screen and you don't work with any other people.

Do Rails 3 Active Record dynamic find_or_create finder methods have some undocumented inconsistencies?

Apologies for the long title, but this is bothering me. I'm new to Rails, so this is my first project. Rails 3.0.3.
In my model, a User may or may not have read many Entries; this is tracked in a model called ReadEntries. This many-to-one relationship is properly defined in the code, I think.
User.rb:
has_many :read_entries
Entry.rb:
has_many :read_entries
ReadEntry.rb:
belongs_to :entry
belongs_to :user
This table has to be populated at some point. If I try to do this:
user.read_entries.find_or_create_by_entry_id(entry.id, :read => false)
I get the error Unknown key(s): read. Leave out trying to set :read, and it works.
However, if I create the same row with this, it works:
ReadEntry.find_or_create_by_entry_id_and_user_id(entry.id, user.id, :read => false)
Logically, these methods should be identical, right? Thanks.
I've also had weird experiences with find_or_create. I would love it if it worked, but it seems inconsistent.
I'm currently having the same issue as you, and I think it may be due to calling find_or_create on an association as opposed to the model directly. Here's my example:
permission_assignments.find_or_create_by_role_id(:role_id => role_id, :is_allowed => false)
This works to create the assignment, except the "is_allowed" field gets set to it's default of "true". This code works for me (in the Permission model, hence the self reference)
PermissionAssignment.find_or_create_by_permission_id_and_role_id(:permission_id => self.id, :role_id => role_id, :is_allowed => false)
It's more verbose, unfortunately, but it works. The only problem that I still notice is that the object that is returned has no id assigned (the record does get created in the database, however, but if I wanted to update any more attributes I wouldn't be able to without the id). Don't know if that's a separate issue or not.
Rails 3.0.4 here with Postgres 8.4
You cannot pass in other fields like that as Rails will assume they are options for the find. Instead, you will need to make your method call longer:
user.read_entries.find_or_create_by_entry_id_and_read(entry.id, false)
Or alternatively use a shorter, custom syntax for that.
For your final example, my thoughts are that Rails will take the second argument and use that as options. Other than that, I am not sure.