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