Codeminer42/cm42-central

View on GitHub
app/models/activity.rb

Summary

Maintainability
A
1 hr
Test Coverage
class Activity < ApplicationRecord
  belongs_to :project
  belongs_to :user
  belongs_to :subject, polymorphic: true

  validates :action, presence: true, inclusion: { in: %w[create update destroy] }
  validates :project, presence: true
  validates :user, presence: true
  validates :subject, presence: true, subject_changed: true

  serialize :subject_changes, Hash

  before_save :parse_changes

  scope :projects, lambda { |ids|
    where(project_id: ids) if ids
  }
  scope :since, lambda { |date|
    where('created_at > ?', date.beginning_of_day) if date
  }
  scope :by_story, ->(story_id) { where(subject_id: story_id, subject_type: 'Story') }

  def decorate
    ActivityPresenter.new(self)
  end

  def self.fetch_polymorphic(ids, since)
    stories = where("subject_type in ('Project', 'Story')")
      .includes(:user, :subject)
      .projects(ids)
      .since(since).to_a
    stories += where("subject_type in ('Note', 'Task')")
      .includes(:user, subject: [:story])
      .projects(ids)
      .since(since).to_a
    stories = stories.group_by(&:subject_id)

    [].tap do |new_activities|
      stories.values.each do |activities|
        sub_list = activities.sort_by(&:updated_at)
        first_story = sub_list.shift
        first_story.merge_story!(sub_list.shift) until sub_list.empty?
        new_activities << first_story unless first_story.subject_changes.empty?
      end
    end
  end

  def self.grouped_activities(allowed_projects, since)
    fetch_polymorphic(allowed_projects.pluck(:id), since).group_by do |activity|
      activity.created_at.beginning_of_day
    end.map do |date, activities|
      [
        date,
        activities.group_by(&:project_id).map do |project_id, activities|
          [
            allowed_projects.find { |p| p.id == project_id },
            activities.group_by do |activity|
              activity.subject_destroyed_type || activity.subject_type
            end.map do |subject_type, activities|
              [
                subject_type,
                activities.map(&:decorate)
              ]
            end
          ]
        end
      ]
    end
  end

  def merge_story!(activity)
    return if subject_type != 'Story' # story only
    return if subject_id != activity.subject_id # only merge the exact same story change
    return if updated_at > activity.updated_at # only merge from future changes
    return if activity.subject_changes.blank?

    self.subject_changes = {} if subject_changes.nil?
    self.action = activity.action

    activity.subject_changes.keys.each do |key|
      if subject_changes[key]
        subject_changes[key][-1] = activity.subject_changes[key][-1]
      else
        subject_changes[key] = activity.subject_changes[key]
      end
      subject_changes.delete(key) if subject_changes[key].first == subject_changes[key].last
    end
  end

  def parse_changes
    if action == 'update'
      self.subject_changes = subject.saved_changes
    elsif action == 'destroy'
      self.subject_changes = subject.attributes
      self.subject_destroyed_type = subject.class.name
      self.subject = nil
    end
  end
end