simple hit counter for page views in rails - ruby-on-rails

I've found several solutions for this problem, for example railstat from this post:
Page views in Rails
I have a bunch of articles and reviews which I would like a hit counter filtered by unique IPs. Exactly like Stackoverflow does for this post. But I don't really care for such a solution as railstat when google analytics is already doing this for me and including a whole lot of code, keeping track of unique IPs, etc.. My present thinking is to use Garb or some other Analytics plugin to pull the pages stats if they are older than say 12 hours updating some table, but I also need a cache_column.
I'm assuming you can pull stats from Analytics for a particular page and that they update their stats every 12 hours?
I'm wondering if there are any reasons why this would be a bad idea, or if someone has a better solution?
Thanks

UPDATE
The code in this answer was used as a basis for http://github.com/charlotte-ruby/impressionist
Try it out
It would probably take you less time to code this into your app then it would to pull the data from Analytics using their API. This data would most likely be more accurate and you would not have to rely an an external dependancy.. also you would have the stats in realtime instead of waiting 12 hours on Analytics data. request.remote_ip works pretty well. Here is a solution using polymorphism. Please note that this code is untested, but it should be close.
Create a new model/migration to store your page views (impressions):
class Impressions < ActiveRecord::Base
belongs_to :impressionable, :polymorphic=>true
end
class CreateImpressionsTable < ActiveRecord::Migration
def self.up
create_table :impressions, :force => true do |t|
t.string :impressionable_type
t.integer :impressionable_id
t.integer :user_id
t.string :ip_address
t.timestamps
end
end
def self.down
drop_table :impressions
end
end
Add a line to your Article model for the association and add a method to return the impression count:
class Article < ActiveRecord::Base
has_many :impressions, :as=>:impressionable
def impression_count
impressions.size
end
def unique_impression_count
# impressions.group(:ip_address).size gives => {'127.0.0.1'=>9, '0.0.0.0'=>1}
# so getting keys from the hash and calculating the number of keys
impressions.group(:ip_address).size.keys.length #TESTED
end
end
Create a before_filter for articles_controller on the show action:
before_filter :log_impression, :only=> [:show]
def log_impression
#article = Article.find(params[:id])
# this assumes you have a current_user method in your authentication system
#article.impressions.create(ip_address: request.remote_ip,user_id:current_user.id)
end
Then you just call the unique_impression_count in your view
<%=#article.unique_impression_count %>
If you are using this on a bunch of models, you may want to DRY it up. Put the before_filter def in application_controller and use something dynamic like:
impressionable_class = controller_name.gsub("Controller","").constantize
impressionable_instance = impressionable_class.find(params[:id])
impressionable_instance.impressions.create(ip_address:request.remote_ip,user_id:current_user.id)
And also move the code in the Article model to a module that will be included in ActiveRecord::Base. You could put the send include in a config/initializer.. or if you want to get crazy, just turn the whole thing into a rails engine, so you can reuse on other apps.
module Impressionable
def is_impressionable
has_many :impressions, :as=>:impressionable
include InstanceMethods
end
module InstanceMethods
def impression_count
impressions.size
end
def unique_impression_count
impressions.group(:ip_address).size
end
end
end
ActiveRecord::Base.extend Impressionable

Related

What is the best way to store a multi-dimensional counter_cache?

