290 lines
9.8 KiB
Ruby
290 lines
9.8 KiB
Ruby
# This file is a part of Redmine Q&A (redmine_questions) plugin,
|
|
# Q&A plugin for Redmine
|
|
#
|
|
# Copyright (C) 2011-2018 RedmineUP
|
|
# http://www.redmineup.com/
|
|
#
|
|
# redmine_questions is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# redmine_questions is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with redmine_questions. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
class Question < ActiveRecord::Base
|
|
unloadable
|
|
|
|
include Redmine::SafeAttributes
|
|
extend ApplicationHelper
|
|
|
|
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
|
belongs_to :section, :class_name => 'QuestionsSection', :foreign_key => 'section_id'
|
|
belongs_to :status, :class_name => 'QuestionsStatus', :foreign_key => 'status_id'
|
|
|
|
delegate :section_type, :to => :section, :allow_nil => true
|
|
|
|
has_many :answers, :class_name => 'QuestionsAnswer', :dependent => :destroy
|
|
|
|
if ActiveRecord::VERSION::MAJOR >= 4
|
|
has_many :comments, lambda { order('created_on') }, :as => :commented, :dependent => :delete_all
|
|
else
|
|
has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
|
|
end
|
|
rcrm_acts_as_viewed
|
|
|
|
acts_as_attachable_questions
|
|
acts_as_watchable
|
|
|
|
acts_as_event :datetime => :created_on,
|
|
:url => Proc.new {|o| {:controller => 'questions', :action => 'show', :id => o }},
|
|
:type => Proc.new {|o| 'icon ' + (o.is_solution? ? 'icon-solution': 'icon-question')},
|
|
:description => :content,
|
|
:title => Proc.new {|o| o.subject }
|
|
|
|
|
|
if ActiveRecord::VERSION::MAJOR >= 4
|
|
acts_as_activity_provider :type => 'questions',
|
|
:permission => :view_questions,
|
|
:author_key => :author_id,
|
|
:timestamp => "#{table_name}.created_on",
|
|
:scope => joins({:section => :project}, :author)
|
|
acts_as_searchable :columns => ["#{table_name}.subject",
|
|
"#{table_name}.content",
|
|
"#{QuestionsAnswer.table_name}.content"],
|
|
:scope => joins({:section => :project}, :answers),
|
|
:project_key => "#{QuestionsSection.table_name}.project_id"
|
|
else
|
|
acts_as_activity_provider :type => 'questions',
|
|
:permission => :view_questions,
|
|
:author_key => :author_id,
|
|
:timestamp => "#{table_name}.created_on",
|
|
:find_options => { :include => [{:section => :project}, :author] }
|
|
acts_as_searchable :columns => ["#{table_name}.subject",
|
|
"#{table_name}.content",
|
|
"#{QuestionsAnswer.table_name}.content"],
|
|
:include => [{:section => :project}, :answers],
|
|
:project_key => "#{QuestionsSection.table_name}.project_id"
|
|
end
|
|
|
|
scope :solutions, lambda { joins(:section).where(:questions_sections => {:section_type => QuestionsSection::SECTION_TYPE_SOLUTIONS}) }
|
|
scope :questions, lambda { joins(:section).where(:questions_sections => {:section_type => QuestionsSection::SECTION_TYPE_QUESTIONS}) }
|
|
scope :by_votes, lambda { order("#{Question.table_name}.cached_weighted_score DESC") }
|
|
scope :by_date, lambda { order("#{Question.table_name}.created_on DESC") }
|
|
scope :by_update, lambda { order("#{Question.table_name}.updated_on DESC") }
|
|
scope :by_views, lambda { order("#{Question.table_name}.views DESC") }
|
|
scope :positive, lambda { where("#{Question.table_name}.cached_weighted_score > 0") }
|
|
scope :featured, lambda {|*args| where(:featured => true) }
|
|
scope :in_section, lambda { |section|
|
|
where(:section_id => section) if section.present?
|
|
}
|
|
scope :in_project, lambda { |project|
|
|
joins(:section => :project).where("#{QuestionsSection.table_name}.project_id = ?", project) if project.present?
|
|
}
|
|
scope :visible, lambda { |*args|
|
|
joins(:section => :project)
|
|
.where(Project.allowed_to_condition(args.shift || User.current, :view_questions, *args))
|
|
}
|
|
|
|
validates_presence_of :author, :content, :subject, :section
|
|
|
|
after_create :add_author_as_watcher
|
|
after_create :send_notification
|
|
|
|
safe_attributes 'author',
|
|
'subject',
|
|
'content',
|
|
'tag_list',
|
|
'section_id',
|
|
'status_id'
|
|
|
|
safe_attributes 'status_id',
|
|
:if => lambda {|question, user| question.is_idea?}
|
|
|
|
def self.visible_condition(user)
|
|
user.reload if user
|
|
global_questions_allowed = user.allowed_to?(:view_questions, nil)
|
|
projects_allowed_to_view_questions = Project.where(Project.allowed_to_condition(user, :view_questions)).pluck(:id)
|
|
allowed_to_view_condition = global_questions_allowed ? "(#{table_name}.project_id IS NULL)" : '(0=1)'
|
|
allowed_to_view_condition += projects_allowed_to_view_questions.empty? ? ' OR (0=1) ' : " OR (#{table_name}.project_id IN (#{projects_allowed_to_view_questions.join(',')}))"
|
|
|
|
user.admin? ? '(1=1)' : allowed_to_view_condition
|
|
end
|
|
|
|
def self.related(question, limit)
|
|
tokens = question.subject.strip.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).
|
|
collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '').gsub(%r{('|"|`)}, '')}.select{|m| m.size > 3} || ""
|
|
|
|
related_questions = where(tokens.map{ |t| "LOWER(subject) LIKE LOWER('%#{t}%')" }.join(' OR '))
|
|
related_questions = related_questions.in_project(question.project)
|
|
related_questions = related_questions.where("#{Question.table_name}.id != ?", question.id)
|
|
related_questions.limit(limit).to_a.compact
|
|
end
|
|
|
|
def commentable?(user = User.current)
|
|
return false if locked?
|
|
user.allowed_to?(:comment_question, project)
|
|
end
|
|
|
|
def visible?(user=User.current)
|
|
user.allowed_to?(:view_questions, project)
|
|
end
|
|
|
|
def to_param
|
|
"#{id}-#{ActiveSupport::Inflector.transliterate(subject || " ").parameterize}"
|
|
end
|
|
|
|
def section_name
|
|
section.try(:name)
|
|
end
|
|
|
|
def project
|
|
section.project
|
|
end
|
|
|
|
def allow_voting?
|
|
false
|
|
end
|
|
|
|
def allow_liking?
|
|
section.allow_liking?
|
|
end
|
|
|
|
def allow_answering?
|
|
!locked? && section.allow_answering?
|
|
end
|
|
|
|
def last_reply
|
|
answers.order('created_on DESC').last
|
|
end
|
|
|
|
def last_comment
|
|
Comment.where(:commented_type => self.class.name, :commented_id => [id] + answer_ids).order('created_on DESC').last
|
|
end
|
|
|
|
def replies_count
|
|
answers.count
|
|
end
|
|
|
|
def editable_by?(user)
|
|
(author == user && user.allowed_to?(:edit_own_questions, project)) ||
|
|
user.allowed_to?(:edit_questions, project)
|
|
end
|
|
|
|
def destroyable_by?(user)
|
|
user.allowed_to?(:delete_questions, project)
|
|
end
|
|
|
|
def votable_by?(user)
|
|
user.allowed_to?(:vote_questions, project)
|
|
end
|
|
|
|
def convertable_by?(user)
|
|
return false if project.blank?
|
|
user.allowed_to?(:convert_questions, project)
|
|
end
|
|
|
|
def answered?
|
|
answers.where(:accepted => true).any?
|
|
end
|
|
|
|
def is_question?
|
|
section && section.is_questions?
|
|
end
|
|
|
|
def is_solution?
|
|
section && section.is_solutions?
|
|
end
|
|
|
|
def is_idea?
|
|
section && section.is_ideas?
|
|
end
|
|
|
|
# def to_issue
|
|
# issue = Issue.new
|
|
# issue.author = self.author
|
|
# issue.created_on = self.created_on
|
|
# issue.subject = self.subject
|
|
# issue.description = self.content
|
|
# issue.watchers = self.watchers
|
|
# issue.attachments = self.attachments
|
|
# issue.project = self.project
|
|
# issue.tracker = self.project.trackers.first
|
|
# issue.status = IssueStatus.first
|
|
# self.answers.each do |ans|
|
|
# journal = Journal.new(:notes => ans.content, :user => ans.author)
|
|
# issue.journals << journal
|
|
# end
|
|
# issue
|
|
# end
|
|
|
|
# def self.from_issue(issue)
|
|
# question = Question.new
|
|
# question.author = issue.author
|
|
# question.created_on = issue.created_on
|
|
# question.subject = issue.subject
|
|
# question.content = issue.description.blank? ? issue.subject : issue.description
|
|
# question.watchers = issue.watchers
|
|
# question.attachments = issue.attachments
|
|
# question.project = issue.project
|
|
# question.section = issue.project.questions_sections.first
|
|
# issue.journals.select{|j| j.notes.present?}.each do |journal|
|
|
# reply = Question.new
|
|
# reply.author = journal.user
|
|
# reply.created_on = journal.created_on
|
|
# reply.content = journal.notes
|
|
# reply.project = issue.project
|
|
# reply.question = question
|
|
# question.answers << reply
|
|
# end
|
|
# question
|
|
# end
|
|
|
|
def notified_users
|
|
project.notified_users.select { |user| visible?(user) }.collect(&:mail)
|
|
end
|
|
|
|
def self.to_text(input)
|
|
textile_glyphs = {
|
|
'’' => "'",
|
|
'‘' => "'",
|
|
'<' => '<',
|
|
'>' => '>',
|
|
'”' => "'",
|
|
'“' => '"',
|
|
'…' => '...',
|
|
'\1—' => '--',
|
|
' → ' => '->',
|
|
'¶' => ' ',
|
|
' – ' => '-',
|
|
'×' => '-',
|
|
'™' => '(TM)',
|
|
'®' => '(R)',
|
|
'©' => '(C)',
|
|
'&' => '&'
|
|
}.freeze
|
|
|
|
html_regexp = /<(?:[^>"']+|"(?:\\.|[^\\"]+)*"|'(?:\\.|[^\\']+)*')*>/xm
|
|
|
|
input.dup.gsub(html_regexp, '').tap do |h|
|
|
textile_glyphs.each do |entity, char|
|
|
h.gsub!(entity, char)
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def add_author_as_watcher
|
|
Watcher.create(:watchable => self, :user => author)
|
|
end
|
|
|
|
def send_notification
|
|
Mailer.question_question_added(self).deliver if Setting.notified_events.include?('question_added')
|
|
end
|
|
end
|