assemblymade/meta

View on GitHub
app/models/wip.rb

Summary

Maintainability
C
1 day
Test Coverage
# I'm thinking about this class more as a discussion. Which may also hold a bounty

require 'activerecord/uuid'
require 'elasticsearch/model'

class Wip < ActiveRecord::Base
  include ActiveRecord::UUID
  include Elasticsearch::Model
  include Kaminari::ActiveRecordModelExtension
  include Workflow

  belongs_to :closer, class_name: 'User'
  belongs_to :flagged_by, class_name: 'User'
  belongs_to :product, :touch => (update_parent_product_for_caching = true), counter_cache: true
  belongs_to :user

  has_one  :chat_room
  has_many :events
  has_many :followings, class_name: 'Watching', as: :watchable
  has_many :followers, through: :followings, source: :user
  has_many :offers, inverse_of: :bounty
  has_many :markings, as: :markable
  has_many :marks, through: :markings
  has_many :milestones, through: :milestone_tasks
  has_many :milestone_tasks, foreign_key: 'task_id'
  has_many :mutings
  has_many :postings, class_name: 'BountyPosting', foreign_key: 'bounty_id'
  has_many :taggings, class_name: 'Wip::Tagging'
  has_many :tags, through: :taggings, class_name: 'Wip::Tag'
  has_many :viewings, as: :viewable
  has_many :awards

  has_one  :news_feed_item, foreign_key: 'target_id'
  has_one :milestone
  accepts_nested_attributes_for :milestone

  validates :title, presence: true, length: { minimum: 2 }

  before_validation :set_author_tip, on: :create
  after_commit :set_number, on: :create
  after_commit -> { Indexer.perform_async(:index, Wip.to_s, self.id) }, on: :create
  after_commit :update_news_feed_item
  after_commit :update_product_markings, on: :create
  after_update :update_elasticsearch

  default_scope -> { where(deleted_at: nil) }

  scope :available,   ->{ where(state: 'open') }
  scope :by_product,  ->(product){ where(product_id: product.id) }
  scope :closed,      -> { where('closed_at is not null') }
  scope :with_mark,   -> (name) { joins(:marks).where(marks: { name: name }) }
  scope :not_posted,  -> { joins('left outer join bounty_postings on bounty_postings.bounty_id = wips.id').where('bounty_postings.id is null') }
  scope :open,        -> { where('closed_at is null') }
  scope :opened_by,   ->(user) { where(user: user) }
  scope :promoted,    -> { where('promoted_at is not null') }
  scope :stale_by,    ->(age) { joins(:events).group('wips.id').having('max(events.created_at) < ?', age).order('max(events.created_at)') }
  scope :tagged_with, ->(name) { joins(:taggings => :tag).includes(:taggings => :tag).where('wip_tags.name = ?', name) }
  scope :tagged_with_any, ->(names) { joins(:taggings => :tag).includes(:taggings => :tag).where('wip_tags.name' => names) }
  scope :tagged_with_all, ->(names) { joins(:taggings => :tag).where('wip_tags.name' => names).group('wips.id').having('count(distinct wip_taggings.wip_tag_id) = ?', names.size) }
  scope :unflagged, -> { where(flagged_at: nil) }
  scope :ordered_by_activity, -> { joins(:events).group('wips.id').order('max(events.created_at)') }

  attr_accessor :readraptor_tag # set which tag you are viewing

  # Workflow
  workflow_column :state
  workflow do
    state :open do
      event :close,       :transitions_to => :resolved
    end
    state :resolved do
      event :reopen,      :transitions_to => :open
    end

    after_transition { notify_state_changed }
  end

  def open?
    closed_at.nil?
  end

  def closed?
    !open?
  end

  def awarded?
    self.awards.any?
  end

  def close(closer, reason=nil)
    add_activity closer, Activities::Close do
      add_event ::Event::Close.new(user: closer, body: reason) do
        set_closed(closer)
        milestones.each(&:touch)
      end
    end
  end

  def reopen(opener, reason)
    add_activity opener, Activities::Open do
      add_event ::Event::Reopen.new(user: opener, body: reason) do
        self.closer = nil
        self.closed_at = nil
        milestones.each(&:touch)
      end
    end
  end

  def update_title!(author, new_title)
    add_activity author, Activities::Update do
      add_event ::Event::TitleChange.new(user: author, body: self.title) do
        self.title = new_title
      end
    end
  end

  def sanitized_description
    Search::Sanitizer.new.sanitize(description)
  end

  def to_param
    number || id
  end

  def slug
    "#{product.slug}/#{number}"
  end

  # TODO finish the WIP split refactoring by removing these methods
  def promoted?
    false
  end

  def winner
    nil
  end

  def awardable?
    false
  end

  def main_thread?
    false
  end

  def followed_by?(user)
    Watching.following?(user, self)
  end

  # TODO: rename to follow
  def watch!(user)
    Watching.watch!(user, self)
  end

  def unfollow!(user)
    Watching.unwatch!(user, self)
  end

  def auto_watch!(user)
    Watching.auto_watch!(user, self)
  end

  def contributors
    # TODO (whatupdave): when we can unwatch a wip we will need to look at this
    User.joins(:watchings)
      .where('watchings.watchable_id = ?', self.id)
      .where('watchings.unwatched_at is null')
  end

  # tagging

  def update_tag_names!(author, new_tag_names)
    return if self.tag_names.sort == new_tag_names.uniq.sort

    add_event ::Event::TagChange.new(user: author, from: self.tag_names.sort.join(','), to: new_tag_names.sort.join(',')) do
      self.tag_names = new_tag_names
    end
  end

  def update_news_feed_item
    if self.news_feed_item
      self.news_feed_item.update(updated_at: Time.now)
    end
  end

  def add_tag!(tag_name)
    self.tag_names = ((tag_names || []) | [tag_name])
    save!
  end

  def tag_names
    tags.map(&:name)
  end

  def tag_names=(names)
    if names[0].class == Array
      names = names[0]
    end

    self.tags = names.map do |n|
      Wip::Tag.find_or_create_by!(name: n.strip)
    end
  end

  def tag_list
    tag_names.join(', ')
  end

  def tag_list=(names)
    self.tag_names = names.split(',')
  end

  def mark_vector
    #get unnormalized mark vector of wip itself
    my_mark_vector = QueryMarks.new.mark_vector_for_object(self)

    #get unnoramlized mark vector of product
    my_parent_mark_vector = QueryMarks.new.mark_vector_for_object(self.product)

    #scale parent vector by constant
    my_parent_mark_vector = QueryMarks.new.scale_mark_vector(my_parent_mark_vector, 1)

    final_mark_vector = QueryMarks.new.add_mark_vectors(my_parent_mark_vector, my_mark_vector)
  end

  def normalized_mark_vector()
    QueryMarks.new.normalize_mark_vector(self.mark_vector())
  end

  def update_product_markings
    if self.type == "Task"
      scalar = 0.2
      AdjustMarkings.perform_async(self.product_id, "Product", self.id, "Wip", scalar)
    end
  end

  # flagging

  def flag!(flagger)
    update! flagged_at: Time.now, flagged_by_id: flagger.id
  end

  def unflag!
    update! flagged_at: nil, flagged_by_id: nil
  end

  def flagged?
    !flagged_at.nil?
  end

  def featured?
    flagged_at.nil?
  end

  # callbacks

  def push_channel
    [product.slug, number].join('.')
  end

  def event_added(event)
    Wip.reset_counters(self.id, :events)
  end

  def vote_added(vote)
    product.watch!(vote.user)
    watch!(vote.user)
  end

  def notify_by_email(user)
    EmailLog.send_once(user.id, id) do
      WipMailer.delay(queue: 'mailer').wip_created(user.id, id) unless user.mail_never?
    end
  end

  def notify_state_changed
    PusherWorker.perform_async push_channel, 'changed', WipSerializer.new(self).to_json
  end

  # elasticsearch
  def update_elasticsearch
    return unless title_changed? || state_changed?

    Indexer.perform_async(:index, Wip.to_s, self.id)
  end

  settings NGRAM_ANALYZER do
    mappings do
      indexes :title,  analyzer: 'ngram_analyzer'
      indexes :hidden, index: 'not_analyzed'
      indexes :state,  index: 'not_analyzed'

      # indexes :comments do
      #   indexes :sanitized_body, analyzer: 'snowball'
      # end

      indexes :product do
        indexes :slug, index: 'not_analyzed'
      end
    end
  end

  def as_indexed_json(options={})
    as_json(
      only: [:title, :number, :state],
      methods: [:hidden],

      include: {
        # comments: {only: [:id, :number], methods: [:sanitized_body]},
        product: {only: [:slug] }
      }
    )
  end

  def hidden
    product.try(:hidden)
  end

  # protected

  def set_author_tip
    self.author_tip = Task::AUTHOR_TIP
  end

  def set_number
    return unless number.nil?

    Room.create_for!(product, self).tap do |shortcut|
      self.update_column :number, shortcut.number
    end
  end

  def add_activity(actor, klass, &block)
    block.call.tap do |event|
      klass.publish!(
        actor: actor,
        subject: event,
        target: self
      )
    end
  end

  def add_event(event, &block)
    with_lock do
      events << event
      block.call(event) if block
      save!
    end
    event
  end

  def set_closed(closer)
    raise ActiveRecord::RecordNotSaved if closed?

    self.closer = closer
    self.closed_at = Time.current

    milestones.each(&:touch)
  end

  def track_activity
    StreamEvent.add_create_event!(actor: user, subject: self, target: product)
  end

  def chat?
    !chat_room.nil?
  end

  def to_partial_path
    # Special case for Projects
    if self.class == Wip
      'projects/project'
    elsif self.class == Task
      'bounties/bounty'
    else
      super
    end
  end

  def update_watchings_count!
    update! watchings_count: followings.count
  end

  def sum_viewings
    Viewing.where(viewable: self).count
  end

  # stories
  def url_params
    [product, self]
  end

end