module SanteyView
module Viewable
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def santey_view(options = {})
# don't allow multiple calls
return if self.included_modules.include?(SanteyView::Viewable::ViewMethods)
include SanteyView::Viewable::ViewMethods
viewing_class = options[:viewing_class] || 'Viewing'
viewer_class = options[:viewer_class] || 'User'
unless Object.const_defined?(viewing_class)
Object.class_eval <<-EOV
class #{viewing_class} < ActiveRecord::Base
belongs_to :viewed, :polymorphic => true
end
EOV
if Object.const_defined?(viewer_class)
Object.class_eval <<-EOV
class #{viewing_class} < ActiveRecord::Base
belongs_to :viewer, :class_name => #{viewer_class}, :foreign_key => :viewer_id
end
EOV
end
end
write_inheritable_attribute( :santey_view_options ,
{ :viewing_class => viewing_class,
:viewer_class => viewer_class } )
class_inheritable_reader :santey_view_options
class_eval do
has_many :viewings, :as => :viewed, :dependent => :delete_all, :class_name => viewing_class.to_s
if Object.const_defined?(viewer_class)
has_many(:viewers, :through => :viewings, :class_name => viewer_class.to_s)
end
before_create :init_viewing_fields
end
# Add to the Viewer a has_many viewings
if Object.const_defined?(viewer_class)
viewer_as_class = viewer_class.constantize
return if viewer_as_class.instance_methods.include?('find_in_viewings')
viewer_as_class.class_eval <<-EOS
has_many :viewings, :foreign_key => :viewer_id, :class_name => #{viewing_class.to_s}
EOS
end
end
def generate_viewings_columns table
table.column :views, :integer
end
# Create the needed columns for santey_view.
# To be used during migration, but can also be used in other places.
def add_viewings_columns
if !self.content_columns.find { |c| 'views' == c.name }
self.connection.add_column table_name, :views, :integer, :default => '0'
self.reset_column_information
end
end
# Remove the santey_view specific columns added with add_viewings_columns
# To be used during migration, but can also be used in other places
def remove_viewings_columns
if self.content_columns.find { |c| 'views' == c.name }
self.connection.remove_column table_name, :views
self.reset_column_information
end
end
# Find all viewings for a specific viewer.
def find_viewed_by viewer
viewing_class = santey_view_options[:viewing_class].constantize
if !(santey_view_options[:viewer_class].constantize === viewer)
raise ViewedError, "The viewer object must be the one used when defining santey_view (or a descendent of it). other objects are not acceptable"
end
raise ViewedError, "Viewer must be a valid and existing object" if viewer.nil? || viewer.id.nil?
raise ViewedError, 'Viewer must be a valid viewer' if !viewing_class.column_names.include? "viewer_id"
viewed_class = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
conds = [ 'viewed_type = ? AND viewer_id = ?', viewed_class, viewer.id ]
santey_view_options[:viewing_class].constantize.find(:all, :conditions => conds).collect {|r| r.viewed_type.constantize.find_by_id r.viewed.id }
end
end
module ViewMethods
def self.included(base) #:nodoc:
base.extend ClassMethods
end
# Is this object viewed already?
def viewed?
return (!self.views.nil? && self.views > 0) if attributes.has_key? 'views'
!viewings.find(:first).nil?
end
# Get the number of viewings for this object based on the views field,
# or with a SQL query if the viewed objects doesn't have the views field
def view_count
return self.views || 0 if attributes.has_key? 'views'
viewings.count
end
# View the object with or without a viewer - create new or update as needed
#
# * ip - the viewer ip
# * viewer - an object of the viewer class. Must be valid and with an id to be used. Or nil
def view ip, viewer = nil
# Sanity checks for the parameters
viewing_class = santey_view_options[:viewing_class].constantize
if viewer && !(santey_view_options[:viewer_class].constantize === viewer)
raise ViewedError, "the viewer object must be the one used when defining santey_view (or a descendent of it). other objects are not acceptable"
end
viewing_class.transaction do
if !viewed_by? ip, viewer
view = viewing_class.new
view.viewer_id = viewer.id if viewer && !viewer.id.nil?
view.ip = ip
viewings << view
target = self if attributes.has_key? 'views'
target.views = ( (target.views || 0) + 1 ) if target
view.save
target.save_without_validation if target
return true
else
return false
end
end
end
# Check if an item was already viewed by the given viewer
def viewed_by? ip, viewer = nil
if viewer && !viewer.nil? && !(santey_view_options[:viewer_class].constantize === viewer)
raise ViewedError, "the viewer object must be the one used when defining santey_view (or a descendent of it). other objects are not acceptable"
end
if viewer && !viewer.id.nil?
return viewings.count(:conditions => ["viewer_id = '#{viewer.id}' or ip = '#{ip}'"]) > 0
else
return viewings.count(:conditions => ["ip = '#{ip}'"]) > 0
end
end
private
def init_viewing_fields #:nodoc:
if attributes.has_key? 'views'
self.views ||= 0
end
end
end
end
end