chaps-io/public_activity

View on GitHub
lib/public_activity/common.rb

Summary

Maintainability
A
0 mins
Test Coverage
module PublicActivity
  # Happens when creating custom activities without either action or a key.
  class NoKeyProvided < Exception; end

  # Used to smartly transform value from metadata to data.
  # Accepts Symbols, which it will send against context.
  # Accepts Procs, which it will execute with controller and context.
  # @since 0.4.0
  def self.resolve_value(context, thing)
    case thing
    when Symbol
      context.__send__(thing)
    when Proc
      thing.call(PublicActivity.get_controller, context)
    when Hash
      thing.dup.tap do |hash|
        hash.each do |key, value|
          hash[key] = PublicActivity.resolve_value(context, value)
        end
      end
    else
      thing
    end
  end

  # Common methods shared across the gem.
  module Common
    extend ActiveSupport::Concern

    included do
      include Trackable
      class_attribute :activity_owner_global, :activity_recipient_global,
                      :activity_parameters_global, :activity_hooks, :activity_custom_fields_global
      set_public_activity_class_defaults
    end

    # @!group Global options

    # @!attribute activity_owner_global
    #   Global version of activity owner
    #   @see #activity_owner
    #   @return [Model]

    # @!attribute activity_recipient_global
    #   Global version of activity recipient
    #   @see #activity_recipient
    #   @return [Model]

    # @!attribute activity_parameters_global
    #   Global version of activity parameters
    #   @see #activity_parameters
    #   @return [Hash<Symbol, Object>]

    # @!attribute activity_hooks
    #   @return [Hash<Symbol, Proc>]
    #   Hooks/functions that will be used to decide *if* the activity should get
    #   created.
    #
    #   The supported keys are:
    #   * :create
    #   * :update
    #   * :destroy

    # @!visibility private
    @@activity_hooks = {}

    # @!endgroup

    # Provides some global methods for every model class.
    module ClassMethods
      #
      # @since 1.0.0
      # @api private
      def set_public_activity_class_defaults
        self.activity_owner_global             = nil
        self.activity_recipient_global         = nil
        self.activity_parameters_global        = {}
        self.activity_hooks                    = {}
        self.activity_custom_fields_global     = {}
      end

      # Extracts a hook from the _:on_ option provided in
      # {Tracked::ClassMethods#tracked}. Returns nil when no hook exists for
      # given action
      # {Common#get_hook}
      #
      # @see Tracked#get_hook
      # @param key [String, Symbol] action to retrieve a hook for
      # @return [Proc, nil] callable hook or nil
      # @since 0.4.0
      # @api private
      def get_hook(key)
        key = key.to_sym
        if self.activity_hooks.has_key?(key) and self.activity_hooks[key].is_a? Proc
          self.activity_hooks[key]
        else
          nil
        end
      end
    end
    #
    # Returns true if PublicActivity is enabled globally.
    # @note This method gets overwritten in {Deactivatable#public_activity_enabled?}
    # @return [Boolean]
    # @api public
    # @since 0.5.0
    # @see {Deactivatable#public_activity_enabled?}
    def public_activity_enabled?
      PublicActivity.enabled?
    end

    # Calls hook safely.
    # If a hook for given action exists, calls it with model (self) and
    # controller (if available, see {StoreController})
    # @param key (see #get_hook)
    # @return [Boolean] if hook exists, it's decision, if there's no hook, true
    # @since 0.4.0
    # @api private
    def call_hook_safe(key)
      hook = self.class.get_hook(key)
      if hook
        # provides hook with model and controller
        hook.call(self, PublicActivity.get_controller)
      else
        true
      end
    end

    # Records activity in the database, based on supplied options and configuration in
    # {Tracked}.
    #
    # If {Tracked} is used and configured for this model, `create_activity`
    # will also gather data as defined in {Tracked}. If any parameters passed here
    # conflict with options defined in {Tracked}, they get precedence over options
    # defined in {Tracked::ClassMethods#tracked}.
    #
    # Whether or not {Tracked} is used, you can provide objects, symbols and procs
    # as values for all parameters of activity. See {PublicActivity.resolve_value} for available
    # value types.
    #
    # If {Tracked} is used and hooks are provided, they will be called upon to decide
    # if this method should really record an activity. To discard defined hooks and create
    # the activity unconditionally, use {PublicActivity::Activity} directly.
    #
    # # Examples
    #     current_user.create_activity(:avatar_changed)
    #     @article.create_activity(action: :commented_on, :owner => current_user)
    #     @post.create_activity(key: 'blog_post.published', parameters: {words_count: 50})
    #
    # # Activity Key
    # The key will be generated from either:
    #
    #  * the first parameter you pass that is not a hash (`action`)
    #  * the _:action_ option in the options hash (`action`)
    #  * the _:key_ option in the options hash ( **full key** )
    #
    #  -------------------
    #  When you pass an *action* (first two options above), they will be
    #  added to parameterized model name:
    #
    # Example:
    #
    #     @article.create_activity :commented_on               #=> #<Activity key: 'article.commented_on' ...>
    #     @article.create_activity action: :commented_on       #=> #<Activity key: 'article.commented_on' ...>
    #     # note the prefix when passing in `key`
    #     @article.create_activity key: 'article.commented_on' #=> #<Activity key: 'article.commented_on' ...>
    # # Options
    # Besides `:action` and `:key` covered above, you can pass options
    # such as `:owner`, `:parameters`, `:recipient`. In addition, if you've configured any
    # *custom fields*, you can pass them in here too.
    #
    # ## Example
    #
    #     @article.create_activity :commented_on, weather_outside: :sunny
    #
    # @note This method won't create the activity if hooks reject creation.
    # @note Options passed in to this method will take precedence over defaults
    #       configured in {Tracked}.
    #
    # @return [PublicActivity::Activity, nil] If created successfully, returns new activity
    # @since 0.4.0
    # @api public
    # @overload create_activity(action, options = {})
    #   @param [Symbol,String] action Name of the action, will be prefixed
    #   @param [Hash] options Options with quality higher than instance options
    #     set in {Tracked#activity}
    #   @option options [Activist] :owner Owner
    #   @option options [Activist] :recipient Recipient
    #   @option options [Hash] :parameters Parameters, see
    #     {PublicActivity.resolve_value}
    # @overload create_activity(options = {})
    #   @param [Hash] options Options with quality higher than instance options
    #     set in {Tracked#activity}
    #   @option options [Symbol,String] :action Name of the action, will be prefixed
    #   @option options [String] :key Full key, won't be prefixed
    #   @option options [Activist] :owner Owner
    #   @option options [Activist] :recipient Recipient
    #   @option options [Hash] :parameters Parameters, see
    #     {PublicActivity.resolve_value}
    def create_activity(*args)
      return unless self.public_activity_enabled?
      options = prepare_settings(*args)

      if call_hook_safe(options[:key].split('.').last)
        return PublicActivity::Adapter.create_activity(self, options)
      end

      nil
    end

    # Prepares settings used during creation of Activity record.
    # parameters passed directly to tracked model have priority over
    # settings specified in tracked() method
    #
    # @see #create_activity
    # @return [Hash] Settings with preserved options that were passed
    # @api private
    # @overload prepare_settings(action, options = {})
    #   @see #create_activity
    # @overload prepare_settings(options = {})
    #   @see #create_activity
    def prepare_settings(*args)
      raw_options = args.extract_options!
      action      = [args.first, raw_options.delete(:action)].compact.first
      key         = prepare_key(action, raw_options)

      raise NoKeyProvided, "No key provided for #{self.class.name}" unless key

      prepare_custom_fields(raw_options.except(:parameters, :params)).merge(
        {
          key:        key,
          owner:      prepare_relation(:owner,     raw_options),
          recipient:  prepare_relation(:recipient, raw_options),
          parameters: prepare_parameters(raw_options.delete(:parameters)),
        }
      )
    end

    # Prepares and resolves custom fields
    # users can pass to `tracked` method
    # @api private
    def prepare_custom_fields(options)
      customs = self.class.activity_custom_fields_global.clone
      customs.merge!(options)
      customs.each do  |k, v|
        customs[k] = PublicActivity.resolve_value(self, v)
      end
    end

    # Prepares i18n parameters that will
    # be serialized into the Activity#parameters column.
    # If a Symbol or a Proc is passed, it will be resolved
    # expecting to return a Hash.
    # @api private
    def prepare_parameters(parameters)
      parameters ||= {}

      [self.class.activity_parameters_global, parameters].reduce({}) do |params, value|
        params.merge!(PublicActivity.resolve_value(self, value))
      end
    end

    # Prepares relation to be saved
    # to Activity. Can be :recipient or :owner
    # @api private
    def prepare_relation(name, options)
      PublicActivity.resolve_value(self,
        (options.has_key?(name) ? options[name] : self.class.send("activity_#{name}_global"))
      )
    end

    # Helper method to serialize class name into relevant key
    # @return [String] the resulted key
    # @param [Symbol | String] action the name of the operation to be done on class
    # @param [Hash] options to be used on key generation, defaults to {}
    # @api private
    def prepare_key(action, options = {})
      (
        options[:key] ||
        ((self.class.name.underscore.gsub('/', '_') + "." + action.to_s) if action)
      ).try(:to_s)
    end
  end
end