In my application I have a search_volume.rb model that looks like this:
search_volume.rb:
class SearchVolume < ApplicationRecord
# t.integer "keyword_id"
# t.integer "search_engine_id"
# t.date "date"
# t.integer "volume"
belongs_to :keyword
belongs_to :search_engine
end
keyword.rb:
class Keyword < ApplicationRecord
has_and_belongs_to_many :labels
has_many :search_volumes
end
search_engine.rb:
class SearchEngine < ApplicationRecord
belongs_to :country
belongs_to :language
end
label.rb:
class Label < ApplicationRecord
has_and_belongs_to_many :keywords
has_many :search_volumes, through: :keywords
end
On the label#index page I am trying to show the sum of search_volumes for the keywords in each label for the last month for the search_engine that the user has cookied. I am able to do this with the following:
<% #labels.each do |label| %>
<%= number_with_delimiter(label.search_volumes.where(search_engine_id: cookies[:search_engine_id]).where(date: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).sum(:volume)) %>
<% end %>
This works well but I have the feeling that the above is very inefficient. With the current approach I also dind it difficult to do operations on search volumes. Most of the time I just want to know about last month's search volume.
Normally I would create a counter_cache on the keywords model to keep track of the latest search_volume, but since there are dozens of search_engines I would have to create one for each, which is also inefficient.
What's the most efficient way to store last month's search volume for all the different search engines separately?
First of all, you can optimize your current implementation by doing one single request for all involved labels like so:
# models
class SearchVolume < ApplicationRecord
# ...
# the best place for your filters!
scope :last_month, -> { where(date: 1.month.ago.beginning_of_month..1.month.ago.end_of_month) }
scope :search_engine, ->(search_engine_id) { where(search_engine_id: search_engine_id) }
end
class Label < ApplicationRecord
# ...
# returns { label_id1 => search_volumn_sum1, label_id2 => search_volumn_sum2, ... }
def self.last_month_search_volumes_per_label_report(labels, search_engine_id:)
labels.
group(:id).
left_outer_joins(:search_volumes).
merge(SearchVolume.last_month.search_engine(search_engine_id)).
pluck(:id, 'SUM(search_volumes.volume)').
to_h
end
end
# controller
class LabelsController < ApplicationController
def index
#labels = Label.all
#search_volumes_report =
Label.last_month_search_volumes_per_label_report(
#labels, search_engine_id: cookies[:search_engine_id]
)
end
end
# view
<% #labels.each do |label| %>
<%= number_with_delimiter(#search_volumes_report[label.id]) %>
<% end %>
Please note that I have not tested it with the same architecture, but with similar models I have on my local machine. It may work by adjusting a few things.
My proposed approach still is live requesting the database. If you really need to store values somewhere because you have very large datasets, I suggest two solutions:
- using materialized views you could refresh each month (scenic gem offers a good way to handle views in Rails application: https://github.com/scenic-views/scenic)
- implementing a new table with standard relations between models, that could store your calculations by ids and months and whose you could populate each month using rake tasks, then you would simply have to eager load your calculations
Please let me know your feedbacks!

In Rails using devise: How can I find out if a user has logged out from a different browser?

Currently, I can tell if a user has singed in and how long it has been online, but I cannot find out if a user has signed out from a different browser when. Here is the current code I have so far:
- if current_user
= "(last login #{time_ago_in_words(user.current_sign_in_at)} ago) - #{user.current_sign_in_at ? 'currently online' : ''}"
- else
= "(last login #{time_ago_in_words(user.current_sign_in_at)} ago)"
As far as I know, there is nothing specific for this in Devise. You can track a particular user's session by storing a unique token specific to that user in the database.
Create a migration to add the field for storing the token.
Check the token for every logout and you can save the timestamp.
I think this way, you know when user has logged out from which browser.
Hope it helps!
You can try
Store a timestamp in your session so that you can timeout a session after a certain period of inactivity. If you use Devise, you can achieve this easily with the timeoutable module. This presents a slight conundrum: if you set the timeout too low, then it’ll be pretty annoying for your users. But the longer the timeout, the less useful it is for security (if the attacker gets in before hitting the timeout, they can just keep refreshing to keep their session active).
Track active session ids on the server side (whilst still storing the actual session data in cookies). This means you can invalidate sessions whenever you want.
add a SessionActivation model to track which sessions are active. Here’s the migration:
class AddUserActiveSessions < ActiveRecord::Migration
def change
create_table :session_activations do |t|
t.integer :user_id, null: false
t.string :session_id, null: false
t.timestamps
end
add_index :session_activations, :user_id
add_index :session_activations, :session_id, unique: true
end
end
Here’s the class:
class SessionActivation < ActiveRecord::Base
LIMIT = 20
def self.active?(id)
id && where(session_id: id).exists?
end
def self.activate(id)
activation = create!(session_id: id)
purge_old
activation
end
def self.deactivate(id)
return unless id
where(session_id: id).delete_all
end
# For some reason using #delete_all causes the order/offset to be ignored
def self.purge_old
order("created_at desc").offset(LIMIT).destroy_all
end
def self.exclusive(id)
where("session_id != ?", id).delete_all
end
end
Make some changes to the User model:
class User < ActiveRecord::Base
# ...
has_many :session_activations, dependent: :destroy
def activate_session
session_activations.activate(SecureRandom.hex).session_id
end
def exclusive_session(id)
session_activations.exclusive(id)
end
def session_active?(id)
session_activations.active? id
end
end

Rails replace collection instead of adding to it from a has_many nested attributes form

I have these models (simplified for readability):
class Place < ActiveRecord::Base
has_many :business_hours, dependent: :destroy
accepts_nested_attributes_for :business_hours
end
class BusinessHour < ActiveRecord::Base
belongs_to :place
end
And this controller:
class Admin::PlacesController < Admin::BaseController
def update
#place = Place.find(params[:id])
if #place.update_attributes(place_params)
# Redirect to OK page
else
# Show errors
end
end
private
def place_params
params.require(:place)
.permit(
business_hours_attributes: [:day_of_week, :opening_time, :closing_time]
)
end
end
I have a somewhat dynamic form which is rendered through javascript where the user can add new opening hours. When submitting these opening hours I would like to always replace the old ones (if they exist). Currently if I send the values via params (e.g.):
place[business_hours][0][day_of_week]: 1
place[business_hours][0][opening_time]: 10:00 am
place[business_hours][0][closing_time]: 5:00 pm
place[business_hours][1][day_of_week]: 2
place[business_hours][1][opening_time]: 10:00 am
place[business_hours][1][closing_time]: 5:00 pm
... and so forth
These new business hours get added to the existing ones. Is there a way to tell rails to always replace the business hours or do I manually have to empty the collection in the controller every time?
Bit optimizing the solution proposed #robertokl, to reduce the number of database queries:
def business_hours_attributes=(*args)
self.business_hours.clear
super(*args)
end
This is the best I could get:
def business_hours_attributes=(*attrs)
self.business_hours = []
super(*attrs)
end
Hopefully is not too late.
You miss id of business_hours try:
def place_params
params.require(:place)
.permit(
business_hours_attributes: [:id, :day_of_week, :opening_time, :closing_time]
)
end
That's why the form is adding a new record instead of updating it.

Rails sha1 hash generator

Hi hopefully somebody can help me out. I'm a bit stuck at the moment. I'm trying to create an app for a tracking system, I currently have a table called sdel_hashed. Following online videos I so far set up digest/sha1 to work partly. If I enter the following commands in the console:
sdel = Sdel.find(1)
sdel.hashed_sdel = Sdel.hash('secret')
sdel.save
And then view the record in the browser it show up as the hash and not secret, but if I try and enter the word secret through the new action it doesn't get hashed. I think there is maybe something missing in the create action but I cannot find answers anywhere. i would greatly appreciate any help. I'll include now what I have in my controller and model.
Thanks
model sdel
require 'digest/sha1'
class Sdel < ActiveRecord::Base
attr_accessible :hashed_sdel
def self.hash(sdel="")
Digest::SHA1.hexdigest(sdel)
end
end
controller sdels
class SdelsController < ApplicationController
def list
#sdel = Sdel.all
end
def new
#sdel = Sdel.new
end
def create
#sdel = Sdel.new(params[:sdel])
if #sdel.save
redirect_to(:action => 'list')
else
render('new')
end
end
end
Migration file
class CreateSdels < ActiveRecord::Migration
def change
create_table :sdels do |t|
t.string "hashed_sdel"
t.timestamps
end
end
end
Sounds like you may want to use a before_save filter to invoke the hash class method on the Sdel model prior to saving when the attribute has been modified. Perhaps something along the lines of this:
require 'digest/sha1'
class Sdel < ActiveRecord::Base
attr_accessible :hashed_sdel
before_save { self.hashed_sdel = self.class.hash(hashed_sdel) if hashed_sdel_changed? }
def self.hash(sdel="")
Digest::SHA1.hexdigest(sdel)
end
end
This way, if you have a form that has a text_field for your hashed_sdel attribute, it will automatically get run through the hash class method you have before it the record gets saved (assuming the attribute has changed from it's previous value).

Creating a Rails change log

I am pretty new to rails (and development) and have a requirement to create a change log. Let's say you have an employees table. On that table you have an employee reference number, a first name, and a last name. When either the first name or last name changes, I need to log it to a table somewhere for later reporting. I only need to log the change, so if employee ref 1 changes from Bill to Bob, then I need to put the reference number and first name into a table. The change table can have all the columns that mnight change, but most only be populated with the reference number and the changed field. I don't need the previous value either, just the new one. hope that makes sense.
Looked at gems such as paper trail, but they seem very complicated for what I need. I don't ever need to manipulate the model or move versions etc, I just need to track which fields have changed, when, and by whom.
I'd appreciate your recommendations.
If you insist on building your own changelog, based on your requirements you can do so using a few callbacks. First create your log table:
def up
create_table :employee_change_logs do |t|
t.references :employee
# as per your spec - copy all column definitions from your employees table
end
end
In your Employee model:
class Employee < ActiveRecord::Base
has_many :employee_change_logs
before_update :capture_changed_columns
after_update :log_changed_columns
# capture the changes before the update occurs
def capture_changed_columns
#changed_columns = changed
end
def log_changed_columns
return if #changed_columns.empty?
log_entry = employee_change_logs.build
#changed_columns.each{|c| log_entry.send(:"#{c}=", self.send(c))}
log_entry.save!
end
end
I recommend the gem vestal_versions.
To version an ActiveRecord model, simply add versioned to your class like so:
class User < ActiveRecord::Base
versioned
validates_presence_of :first_name, :last_name
def name
"#{first_name} #{last_name}"
end
end
And use like this:
#user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler")
#user.version # => 2
#user.versions.last.user # => "Tyler"
The first thing we did was put an around filter in the application controller. This was how I get the current_employee into the employee model, which was the challenge, especially for a newbie like me!
around_filter :set_employee_for_log, :if => Proc.new { #current_account &&
#current_account.log_employee_changes? && #current_employee }
def set_employee_for_log
Thread.current[:current_employee] = #current_employee.id
begin
yield
ensure
Thread.current[:current_employee ] = nil
end
end
end
Next, in the employee model I defined which fields I was interested in monitoring
CHECK_FIELDS = ['first_name', 'last_name', 'middle_name']
then I added some hooks to actually capture the changes IF logging is enabled at the account level
before_update :capture_changed_columns
after_update :log_changed_columns, :if => Proc.new { self.account.log_employee_changes? }
def capture_changed_columns
#changed_columns = changed
#changes = changes
end
def log_changed_columns
e = EmployeeChangeLog.new
Employee::CHECK_FIELDS.each do |field|
if self.send("#{field}_changed?")
e.send("#{field}=", self.send(field))
end
end
if e.changed?
e.update_attribute(:account_id, self.account.id)
e.update_attribute(:employee_id, self.id)
e.update_attribute(:employee_ref, self.employee_ref)
e.update_attribute(:user_id, Thread.current[:current_employee])
e.save
else return
end
end
And that;s it. If the account enables it, the app keeps an eye on specific fields and then all changes to those fields are logged to a table, creating an simple audit trail.

Resources