Permalink
Fetching contributors…
Cannot retrieve contributors at this time
244 lines (194 sloc) 6.33 KB
# encoding: utf-8
# == Schema Information
#
# Table name: comments
#
# id :integer not null, primary key
# node_id :integer not null
# user_id :integer
# state :string(10) default("published"), not null
# title :string(160) not null
# score :integer default(0), not null
# answered_to_self :boolean default(FALSE), not null
# materialized_path :string(1022)
# body :text(4294967295)
# wiki_body :text(16777215)
# created_at :datetime
# updated_at :datetime
#
# The users can comment any content.
# Those comments are threaded and can be noted.
#
class Comment < ActiveRecord::Base
include Canable::Ables
belongs_to :user
belongs_to :node, counter_cache: :comments_count
delegate :content, :content_type, to: :node
scope :under, ->(path) { where("materialized_path LIKE ?", "#{path}_%") }
scope :published, -> { where(state: 'published') }
scope :on_dashboard, -> { published.order(created_at: :desc) }
scope :footer, -> { published.order(created_at: :desc).limit(12).select([:id, :node_id, :title]) }
validates :title, presence: { message: "Le titre est obligatoire" },
length: { maximum: 100, message: "Le titre est trop long" }
validates :wiki_body, presence: { message: "Vous ne pouvez pas poster un commentaire vide" }
wikify_attr :body
### Reading status ###
# Returns true if this comment has been read by the given user,
# but also for anonymous users
def read_by?(account)
return true if account.nil?
r = Node.last_reading(node_id, account.id)
r && r >= created_at
end
before_create :touch_node
def touch_node
node.touch(:last_commented_at) if node
end
### Threads ###
PATH_SIZE = 12 # Each id in the materialized_path is coded on 12 chars
MAX_DEPTH = 1022 / PATH_SIZE
EDITION_PERIOD_IN_MINUTES = 5
after_create :generate_materialized_path
def generate_materialized_path
parent = Comment.find(parent_id) if parent_id.present?
parent_path = parent ? parent.materialized_path : ''
self.materialized_path = "%s%0#{PATH_SIZE}d" % [parent_path, self.id]
self.answered_to_self = answer_to_self?
save
end
def parent_id
@parent_id ||= materialized_path && materialized_path[-2 * PATH_SIZE .. - PATH_SIZE - 1].to_i
@parent_id
end
def parent_id=(parent_id)
@parent_id = parent_id
return if parent_id.blank?
@parent = node.comments.find(parent_id)
return if @parent.blank? || @parent.deleted?
self.title ||= @parent ? "#{@parent.title.starts_with?('Re:') ? '' : 'Re: '}#{@parent.title}" : ''
end
attr_reader :parent
def depth
(materialized_path.length / PATH_SIZE) - 1
end
def root?
depth == 0
end
def answer_to_self?
return false if root?
Comment.where(node_id: node_id, user_id: user_id).
where("LOCATE(materialized_path, ?) > 0", materialized_path).
where("id != ?", self.id).
exists?
end
def children?
Comment.where(node_id: node_id, user_id: user_id).
where("materialized_path LIKE '#{materialized_path}%'").
where("id != ?", self.id).
exists?
end
def parents
Comment.where(node_id: node_id).
where("LOCATE(materialized_path, ?) = 1", materialized_path).
where("id != ?", self.id)
end
def edition_period
return EDITION_PERIOD_IN_MINUTES
end
### Notifications ###
after_create :notify_parents
def notify_parents
parents.each do |p|
next if p.user_id == user_id
account = p.user.try(:account)
account.notify_answer_on node_id if account
end
end
### Calculations ###
before_validation :default_score, on: :create
def default_score
if user.account.karma > 0
self.score = Math.log10(user.account.karma).to_i - 1
self.score = [self.score, 1].max if content_type == 'Post'
else
self.score = [-2 + user.account.karma/30, -10].max
end
end
def nb_answers
self.class.published.under(materialized_path).count
end
def last_answer
self.class.published.under(materialized_path).last
end
### ACL ###
def viewable_by?(account)
state != 'deleted' || account.try(:admin?)
end
def creatable_by?(account)
node && node.content && node.content.commentable_by?(account) && !account.blocked?
end
def updatable_by?(account)
account.moderator? || account.admin? ||
(user_id == account.user_id && created_at >= EDITION_PERIOD_IN_MINUTES.minutes.ago && !children?)
end
def destroyable_by?(account)
account.moderator? || account.admin?
end
def votable_by?(account)
!deleted? && user_id != account.user_id &&
(Time.now - created_at) < 3.months &&
(account.nb_votes > 0 || account.amr?) &&
!vote_by?(account.id)
end
### Votes ###
def vote_by?(account_id)
%w(for against).find {|w| $redis.sismember("comments/#{self.id}/votes/#{w}", account_id) }
end
def vote_for(account)
vote(account, :for) && Comment.increment_counter(:score, self.id)
end
def vote_against(account)
vote(account, :against) && Comment.decrement_counter(:score, self.id)
end
def vote(account, what)
value = what == :for ? 1 : -1
key = "comments/#{self.id}/votes/#{what}"
return false unless $redis.sadd(key, account.id)
$redis.expire(key, 7776000) # 3 months
unless score * (score + value) > 100 # Score is out of the [-10, 10] bounds
$redis.incrby("users/#{self.user_id}/diff_karma", value)
end
Account.decrement_counter(:nb_votes, account.id)
true
end
def nb_votes_for
$redis.scard "comments/#{self.id}/votes/for"
end
def nb_votes_against
$redis.scard "comments/#{self.id}/votes/against"
end
### Workflow ###
def mark_as_deleted
self.state = 'deleted'
save
end
def deleted?
state == 'deleted'
end
### Statistics ###
after_create :compute_stats
def compute_stats
return unless node
today = Date.today
$redis.incr "stats/comments/year/#{today.year}/#{content_type}"
$redis.incr "stats/comments/month/#{today.strftime "%Y%m"}/#{content_type}"
$redis.incr "stats/comments/wday/#{(today.wday - 1) % 7}"
end
### Presentation ###
def user_name
user.try :name
end
def bound_score
[[score, -10].max, 10].min
end
end