app/models/course/discussion/post.rb
# frozen_string_literal: true
class Course::Discussion::Post < ApplicationRecord
include Workflow
extend Course::Discussion::Post::OrderingConcern
include Course::Discussion::Post::RetrievalConcern
include Course::ForumParticipationConcern
workflow do
state :draft do
event :delay_publish, transitions_to: :delayed
event :publish, transitions_to: :published
end
state :delayed
state :published do
event :unpublish, transitions_to: :draft
end
end
acts_as_forest order: :created_at, optional: true
acts_as_readable on: :updated_at
has_many_attachments on: :text
after_initialize :set_topic, if: :new_record?
after_commit :mark_topic_as_read
after_save :mark_self_as_read
after_update :mark_self_as_read
before_destroy :reparent_children, unless: :destroyed_by_association
before_destroy :unparent_children, if: :destroyed_by_association
before_save :sanitize_text
validate :parent_topic_consistency
validates :text, presence: true
validates :title, length: { maximum: 255 }, allow_nil: true
validates :creator, presence: true
validates :updater, presence: true
validates :topic, presence: true
validates :workflow_state, length: { maximum: 255 }, presence: true
validates :is_anonymous, inclusion: { in: [true, false] }
belongs_to :topic, inverse_of: :posts, touch: true
has_many :votes, inverse_of: :post, dependent: :destroy
has_one :codaveri_feedback, inverse_of: :post, dependent: :destroy
accepts_nested_attributes_for :codaveri_feedback
default_scope { ordered_by_created_at.with_creator }
scope :ordered_by_created_at, -> { order(created_at: :asc) }
scope :with_creator, -> { includes(:creator) }
scope :only_draft_posts, -> { where(workflow_state: :draft) }
scope :only_published_posts, -> { where(workflow_state: :published) }
scope :only_delayed_posts, -> { where(workflow_state: :delayed) }
# @!method self.with_user_votes(user)
# Preloads the given posts with votes from the given user.
#
# @param [User] user The user to load votes for.
scope :with_user_votes, (lambda do |user|
post_ids = pluck('course_discussion_posts.id')
votes = Course::Discussion::Post::Vote.
where('course_discussion_post_votes.post_id IN (?)', post_ids).
where('course_discussion_post_votes.creator_id = ?', user.id)
all.tap do |result|
preloader = ActiveRecord::Associations::Preloader.new(records: result,
associations: :votes,
scope: votes)
preloader.call
end
end)
# @!attribute [r] upvotes
# The number of upvotes for the given post.
calculated :upvotes, (lambda do
Vote.upvotes.
select('count(id)').
where('post_id = course_discussion_posts.id')
end)
# @!attribute [r] downvotes
# The number of downvotes for the given post.
calculated :downvotes, (lambda do
Vote.downvotes.
select('count(id)').
where('post_id = course_discussion_posts.id')
end)
# Calculates the total number of votes given to this post.
#
# @return [Integer]
def vote_tally
upvotes - downvotes
end
# Gets the vote cast by the given user for the current post.
#
# @param [User] user The user to retrieve the vote for.
# @return [Course::Discussion::Post::Vote] The vote that the user cast.
# @return [nil] The user has not cast a vote.
def vote_for(user)
votes.loaded? ? votes.find { |vote| vote.creator_id == user.id } : votes.find_by(creator: user)
end
# Allows a user to cast a vote for this post.
#
# @param [User] user The user casting the vote.
# @param [Integer] vote {-1, 0, 1} indicating whether this is a downvote, no vote, or upvote.
def cast_vote!(user, vote)
vote = vote <=> 0
vote_record = votes.find_by(creator: user)
if vote == 0
vote_record&.destroy!
else
vote_record ||= votes.build(creator: user)
vote_record.vote_flag = vote > 0
vote_record.save!
end
end
# Mark/unmark post as the correct answer.
def toggle_answer
self.class.transaction do
raise ActiveRecord::Rollback unless update_column(:answer, !answer)
raise ActiveRecord::Rollback unless topic.specific.update_resolve_status
end
true
end
# Use the CourseUser name if available, else fallback to the User name.
#
# @return [String] The CourseUser/User name of the post author.
def author_name
course_user = topic.course.course_users.for_user(creator).first
course_user&.name || creator.name
end
private
def set_topic
self.topic ||= parent.topic if parent
end
def parent_topic_consistency
errors.add(:topic_inconsistent) if parent && topic != parent.topic
end
def reparent_children
children.update_all(parent_id: parent_id)
end
# Should be called only when destroyed by association.
#
# We unset the children's parent id so they don't trigger a foreign key exception when the
# parent is marked for destruction first. They will be destroyed by association later.
#
# This method assumes that :destroyed_by_association is true if and only if the entire topic
# the post belongs to is being destroyed.
def unparent_children
children.update_all(parent_id: nil)
end
def mark_topic_as_read
topic.mark_as_read! for: creator
topic.actable.mark_as_read! for: creator
end
def mark_self_as_read
mark_as_read! for: creator
end
def sanitize_text
self.text = ApplicationController.helpers.sanitize_ckeditor_rich_text(text)
end
end