Actualizado plugin Redmine Questions 1.0.0 light
|
@ -103,7 +103,7 @@ redmine_private_wiki
|
||||||
|
|
||||||
### Redmine Q&A plugin
|
### Redmine Q&A plugin
|
||||||
|
|
||||||
* questions 0.0.7
|
* questions 1.0.0
|
||||||
* https://www.redmine.org/plugins/redmine_questions
|
* https://www.redmine.org/plugins/redmine_questions
|
||||||
* http://www.redminecrm.com/projects/questions
|
* http://www.redminecrm.com/projects/questions
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
gem "redmine_crm"
|
gem "redmine_crm"
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
# 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 QuestionsAnswersController < ApplicationController
|
||||||
|
unloadable
|
||||||
|
|
||||||
|
before_action :find_question, :only => [:new, :create]
|
||||||
|
before_action :find_answer, :only => [:update, :destroy, :edit, :show]
|
||||||
|
|
||||||
|
helper :questions
|
||||||
|
helper :watchers
|
||||||
|
helper :attachments
|
||||||
|
|
||||||
|
include QuestionsHelper
|
||||||
|
|
||||||
|
def new
|
||||||
|
@answer = QuestionsAnswer.new(:question => @question_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@answer.safe_attributes = params[:answer]
|
||||||
|
@answer.save_attachments(params[:attachments])
|
||||||
|
if @answer.save
|
||||||
|
flash[:notice] = l(:label_answer_successful_update)
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to_question }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render :edit}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@answer = QuestionsAnswer.new
|
||||||
|
@answer.author = User.current
|
||||||
|
@answer.question = @question_item
|
||||||
|
@answer.safe_attributes = params[:answer]
|
||||||
|
@answer.save_attachments(params[:attachments])
|
||||||
|
if @answer.save
|
||||||
|
flash[:notice] = l(:label_answer_successful_added)
|
||||||
|
render_attachment_warning_if_needed(@answer)
|
||||||
|
end
|
||||||
|
redirect_to_question
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
if @answer.destroy
|
||||||
|
flash[:notice] = l(:notice_successful_delete)
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to_question }
|
||||||
|
format.api { render_api_ok }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = l(:notice_unsuccessful_save)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def preview
|
||||||
|
if params[:id].present? && answer = Question.find_by_id(params[:id])
|
||||||
|
@previewed = answer
|
||||||
|
end
|
||||||
|
@text = (params[:answer] ? params[:answer][:content] : nil)
|
||||||
|
render :partial => 'common/preview'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def redirect_to_question
|
||||||
|
redirect_to question_path(@answer.question, :anchor => "question_item_#{@answer.id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_answer
|
||||||
|
@answer = QuestionsAnswer.find(params[:id])
|
||||||
|
@question_item = @answer.question
|
||||||
|
@project = @question_item.project
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_question
|
||||||
|
@question_item = Question.visible.find(params[:question_id]) unless params[:question_id].blank?
|
||||||
|
@project = @question_item.project
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,78 @@
|
||||||
|
# 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 QuestionsCommentsController < ApplicationController
|
||||||
|
before_action :find_comment_source
|
||||||
|
|
||||||
|
helper :questions
|
||||||
|
|
||||||
|
def create
|
||||||
|
raise Unauthorized unless @comment_source.commentable?
|
||||||
|
|
||||||
|
@comment = Comment.new
|
||||||
|
@comment.safe_attributes = params[:comment]
|
||||||
|
@comment.author = User.current
|
||||||
|
if @comment_source.comments << @comment
|
||||||
|
@comment_source.touch
|
||||||
|
flash[:notice] = l(:label_comment_added) unless request.xhr?
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to_question }
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@comment = @comment_source.comments.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@comment = @comment_source.comments.find(params[:id])
|
||||||
|
@comment.safe_attributes = params[:comment]
|
||||||
|
if @comment.save
|
||||||
|
flash[:notice] = l(:notice_successful_update)
|
||||||
|
redirect_to_question
|
||||||
|
else
|
||||||
|
render :action => 'edit'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@comment_source.comments.find(params[:id]).destroy
|
||||||
|
redirect_to_question
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_comment_source
|
||||||
|
comment_source_type = params[:source_type]
|
||||||
|
comment_source_id = params[:source_id]
|
||||||
|
|
||||||
|
klass = Object.const_get(comment_source_type.camelcase)
|
||||||
|
@comment_source = klass.find(comment_source_id)
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_to_question
|
||||||
|
question = @comment_source.is_a?(QuestionsAnswer) ? @comment_source.question : @comment_source
|
||||||
|
redirect_to question_path(question, :anchor => @comment.blank? ? "#{@comment_source.class.name.underscore}_#{@comment_source.id}" : "comment_#{@comment.id}")
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,76 +1,146 @@
|
||||||
|
# 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 QuestionsController < ApplicationController
|
class QuestionsController < ApplicationController
|
||||||
unloadable
|
unloadable
|
||||||
|
|
||||||
before_filter :find_optional_project, :only => [:autocomplete_for_topic, :topics]
|
before_action :find_question, :only => [:edit, :show, :update, :destroy]
|
||||||
before_filter :find_optional_board, :only => [:autocomplete_for_topic, :topics]
|
before_action :find_optional_project, :only => [:index, :update_form, :new, :create, :autocomplete_for_subject]
|
||||||
before_filter :find_topic, :authorize, :only => :vote
|
before_action :find_section, :only => [:new, :create, :update, :edit]
|
||||||
before_filter :find_topics, :only => [:topics, :autocomplete_for_topic]
|
before_action :find_questions, :only => [:autocomplete_for_subject, :index] #:autocomplete_for_subject
|
||||||
|
|
||||||
helper :questions
|
helper :questions
|
||||||
if Redmine::VERSION.to_s > '2.1'
|
helper :watchers
|
||||||
helper :boards
|
helper :attachments
|
||||||
end
|
|
||||||
include QuestionsHelper
|
include QuestionsHelper
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@boards = Board.visible.includes(:last_message => :author).includes(:messages).order(:project_id)
|
@question_item = Question.new
|
||||||
# show the board if there is only one
|
end
|
||||||
if @boards.size == 1
|
|
||||||
@board = @boards.first
|
def new
|
||||||
redirect_to project_board_url(@board, :project_id => @board.project)
|
@question_item = Question.new
|
||||||
|
@question_item.section ||= @section
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@answers = @question_item.answers.by_accepted.by_votes.by_date
|
||||||
|
if @answers
|
||||||
|
@limit = Setting.issues_export_limit.to_i
|
||||||
|
@answer_count = @answers.count
|
||||||
|
@answer_pages = Paginator.new @answer_count, @limit, (params[:page] || 1)
|
||||||
|
@offset ||= @answer_pages.offset
|
||||||
|
end
|
||||||
|
@answer = QuestionsAnswer.new
|
||||||
|
@answer.question = @question_item
|
||||||
|
@question_item.view request.remote_addr, User.current
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@question_item.safe_attributes = params[:question]
|
||||||
|
@question_item.save_attachments(params[:attachments])
|
||||||
|
if @question_item.save
|
||||||
|
flash[:notice] = l(:label_question_successful_update)
|
||||||
|
respond_to do |format|
|
||||||
|
format.html {redirect_to :action => :show, :id => @question_item}
|
||||||
|
end
|
||||||
else
|
else
|
||||||
render "boards/index"
|
respond_to do |format|
|
||||||
|
format.html { render :edit}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def topics
|
def update_form
|
||||||
|
@question_item = Question.new
|
||||||
|
@question_item.safe_attributes = params[:question]
|
||||||
end
|
end
|
||||||
|
|
||||||
def vote
|
def create
|
||||||
User.current.voted_for?(@topic) ? @topic.dislike(User.current.becomes(Principal)) : @topic.like(User.current.becomes(Principal))
|
@question_item = Question.new
|
||||||
|
@question_item.section = @section
|
||||||
|
@question_item.safe_attributes = params[:question]
|
||||||
|
@question_item.author = User.current
|
||||||
|
@question_item.save_attachments(params[:attachments])
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to_referer_or {render :text => (watching ? 'Vote added.' : 'Vote removed.'), :layout => true}}
|
if @question_item.save
|
||||||
|
format.html { redirect_to :action => :show, :id => @question_item}
|
||||||
|
else
|
||||||
|
format.html { render :action => 'new' }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def autocomplete_for_topic
|
def autocomplete_for_subject
|
||||||
render :layout => false
|
render :layout => false
|
||||||
end
|
end
|
||||||
|
|
||||||
def convert_issue
|
# def convert_issue_to_question
|
||||||
issue = Issue.visible.find(params[:issue_id])
|
# issue = Issue.visible.find(params[:issue_id])
|
||||||
board = Board.visible.find(params[:board_id])
|
# question = Question.from_issue(issue)
|
||||||
message = Message.new
|
# if question.save
|
||||||
message.author = issue.author
|
# issue.destroy if params[:destroy]
|
||||||
message.created_on = issue.created_on
|
# redirect_to _question_path(question)
|
||||||
message.board = board
|
# else
|
||||||
message.subject = issue.subject
|
# redirect_back_or_default({:controller => 'issues', :action => 'show', :id => issue})
|
||||||
message.content = issue.description.blank? ? issue.subject : issue.description
|
# end
|
||||||
message.watchers = issue.watchers
|
# end
|
||||||
message.add_watcher(issue.author)
|
|
||||||
message.attachments = issue.attachments
|
|
||||||
issue.journals.select{|j| !j.notes.blank?}.each do |journal|
|
|
||||||
reply = Message.new
|
|
||||||
reply.author = journal.user
|
|
||||||
reply.created_on = journal.created_on
|
|
||||||
reply.subject = "Re: #{message.subject}"
|
|
||||||
reply.content = journal.notes
|
|
||||||
reply.board = board
|
|
||||||
message.children << reply
|
|
||||||
end
|
|
||||||
if message.save
|
|
||||||
issue.destroy if params[:destroy]
|
|
||||||
redirect_to board_message_path(board, message)
|
|
||||||
else
|
|
||||||
redirect_back_or_default({:controller => 'issues', :action => 'show', :id => issue})
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# def convert_to_issue
|
||||||
|
# issue = @question_item.to_issue
|
||||||
|
# if issue.save
|
||||||
|
# redirect_to issue_path(issue)
|
||||||
|
# else
|
||||||
|
# redirect_back_or_default question_path(@question_item)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
back_id = @question_item.section
|
||||||
|
if @question_item.destroy
|
||||||
|
flash[:notice] = l(:notice_successful_delete)
|
||||||
|
else
|
||||||
|
flash[:error] = l(:notice_unsuccessful_save)
|
||||||
|
end
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_back_or_default questions_path(:section_id => back_id) }
|
||||||
|
format.api { render_api_ok }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def preview
|
||||||
|
if params[:id].present? && query = Question.find_by_id(params[:question_id])
|
||||||
|
@previewed = query
|
||||||
|
end
|
||||||
|
@text = (params[:question] ? params[:question][:content] : nil)
|
||||||
|
render :partial => 'common/preview'
|
||||||
|
end
|
||||||
|
private
|
||||||
|
|
||||||
def find_topics
|
def find_questions
|
||||||
seach = params[:q] || params[:topic_search]
|
seach = params[:q] || params[:topic_search]
|
||||||
|
@section = QuestionsSection.find(params[:section_id]) if params[:section_id]
|
||||||
|
|
||||||
|
scope = Question.visible
|
||||||
|
scope = scope.where(:section_id => @section) if @section
|
||||||
|
|
||||||
columns = ["subject", "content"]
|
columns = ["subject", "content"]
|
||||||
tokens = seach.to_s.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect{|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}.uniq.select {|w| w.length > 1 }
|
tokens = seach.to_s.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect{|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}.uniq.select {|w| w.length > 1 }
|
||||||
|
@ -79,39 +149,49 @@ private
|
||||||
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(' AND ')
|
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(' AND ')
|
||||||
find_options = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
|
find_options = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
|
||||||
|
|
||||||
scope = Message.joins(:board).where({})
|
scope = scope.in_project(@project)
|
||||||
scope = scope.where("#{Message.table_name}.parent_id IS NULL")
|
|
||||||
scope = scope.where(["#{Board.table_name}.project_id = ?", @project.id]) if @project
|
|
||||||
scope = scope.where(["#{Message.table_name}.board_id = ?", @board.id]) if @board
|
|
||||||
scope = scope.where(find_options) unless tokens.blank?
|
scope = scope.where(find_options) unless tokens.blank?
|
||||||
scope = scope.visible.includes(:board).order("#{Message.table_name}.updated_on DESC")
|
@sort_order = params[:sort_order]
|
||||||
|
case @sort_order
|
||||||
|
when 'popular'
|
||||||
|
scope = scope.by_views.by_update
|
||||||
|
when 'newest'
|
||||||
|
scope = scope.by_date
|
||||||
|
when 'active'
|
||||||
|
scope = scope.by_update
|
||||||
|
when 'unanswered'
|
||||||
|
scope = scope.questions.where(:answers_count => 0)
|
||||||
|
else
|
||||||
|
scope = scope.by_votes.by_views.by_update
|
||||||
|
end
|
||||||
|
|
||||||
@topic_count = scope.count
|
|
||||||
@limit = per_page_option
|
@limit = per_page_option
|
||||||
@topic_pages = Paginator.new(self, @topic_count, @limit, params[:page])
|
@offset = params[:page].to_i*@limit
|
||||||
@offset = @topic_pages.current.offset
|
|
||||||
scope = scope.limit(@limit).offset(@offset)
|
scope = scope.limit(@limit).offset(@offset)
|
||||||
scope = scope.tagged_with(params[:tag]) unless params[:tag].blank?
|
scope = scope.tagged_with(params[:tag]) unless params[:tag].blank?
|
||||||
@topics = scope
|
|
||||||
|
@topic_count = scope.count
|
||||||
|
@topic_pages = Paginator.new @topic_count, @limit, params[:page]
|
||||||
|
|
||||||
|
@question_items = scope
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_topic
|
def find_section
|
||||||
@topic = Message.visible.find(params[:id]) unless params[:id].blank?
|
@section = QuestionsSection.find_by_id(params[:section_id] || (params[:question] && params[:question][:section_id]))
|
||||||
@board = @topic.board
|
@section ||= @project.questions_sections.first
|
||||||
@project = @board.project
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_optional_board
|
def find_question
|
||||||
@board = Board.visible.find(params[:board_id]) unless params[:board_id].blank?
|
if Redmine::VERSION.to_s =~ /^2.6/
|
||||||
@project = @board.project if @board
|
@question_item = Question.visible.find(params[:id], readonly: false)
|
||||||
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
|
else
|
||||||
allowed ? true : deny_access
|
@question_item = Question.visible.find(params[:id])
|
||||||
|
end
|
||||||
|
return deny_access unless @question_item.visible?
|
||||||
|
@project = @question_item.project
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
# 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 QuestionsSectionsController < ApplicationController
|
||||||
|
menu_item :questions
|
||||||
|
|
||||||
|
before_action :find_section, :only => [:edit, :update, :destroy]
|
||||||
|
before_action :find_optional_project, :only => [:index, :new, :create]
|
||||||
|
|
||||||
|
helper :questions
|
||||||
|
|
||||||
|
def new
|
||||||
|
@section = @project.questions_sections.build
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@section = @project.nil? ? QuestionsSection.new : @project.questions_sections.build
|
||||||
|
@section.safe_attributes = params[:questions_section]
|
||||||
|
if @section.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash[:notice] = l(:notice_successful_create)
|
||||||
|
redirect_to_settings_in_projects
|
||||||
|
end
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render :action => 'new' }
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@section.safe_attributes = params[:questions_section]
|
||||||
|
if @section.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash[:notice] = l(:notice_successful_update)
|
||||||
|
redirect_to_settings_in_projects
|
||||||
|
end
|
||||||
|
format.js { head 200 }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render :action => 'edit' }
|
||||||
|
format.js { head 422 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@section.destroy
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to_settings_in_projects }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
ApplicationController.menu_item :questions
|
||||||
|
@question_item = Question.new
|
||||||
|
@sections = QuestionsSection.visible.order(:project_id).sorted.for_project(@project)
|
||||||
|
redirect_to project_questions_path(:section_id => @sections.last, :project_id => @sections.last.project) if @sections.size == 1
|
||||||
|
|
||||||
|
@sections = @sections.with_questions_count
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_section
|
||||||
|
@section = QuestionsSection.find(params[:id])
|
||||||
|
@project = @section.project
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_to_settings_in_projects
|
||||||
|
redirect_back_or_default( @project ? settings_project_path(@project, :tab => 'questions') : plugin_settings_path(:id => "redmine_questions"))
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,82 @@
|
||||||
|
# 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 QuestionsStatusesController < ApplicationController
|
||||||
|
unloadable
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :require_admin, :except => :index
|
||||||
|
before_action :require_admin_or_api_request, :only => :index
|
||||||
|
|
||||||
|
accept_api_auth :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
respond_to do |format|
|
||||||
|
format.api {
|
||||||
|
@questions_statuses = QuestionsStatus.sorted
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@questions_status = QuestionsStatus.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@questions_status = QuestionsStatus.new(params[:questions_status])
|
||||||
|
if request.post? && @questions_status.save
|
||||||
|
flash[:notice] = l(:notice_successful_create)
|
||||||
|
redirect_to :action => "plugin", :id => "redmine_questions", :controller => "settings", :tab => 'questions_statuses'
|
||||||
|
else
|
||||||
|
render :action => 'new'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@questions_status = QuestionsStatus.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@questions_status = QuestionsStatus.find(params[:id])
|
||||||
|
if @questions_status.update_attributes(params[:questions_status])
|
||||||
|
respond_to do |format|
|
||||||
|
format.html {
|
||||||
|
flash[:notice] = l(:notice_successful_update)
|
||||||
|
redirect_to :action => 'plugin', :id => 'redmine_questions', :controller => 'settings', :tab => 'questions_statuses'
|
||||||
|
}
|
||||||
|
format.js { head 200 }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render :action => 'edit' }
|
||||||
|
format.js { head 422 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
QuestionsStatus.find(params[:id]).destroy
|
||||||
|
redirect_to :action =>"plugin", :id => "redmine_questions", :controller => "settings", :tab => 'questions_statuses'
|
||||||
|
rescue
|
||||||
|
flash[:error] = l(:error_products_unable_delete_questions_status)
|
||||||
|
redirect_to :action =>"plugin", :id => "redmine_questions", :controller => "settings", :tab => 'questions_statuses'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
|
@ -1,15 +1,51 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
module QuestionsHelper
|
module QuestionsHelper
|
||||||
def vote_tag(object, user, options={})
|
def question_status_tag(status)
|
||||||
content_tag("span", vote_link(object, user))
|
return '' unless status
|
||||||
|
content_tag(:span, status.name, :class => 'question-status-tag tag-label-color', :style => "background-color: #{status.color}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def vote_link(object, user)
|
def allow_voting?(votable, user = User.current)
|
||||||
return '' unless user && user.logged? && user.respond_to?('voted_for?')
|
(votable.author == user && QuestionsSettings.vote_own? || votable.author != user) &&
|
||||||
voted = user.voted_for?(object)
|
user.allowed_to?(:vote_questions, votable.project)
|
||||||
url = {:controller => 'questions', :action => 'vote', :id => object}
|
end
|
||||||
link_to((voted ? l(:button_questions_unvote) : l(:button_questions_vote)), url,
|
|
||||||
:class => (voted ? 'icon icon-vote' : 'icon icon-unvote'))
|
|
||||||
|
|
||||||
end
|
def question_breadcrumb(item)
|
||||||
|
links = []
|
||||||
|
links << link_to(l(:label_questions), { :controller => 'questions_sections', :action => 'index', :project_id => nil})
|
||||||
|
links << link_to(item.project.name, { :controller => 'questions_sections', :action => 'index', :project_id => item.project }) if item && item.project
|
||||||
|
links << link_to(item.section.name, { :controller => 'questions', :action => 'index', :project_id => item.project, :section_id => item.section }) if item && item.is_a?(Question) && item.section.present?
|
||||||
|
breadcrumb links
|
||||||
|
end
|
||||||
|
|
||||||
|
def global_modificator
|
||||||
|
return {:global => true} if !@project
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
def path_to_sections
|
||||||
|
return project_questions_sections_path if @project
|
||||||
|
questions_sections_path
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
290
plugins/redmine_questions/app/models/question.rb
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
# 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
|
126
plugins/redmine_questions/app/models/questions_answer.rb
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
# 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 QuestionsAnswer < ActiveRecord::Base
|
||||||
|
unloadable
|
||||||
|
|
||||||
|
include Redmine::SafeAttributes
|
||||||
|
extend ApplicationHelper
|
||||||
|
|
||||||
|
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||||
|
belongs_to :question, :counter_cache => 'answers_count', :touch => true
|
||||||
|
|
||||||
|
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||||
|
|
||||||
|
acts_as_attachable_questions
|
||||||
|
|
||||||
|
acts_as_event :datetime => :created_on,
|
||||||
|
:url => Proc.new {|o| {:controller => 'questions', :action => 'show', :id => o.question, :anchor => "questions_answer_#{o.id}" }},
|
||||||
|
:group => :question,
|
||||||
|
:type => Proc.new {|o| 'icon icon-reply'},
|
||||||
|
:description => :content,
|
||||||
|
:title => Proc.new {|o| o.question.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({ :question => { :section => :project } }, :author)
|
||||||
|
else
|
||||||
|
acts_as_activity_provider :type => 'questions',
|
||||||
|
:permission => :view_questions,
|
||||||
|
:author_key => :author_id,
|
||||||
|
:timestamp => "#{table_name}.created_on",
|
||||||
|
:find_options => { :joins => [{ :question => { :section => :project } }, :author] }
|
||||||
|
end
|
||||||
|
|
||||||
|
scope :visible, lambda {|*args| where(Question.visible_condition(args.shift || User.current)) }
|
||||||
|
scope :by_votes, lambda { order("#{table_name}.cached_weighted_score DESC") }
|
||||||
|
scope :by_accepted, lambda { order("#{table_name}.accepted DESC") }
|
||||||
|
scope :by_date, lambda { order("#{table_name}.created_on DESC") }
|
||||||
|
scope :featured, lambda {|*args| where(:featured => true) }
|
||||||
|
|
||||||
|
validates_presence_of :question, :author, :content
|
||||||
|
validate :cannot_answer_to_locked_question, :on => :create
|
||||||
|
|
||||||
|
after_create :add_author_as_watcher
|
||||||
|
after_create :send_notification
|
||||||
|
|
||||||
|
safe_attributes 'author',
|
||||||
|
'content'
|
||||||
|
|
||||||
|
def commentable?(user = User.current)
|
||||||
|
return false if question.locked?
|
||||||
|
user.allowed_to?(:comment_question, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cannot_answer_to_locked_question
|
||||||
|
# Can not reply to a locked topic
|
||||||
|
errors.add :base, 'Question is locked' if question && question.locked?
|
||||||
|
end
|
||||||
|
|
||||||
|
def section_name
|
||||||
|
question.try(:section).try(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def project
|
||||||
|
question.project if question
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_voting?
|
||||||
|
question && question.section && question.section.allow_voting?
|
||||||
|
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)
|
||||||
|
user.allowed_to?(:edit_questions, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroyable_by?(user)
|
||||||
|
user.allowed_to?(:delete_answers, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def votable_by?(user)
|
||||||
|
user.allowed_to?(:vote_questions, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def check_accepted
|
||||||
|
question.answers.update_all(:accepted => false) if question &&
|
||||||
|
accepted? &&
|
||||||
|
accepted_changed?
|
||||||
|
end
|
||||||
|
# </PRO>
|
||||||
|
|
||||||
|
def add_author_as_watcher
|
||||||
|
Watcher.create(:watchable => question, :user => author)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
Mailer.question_answer_added(self).deliver if Setting.notified_events.include?('question_answer_added')
|
||||||
|
end
|
||||||
|
end
|
92
plugins/redmine_questions/app/models/questions_section.rb
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
# 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 QuestionsSection < ActiveRecord::Base
|
||||||
|
unloadable
|
||||||
|
include Redmine::SafeAttributes
|
||||||
|
|
||||||
|
belongs_to :project
|
||||||
|
has_many :questions, :foreign_key => "section_id", :dependent => :destroy
|
||||||
|
|
||||||
|
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||||
|
safe_attributes 'name', 'project', 'position', 'description', 'section_type'
|
||||||
|
|
||||||
|
scope :with_questions_count, lambda { select("#{QuestionsSection.table_name}.*, count(#{QuestionsSection.table_name}.id) as questions_count").joins(:questions).order("project_id ASC").group("#{QuestionsSection.table_name}.id, #{QuestionsSection.table_name}.name, #{QuestionsSection.table_name}.project_id, #{QuestionsSection.table_name}.section_type") }
|
||||||
|
scope :for_project, lambda { |project| where(:project_id => project) unless project.blank? }
|
||||||
|
scope :visible, lambda {|*args|
|
||||||
|
joins(:project).
|
||||||
|
where(Project.allowed_to_condition(args.shift || User.current, :view_questions, *args))
|
||||||
|
}
|
||||||
|
scope :sorted, lambda { order(:position) }
|
||||||
|
|
||||||
|
rcrm_acts_as_list :scope => 'project_id = #{project_id}'
|
||||||
|
acts_as_watchable
|
||||||
|
|
||||||
|
SECTION_TYPE_QUESTIONS = 'questions'
|
||||||
|
|
||||||
|
validates_presence_of :section_type, :project_id, :name
|
||||||
|
validates_uniqueness_of :name, :scope => :project_id
|
||||||
|
|
||||||
|
def initialize(attributes=nil, *args)
|
||||||
|
super
|
||||||
|
if new_record?
|
||||||
|
# set default values for new records only
|
||||||
|
self.section_type ||= SECTION_TYPE_QUESTIONS
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
"#{id}-#{ActiveSupport::Inflector.transliterate(name).parameterize}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_questions?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_solutions?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_ideas?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_voting?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_liking?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_answering?
|
||||||
|
is_questions?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.types_list
|
||||||
|
end
|
||||||
|
|
||||||
|
def l_type
|
||||||
|
I18n.t("label_questions_section_type_#{section_type}") if section_type
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
name
|
||||||
|
end
|
||||||
|
end
|
47
plugins/redmine_questions/app/models/questions_settings.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# 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 QuestionsSettings
|
||||||
|
unloadable
|
||||||
|
|
||||||
|
IDEA_COLORS = {
|
||||||
|
:green => 'green',
|
||||||
|
:blue => 'blue',
|
||||||
|
:turquoise => 'turquoise',
|
||||||
|
:light_green => 'lightgreen',
|
||||||
|
:yellow => 'yellow',
|
||||||
|
:orange => 'orange',
|
||||||
|
:red => 'red',
|
||||||
|
:purple => 'purple',
|
||||||
|
:gray => 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
class << self
|
||||||
|
|
||||||
|
def vote_own?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_popular?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
38
plugins/redmine_questions/app/models/questions_status.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# 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 QuestionsStatus < ActiveRecord::Base
|
||||||
|
unloadable
|
||||||
|
include Redmine::SafeAttributes
|
||||||
|
|
||||||
|
belongs_to :question
|
||||||
|
|
||||||
|
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||||
|
safe_attributes 'name', 'is_closed', 'position', 'color'
|
||||||
|
|
||||||
|
validates :name, :presence => true, :uniqueness => true
|
||||||
|
|
||||||
|
scope :sorted, lambda { order(:position) }
|
||||||
|
|
||||||
|
rcrm_acts_as_list
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
name
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
<%= raw @questions_tags.map { |question_tag| {
|
||||||
|
'id' => @names_only ? question_tag.name : question_tag.id,
|
||||||
|
'text' => question_tag.name
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
%>
|
|
@ -1 +0,0 @@
|
||||||
<%= render :partial => "questions/forums" %>
|
|
|
@ -1,64 +0,0 @@
|
||||||
<%= board_breadcrumb(@board) %>
|
|
||||||
|
|
||||||
<div class="board details">
|
|
||||||
<div class="contextual">
|
|
||||||
<%= link_to_if_authorized l(:label_message_new),
|
|
||||||
{:controller => 'messages', :action => 'new', :board_id => @board},
|
|
||||||
:class => 'icon icon-add',
|
|
||||||
:onclick => 'showAndScrollTo("add-message", "message_subject"); return false;' %>
|
|
||||||
<%= content_tag('span', watcher_link(@board, User.current), :id => 'watcher') %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="add-message" style="display:none;">
|
|
||||||
<% if authorize_for('messages', 'new') %>
|
|
||||||
<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%= l(:label_message_new) %></h2>
|
|
||||||
<%= form_for @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
|
|
||||||
<%= render :partial => 'messages/form', :locals => {:f => f} %>
|
|
||||||
<p><%= submit_tag l(:button_create) %>
|
|
||||||
<%= preview_link({:controller => 'messages', :action => 'preview', :board_id => @board}, 'message-form') %> |
|
|
||||||
<%= link_to l(:button_cancel), "#", :onclick => '$("#add-message").hide(); return false;' %></p>
|
|
||||||
<% end %>
|
|
||||||
<div id="preview" class="wiki"></div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2><%=h @board.name %></h2>
|
|
||||||
<p class="subtitle"><%=h @board.description %></p>
|
|
||||||
|
|
||||||
<div class="filters">
|
|
||||||
<%= form_tag({:controller => "questions", :action => "topics" }, :method => :get, :id => "query_form") do %>
|
|
||||||
<%= hidden_field_tag('project_id', @project.to_param) if @project %>
|
|
||||||
<%= hidden_field_tag('board_id', @board.to_param) if @board %>
|
|
||||||
<% no_filters = true %>
|
|
||||||
|
|
||||||
<%= text_field_tag(:topic_search, params[:topic_search], :autocomplete => "off", :class => "questions-search", :placeholder => l(:label_questions_search) ) %>
|
|
||||||
<%= javascript_tag "observeSearchfield('topic_search', 'topics_list', '#{ escape_javascript(autocomplete_for_topic_questions_path(:project_id => @project, :board_id => @board)) }')" %>
|
|
||||||
|
|
||||||
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="topics_list" >
|
|
||||||
<%= render :partial => "questions/topic_list" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% other_formats_links do |f| %>
|
|
||||||
<%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
|
|
||||||
<% content_for :sidebar do %>
|
|
||||||
<%= render :partial => "questions/notice" %>
|
|
||||||
<%= render :partial => "questions/tag_cloud" %>
|
|
||||||
<%= render :partial => "questions/voted_topics" %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% html_title @board.name %>
|
|
||||||
|
|
||||||
<% content_for :header_tags do %>
|
|
||||||
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
|
|
||||||
<%= javascript_include_tag :questions, :plugin => 'redmine_questions' %>
|
|
||||||
<% end %>
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<h1><%= @question.section_name %>: <%= link_to(@question.subject, @question_url) %></h1>
|
||||||
|
|
||||||
|
<p><%= l(:text_user_wrote, :value => h(@answer.author)) %></p>
|
||||||
|
|
||||||
|
<%= textilizable @answer, :content, :only_path => false %>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<%= @question.section_name %>: <%= @question.subject %>
|
||||||
|
<%= @question_url %>
|
||||||
|
|
||||||
|
<%= l(:text_user_wrote, :value => @answer.author) %>
|
||||||
|
|
||||||
|
<%= @answer.content %>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<h1><%= @question.section_name %>: <%= link_to(@question.subject, @question_url) %></h1>
|
||||||
|
|
||||||
|
<p><%= l(:text_user_wrote, :value => h(@comment.author)) %></p>
|
||||||
|
|
||||||
|
<%= textilizable @comment, :comments, :only_path => false %>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<%= @question.section_name %>: <%= @question.subject %>
|
||||||
|
<%= @question_url %>
|
||||||
|
|
||||||
|
<%= l(:text_user_wrote, :value => @comment.author) %>
|
||||||
|
|
||||||
|
<%= @comment.comments %>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<h1><%= @question.section_name %>: <%= link_to(@question.subject, @question_url) %></h1>
|
||||||
|
|
||||||
|
<p><%= l(:text_user_wrote, :value => h(@question.author)) %></p>
|
||||||
|
|
||||||
|
<%= textilizable @question, :content, :only_path => false %>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<%= @question.section_name %>: <%= @question.subject %>
|
||||||
|
<%= @question_url %>
|
||||||
|
|
||||||
|
<%= l(:text_user_wrote, :value => @question.author) %>
|
||||||
|
|
||||||
|
<%= @question.content %>
|
|
@ -1,39 +0,0 @@
|
||||||
<%= error_messages_for 'message' %>
|
|
||||||
<% replying ||= false %>
|
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<!--[form:message]-->
|
|
||||||
<p style=<%= "display:none;" if replying %> ><label for="message_subject"><%= l(:field_subject) %></label><br />
|
|
||||||
<%= f.text_field :subject, :style => "width: 80%;", :id => "message_subject" %>
|
|
||||||
<% unless replying %>
|
|
||||||
|
|
||||||
<% if @message.safe_attribute? 'sticky' %>
|
|
||||||
<%= f.check_box :sticky %> <%= label_tag 'message_sticky', l(:label_board_sticky) %>
|
|
||||||
<% end %>
|
|
||||||
<% if @message.safe_attribute? 'locked' %>
|
|
||||||
<%= f.check_box :locked %> <%= label_tag 'message_locked', l(:label_board_locked) %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<% if !replying && !@message.new_record? && @message.safe_attribute?('board_id') %>
|
|
||||||
<p><label><%= l(:label_board) %></label><br />
|
|
||||||
<%= f.select :board_id, boards_options_for_select(@message.project.boards) %></p>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<%= label_tag "message_content", l(:description_message_content), :class => "hidden-for-sighted" %>
|
|
||||||
<%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %></p>
|
|
||||||
<%= wikitoolbar_for 'message_content' %>
|
|
||||||
<!--[eoform:message]-->
|
|
||||||
|
|
||||||
<% if !replying && @message.safe_attribute?('tag_list') %>
|
|
||||||
<p>
|
|
||||||
<label for="message_tag_list"><%= l(:field_questions_tags) %></label><br />
|
|
||||||
<%= render :partial => 'questions/form_tags' %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<p><%= l(:label_attachment_plural) %><br />
|
|
||||||
<%= render :partial => 'attachments/form', :locals => {:container => @message} %></p>
|
|
||||||
</div>
|
|
|
@ -1,18 +0,0 @@
|
||||||
<%= board_breadcrumb(@message) %>
|
|
||||||
|
|
||||||
<h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
|
|
||||||
|
|
||||||
<%= form_for @message, {
|
|
||||||
:as => :message,
|
|
||||||
:url => {:action => 'edit'},
|
|
||||||
:html => {:multipart => true,
|
|
||||||
:id => 'message-form',
|
|
||||||
:method => :post}
|
|
||||||
} do |f| %>
|
|
||||||
<%= render :partial => 'form',
|
|
||||||
:locals => {:f => f, :replying => !@message.parent.nil?} %>
|
|
||||||
<%= submit_tag l(:button_save) %>
|
|
||||||
<%= preview_link({:controller => 'messages', :action => 'preview', :board_id => @board, :id => @message}, 'message-form') %>
|
|
||||||
<% end %>
|
|
||||||
<div id="preview" class="wiki"></div>
|
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
<%= board_breadcrumb(@message) %>
|
|
||||||
|
|
||||||
<div class="contextual">
|
|
||||||
<%= content_tag('span', watcher_link(@topic, User.current), :id => 'watcher') %>
|
|
||||||
<% voted = User.current.voted_for?(@topic) %>
|
|
||||||
<%= link_to(
|
|
||||||
voted ? l(:button_questions_unvote) : l(:button_questions_vote),
|
|
||||||
{:controller => 'questions', :action => 'vote', :id => @topic},
|
|
||||||
:class => 'icon ' + (voted ? 'icon-vote' : 'icon-unvote')
|
|
||||||
) if User.current.allowed_to?(:vote_messages, @project) %>
|
|
||||||
|
|
||||||
<%= link_to(
|
|
||||||
l(:button_quote),
|
|
||||||
{:action => 'quote', :id => @topic},
|
|
||||||
:remote => true,
|
|
||||||
:method => 'get',
|
|
||||||
:class => 'icon icon-comment',
|
|
||||||
:remote => true) if !@topic.locked? && authorize_for('messages', 'reply') %>
|
|
||||||
<%= link_to(
|
|
||||||
l(:button_edit),
|
|
||||||
{:action => 'edit', :id => @topic},
|
|
||||||
:class => 'icon icon-edit'
|
|
||||||
) if @message.editable_by?(User.current) %>
|
|
||||||
<%= link_to(
|
|
||||||
l(:button_delete),
|
|
||||||
{:action => 'destroy', :id => @topic},
|
|
||||||
:method => :post,
|
|
||||||
:data => {:confirm => l(:text_are_you_sure)},
|
|
||||||
:class => 'icon icon-del'
|
|
||||||
) if @message.destroyable_by?(User.current) %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="message details">
|
|
||||||
<h2><%=h @topic.subject %></h2>
|
|
||||||
<%= avatar(@topic.author, :size => "32") %>
|
|
||||||
<p class="author"><%= link_to_user @topic.author %><br>
|
|
||||||
<%= l(:label_questions_added_time, :value => time_tag(@topic.created_on)).html_safe %>
|
|
||||||
</p>
|
|
||||||
<div class="wiki">
|
|
||||||
<%= textilizable(@topic, :content) %>
|
|
||||||
</div>
|
|
||||||
<%= link_to_attachments @topic, :author => false %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<% unless @replies.empty? %>
|
|
||||||
<h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
|
|
||||||
<% @replies.each do |message| %>
|
|
||||||
<div class="message reply" id="<%= "message-#{message.id}" %>">
|
|
||||||
<div class="contextual">
|
|
||||||
<% liked = User.current.voted_for?(message) %>
|
|
||||||
<%= link_to(
|
|
||||||
message.count_votes_up > 0 ? "(#{message.count_votes_up})" : "",
|
|
||||||
{:controller => 'questions', :action => 'vote', :id => message},
|
|
||||||
:class => 'vote icon ' + (liked ? 'icon-vote' : 'icon-unvote')
|
|
||||||
) if true || User.current.allowed_to?(:vote_messages, @project) %>
|
|
||||||
<%= link_to(
|
|
||||||
image_tag('comment.png'),
|
|
||||||
{:action => 'quote', :id => message},
|
|
||||||
:remote => true,
|
|
||||||
:method => 'get',
|
|
||||||
:title => l(:button_quote)) if !@topic.locked? && authorize_for('messages', 'reply') %>
|
|
||||||
<%= link_to(
|
|
||||||
image_tag('edit.png'),
|
|
||||||
{:action => 'edit', :id => message},
|
|
||||||
:title => l(:button_edit)
|
|
||||||
) if message.editable_by?(User.current) %>
|
|
||||||
<%= link_to(
|
|
||||||
image_tag('delete.png'),
|
|
||||||
{:action => 'destroy', :id => message},
|
|
||||||
:method => :post,
|
|
||||||
:data => {:confirm => l(:text_are_you_sure)},
|
|
||||||
:title => l(:button_delete)
|
|
||||||
) if message.destroyable_by?(User.current) %>
|
|
||||||
</div>
|
|
||||||
<% if Setting.gravatar_enabled? %>
|
|
||||||
<div class="avatar">
|
|
||||||
<%= message_avatar = avatar(message.author, :size => "32") %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="reply-details <%= 'use-avatar' unless message_avatar.blank? %>">
|
|
||||||
<h4 class="author"><%= authoring message.created_on, message.author %></h4>
|
|
||||||
<div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
|
|
||||||
<%= link_to_attachments message, :author => false %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<p class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></p>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if !@topic.locked? && authorize_for('messages', 'reply') %>
|
|
||||||
<p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
|
|
||||||
<div id="reply" style="display:none;">
|
|
||||||
<%= form_for @reply, :as => :reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
|
|
||||||
<%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
|
|
||||||
<%= submit_tag l(:button_submit) %>
|
|
||||||
<%= preview_link({:controller => 'messages', :action => 'preview', :board_id => @board}, 'message-form') %>
|
|
||||||
<% end %>
|
|
||||||
<div id="preview" class="wiki"></div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
|
|
||||||
<% content_for :sidebar do %>
|
|
||||||
<h3><%= l(:label_questions_message) %></h3>
|
|
||||||
<ul class="question-meta">
|
|
||||||
<li class="votes icon icon-vote">
|
|
||||||
<%= l(:label_questions_votes, :count => @topic.count_votes_up - @topic.count_votes_down ) %>
|
|
||||||
</li>
|
|
||||||
<li class="views icon icon-view">
|
|
||||||
<%= l(:label_questions_views, :count => @topic.view_count ) %>
|
|
||||||
</li>
|
|
||||||
<% unless @topic.tags.blank? %>
|
|
||||||
<li class="tags icon icon-tag">
|
|
||||||
<%=
|
|
||||||
@topic.tags.collect do |tag|
|
|
||||||
link_to tag, {:controller => "questions", :action => "topics", :project_id => @project, :tag => tag.name}
|
|
||||||
end.join(', ').html_safe
|
|
||||||
%>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3><%= l(:label_questions_related_messages) %></h3>
|
|
||||||
<ul class="related-topics">
|
|
||||||
<%# Board.all.map(&:topics).flatten.first(5).each do |topic| %>
|
|
||||||
<% tokens = @topic.subject.strip.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} || "" %>
|
|
||||||
<% if ActiveRecord::VERSION::MAJOR >= 4 %>
|
|
||||||
<% related_topics = Message.where(tokens.map{ |t| "subject LIKE '%#{t}%'" }.join(' OR ')).to_a.compact if tokens %>
|
|
||||||
<% else %>
|
|
||||||
<% related_topics = Message.search(tokens, @project, :limit => 5)[0].select{|m| m != @topic && m.parent_id == nil }.compact if tokens %>
|
|
||||||
<% end %>
|
|
||||||
<% related_topics.each do |topic| %>
|
|
||||||
<li class="related-topic">
|
|
||||||
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => topic.board, :id => topic } %>
|
|
||||||
</li>
|
|
||||||
<% end if related_topics %>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% html_title @topic.subject %>
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<h3><%= l(:label_questions_sections_plural) %></h3>
|
||||||
|
|
||||||
|
<table class="list questions_sections">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><%= l(:field_name) %></th>
|
||||||
|
<th><%=l(:field_type)%></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% QuestionsSection.for_project(@project).sorted.each do |section| %>
|
||||||
|
<tr class="<%= cycle 'odd', 'even' %>">
|
||||||
|
<td class="name">
|
||||||
|
<%= h(section.name) %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= section.l_type %>
|
||||||
|
</td>
|
||||||
|
<td class="buttons">
|
||||||
|
<% if User.current.allowed_to?(:manage_sections, @project) %>
|
||||||
|
<%= reorder_handle(section, :url => project_questions_section_path(@project, section), :param => 'questions_section') if respond_to?(:reorder_handle) %>
|
||||||
|
<%= link_to l(:button_edit), {:controller => 'questions_sections', :action => 'edit', :project_id => @project, :id => section}, :class => 'icon icon-edit' %>
|
||||||
|
<%= delete_link :controller => 'questions_sections', :action => 'destroy', :project_id => @project, :id => section %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<% if User.current.allowed_to?(:manage_sections, @project) %>
|
||||||
|
<%= link_to image_tag('add.png', :style => 'vertical-align: middle;')+l(:label_questions_section_new), :controller => 'questions_sections', :action => 'new', :project_id => @project %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= javascript_tag do %>
|
||||||
|
$(function() { $("table.questions_sections tbody").positionedItems(); });
|
||||||
|
<% end if respond_to?(:reorder_handle) %>
|
43
plugins/redmine_questions/app/views/questions/_form.html.erb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<%= error_messages_for @question_item %>
|
||||||
|
<%= fields_for :question, @question_item do |f| %>
|
||||||
|
<div class="box">
|
||||||
|
<!--[form:message]-->
|
||||||
|
<p><label for="message_subject"><%= l(:field_subject) %></label><br />
|
||||||
|
<%= f.text_field :subject, :id => "question_subject", :size => 120%>
|
||||||
|
</p>
|
||||||
|
<p><label><%= l(:label_questions_section) %></label><br />
|
||||||
|
<%= f.select :section_id, options_from_collection_for_select(QuestionsSection.where(:project_id => @project),:id, :name, f.object.section_id), :style => "width: 80%;", :required => true %>
|
||||||
|
<%= javascript_tag do %>
|
||||||
|
$('#question_section_id').change(function() {
|
||||||
|
$.ajax({
|
||||||
|
url: '<%= escape_javascript update_form_questions_path(:id => @question_item, :format => 'js', :project_id => @project) %>',
|
||||||
|
type: 'put',
|
||||||
|
data: $('#question_form').serialize()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
<% end %>
|
||||||
|
<%= link_to(image_tag('add.png', :style => 'vertical-align: middle;'),
|
||||||
|
(@project ? new_project_questions_section_url(:project_id => @project) : new_questions_section_url ),
|
||||||
|
:remote => true,
|
||||||
|
:method => 'get',
|
||||||
|
:title => l(:label_questions_section_new),
|
||||||
|
:class => "add_section",
|
||||||
|
:tabindex => 200) if User.current.allowed_to?(:manage_sections, @project) %>
|
||||||
|
</p>
|
||||||
|
<% if @question_item.is_idea? %>
|
||||||
|
<p><label><%= l(:field_status) %></label><br />
|
||||||
|
<%= f.select :status_id, options_from_collection_for_select(QuestionsStatus.sorted,:id, :name, @question_item.status_id ), :include_blank => true %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<p>
|
||||||
|
<%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'question_content', :label => l(:label_questions_message) %>
|
||||||
|
</p>
|
||||||
|
<%= wikitoolbar_for 'question_content' %>
|
||||||
|
<!--[eoform:question]-->
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= l(:label_attachment_plural) %><br />
|
||||||
|
<%= render :partial => 'attachments/form', :locals => {:container => @question_item} %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -1,23 +0,0 @@
|
||||||
<script type="text/javascript">
|
|
||||||
$(function(){
|
|
||||||
<% available_tags = RedmineCrm::Tag.joins(:taggings).joins("INNER JOIN messages ON taggings.taggable_id = messages.id AND taggings.taggable_type = 'Message'").joins("INNER JOIN boards ON messages.board_id = boards.id").where(["boards.project_id = ?", @project]) %>
|
|
||||||
var currentTags = ['<%= available_tags.map(&:name).join("\',\'").html_safe %>'];
|
|
||||||
|
|
||||||
$('#allowSpacesTags').tagit({
|
|
||||||
availableTags: currentTags,
|
|
||||||
allowSpaces: true,
|
|
||||||
caseSensitive: false,
|
|
||||||
removeConfirmation: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span class="message-tags-edit">
|
|
||||||
<%= text_field_tag 'message[tag_list]', "#{@message.tags.map(&:name).join(',').html_safe}", :size => 10, :class => 'hol', :id => "allowSpacesTags" %>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
|
|
||||||
<% content_for :header_tags do %>
|
|
||||||
<%= javascript_include_tag :"tag-it", :plugin => 'redmine_questions' %>
|
|
||||||
<%= stylesheet_link_tag :"jquery.tagit.css", :plugin => 'redmine_questions' %>
|
|
||||||
<% end %>
|
|
|
@ -1,68 +0,0 @@
|
||||||
<h2><%= l(:label_questions) %></h2>
|
|
||||||
|
|
||||||
<div class="filters">
|
|
||||||
<%= form_tag({:controller => "questions", :action => "topics"}, :method => :get, :id => "query_form") do %>
|
|
||||||
<%= hidden_field_tag('project_id', @project.to_param) if @project %>
|
|
||||||
<%= text_field_tag(:topic_search, params[:topic_search] , :autocomplete => "off", :class => "questions-search", :placeholder => l(:label_questions_search) ) %>
|
|
||||||
<%= javascript_tag "observeSearchfield('topic_search', 'forum_list', '#{ escape_javascript(autocomplete_for_topic_questions_path(:project_id => @project, :board_id => @board)) }')" %>
|
|
||||||
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="forum_list">
|
|
||||||
<% previous_group = false %>
|
|
||||||
<% boards = @project ? @boards : @boards.select{|b| b.topics_count > 0} %>
|
|
||||||
<% if @project %>
|
|
||||||
<ul>
|
|
||||||
<% end %>
|
|
||||||
<% boards.each do |board| %>
|
|
||||||
<% cache(Message.last.updated_on.to_s + board.id.to_s) do %>
|
|
||||||
<% if @project.blank? && (group = board.project) != previous_group %>
|
|
||||||
<% reset_cycle %>
|
|
||||||
</ul>
|
|
||||||
<div class="project-forums">
|
|
||||||
<h3><%= group.blank? ? 'None' : group.name %><%= link_to " \xc2\xbb", project_boards_path(:project_id => group) %></h3>
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
<% previous_group = group %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<li class="<%= cycle('odd', 'even') %> ">
|
|
||||||
<h3>
|
|
||||||
<%= link_to h(board.name), {:controller => "boards", :action => 'show', :id => board, :project_id => board.project_id}, :class => "board" %>
|
|
||||||
<span class="topic-count"><%= "(#{board.topics.count})" %></span>
|
|
||||||
</h3>
|
|
||||||
<div class="topic-list">
|
|
||||||
<% board.topics.sort_by{|m| [m.sticky, m.updated_on] }.reverse.first(5).each do |topic| %>
|
|
||||||
<div class="list-item">
|
|
||||||
<span class="topic-subject">
|
|
||||||
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => board, :id => topic } %>
|
|
||||||
</span><br>
|
|
||||||
<span class="last-author">
|
|
||||||
<% last_update = [topic.last_reply ? topic.last_reply.updated_on : topic.created_on, topic.updated_on].max %>
|
|
||||||
<% last_author = (topic.last_reply && topic.last_reply.updated_on) ? topic.last_reply.author : topic.author %>
|
|
||||||
|
|
||||||
<%= authoring last_update, last_author, :label => :label_updated_time_by %><br />
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% content_for :sidebar do %>
|
|
||||||
<%= render :partial => "questions/notice" %>
|
|
||||||
<%= render :partial => "questions/tag_cloud" %>
|
|
||||||
<%= render :partial => "questions/latest_topics" %>
|
|
||||||
<%= render :partial => "questions/voted_topics" %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% content_for :header_tags do %>
|
|
||||||
<%= javascript_include_tag :questions, :plugin => 'redmine_questions' %>
|
|
||||||
<% end %>
|
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
<%
|
|
||||||
scope = Message.where({})
|
|
||||||
scope = scope.where("#{Message.table_name}.parent_id IS NULL")
|
|
||||||
scope = scope.where(["#{Board.table_name}.project_id = ?", @project.id]) if @project
|
|
||||||
@latest_topics = scope.visible.includes(:board).order("#{Message.table_name}.created_on DESC").limit(5)
|
|
||||||
%>
|
|
||||||
<h3><%= l(:label_questions_latest_messages) %></h3>
|
<h3><%= l(:label_questions_latest_messages) %></h3>
|
||||||
<ul class="related-topics">
|
<ul class="related-topics">
|
||||||
<% @latest_topics.each do |topic| %>
|
<% Question.visible.by_date.in_project(@project).limit(5).each do |question| %>
|
||||||
<li class="related-topic">
|
<li class="related-topic">
|
||||||
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => topic.board, :id => topic } %>
|
<%= link_to h(question.subject), { :controller => 'questions', :action => 'show', :id => question, :project_id => @project } %>
|
||||||
</li>
|
</li>
|
||||||
<% end unless @latest_topics.blank? %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<% unless Setting.plugin_redmine_questions[:sidebar_message].blank? %>
|
|
||||||
<div class="wiki"><%= textilizable(Setting.plugin_redmine_questions[:sidebar_message]) %></div>
|
|
||||||
<% end %>
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<%
|
||||||
|
@popular_topics = Question.
|
||||||
|
visible.
|
||||||
|
in_project(@project).
|
||||||
|
in_section(@section).
|
||||||
|
positive.
|
||||||
|
by_views.
|
||||||
|
by_votes.
|
||||||
|
limit(5)
|
||||||
|
%>
|
||||||
|
<h3><%= l(:label_questions_most_popular) %></h3>
|
||||||
|
<ul class="related-topics">
|
||||||
|
<% @popular_topics.each do |question| %>
|
||||||
|
<li class="related-topic">
|
||||||
|
<%= link_to h(question.subject), { :controller => 'questions', :action => 'show', :section_id => question.section, :id => question } %>
|
||||||
|
</li>
|
||||||
|
<% end unless @popular_topics.blank? %>
|
||||||
|
</ul>
|
|
@ -0,0 +1,65 @@
|
||||||
|
<div class="contextual">
|
||||||
|
<%= content_tag('span', watcher_link(@question_item, User.current), :id => 'watcher') %>
|
||||||
|
<%= link_to(l(:button_edit), edit_question_path(@question_item), :class => 'icon icon-edit' ) if @question_item.editable_by?(User.current)
|
||||||
|
%>
|
||||||
|
<%= link_to(l(:button_delete), question_path(@question_item), :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del') if @question_item.destroyable_by?(User.current)
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="question-title"><%=h @question_item.subject %></h1>
|
||||||
|
<%= render :partial => 'question_item', :object => @question_item %>
|
||||||
|
|
||||||
|
<% if @question_item.section.allow_answering? %>
|
||||||
|
<div id="answers">
|
||||||
|
<% if @answers.any? %>
|
||||||
|
<h3><%= l(:label_questions_answer_plural) %> (<%= @answer_count %>)</h3>
|
||||||
|
<% @answers.each do |answer| %>
|
||||||
|
<%= render :partial => 'questions_answers/answer_item', :locals => { :question_item => answer } %>
|
||||||
|
<% end %>
|
||||||
|
<span class="pagination"><%= pagination_links_full @answer_pages, @answer_count %></span>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @question_item.allow_answering? && User.current.allowed_to?(:add_answers, @project) %>
|
||||||
|
<h3><%= l(:label_questions_your_answer) %></h3>
|
||||||
|
<div id="reply" >
|
||||||
|
<%= form_for @answer, :as => :answer, :url => question_answers_path(@question_item), :html => {:multipart => true, :id => 'answer-form'} do |f| %>
|
||||||
|
<%= render :partial => 'questions_answers/form', :locals => {:f => f, :replying => true} %>
|
||||||
|
<%= submit_tag l(:button_submit) %>
|
||||||
|
<% end %>
|
||||||
|
<div id="preview" class="wiki"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<h3><%= l(:label_questions_message) %></h3>
|
||||||
|
<ul class="question-meta">
|
||||||
|
<li class="views icon icon-view">
|
||||||
|
<%= l(:label_questions_views, :count => @question_item.views ) %>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<% if @question_item.convertable_by?(User.current) && User.current.allowed_to?(:add_issues, @project) %>
|
||||||
|
<h3><%= l(:label_questions_actions) %></h3>
|
||||||
|
<ul class="action">
|
||||||
|
<li>
|
||||||
|
<%= link_to(
|
||||||
|
l(:button_questions_to_issue),
|
||||||
|
convert_to_issue_project_question_path(@project, @question_item)
|
||||||
|
)
|
||||||
|
%>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<h3><%= l(:label_questions_related_questions) %></h3>
|
||||||
|
<ul class="related-topics">
|
||||||
|
<% Question.visible.related(@question_item, 5).each do |question| %>
|
||||||
|
<li class="related-topic">
|
||||||
|
<%= link_to h(question.subject), { :controller => 'questions', :action => 'show', :board_id => nil, :id => question } %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<% end %>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="question<%= " votable" if question_item.allow_voting? %> div-table" id="question_<%= question_item.id %>">
|
||||||
|
<a href="#<%= question_item.id %>" class="wiki-anchor"></a>
|
||||||
|
<% if question_item.allow_voting? && User.current.allowed_to?(:vote_questions, @project) %>
|
||||||
|
<div class="vote">
|
||||||
|
<%= render :partial => 'questions_votes/question_item_vote', :locals => {:question_item => question_item} %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="question-container div-table-cell">
|
||||||
|
<%= avatar(question_item.author, :size => "32") %>
|
||||||
|
<p class="author">
|
||||||
|
<%= link_to_user question_item.author %><br>
|
||||||
|
<%= l(:label_questions_added_time, :value => time_tag(question_item.created_on)).html_safe %>
|
||||||
|
</p>
|
||||||
|
<div class="wiki">
|
||||||
|
<%= textilizable(question_item, :content) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= link_to_attachments question_item, :author => false %>
|
||||||
|
|
||||||
|
<%= render :partial => 'questions_comments/comments_container', :locals => { :question_item => question_item } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<% if @question_items && @question_items.any? %>
|
||||||
|
<% unless params[:tag].blank? %>
|
||||||
|
<div class="title-bar">
|
||||||
|
<h4><%= l(:label_questions_tagged_by, :count => @question_items.size, :tag => params[:tag]) %></h4>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div id="forum_list">
|
||||||
|
<div id="topics_container" class="<%= " votable" if @section && @section.allow_voting? %>">
|
||||||
|
<% @question_items.each do |question| %>
|
||||||
|
<div class="topic">
|
||||||
|
<% if @section && @section.allow_voting? %>
|
||||||
|
<div class="topic-vote">
|
||||||
|
<span class="vote-score"><%= question.weighted_score %></span>
|
||||||
|
<label><%= l(:label_questions_x_votes, :count => question.weighted_score.abs) %></label>
|
||||||
|
<% if question.answered? %>
|
||||||
|
<div class="status-answered" title="Answered"></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="topic-content">
|
||||||
|
<h3 class="subject">
|
||||||
|
<%= link_to h(question.subject), { :controller => 'questions', :action => 'show', :project_id => question.project, :id => question } %>
|
||||||
|
<%= question_status_tag(question.status) %>
|
||||||
|
</h3>
|
||||||
|
<p><%= truncate(Question.to_text(textilizable(question.content)), :length => 100) %></p>
|
||||||
|
<ul class="meta">
|
||||||
|
<% if question.allow_answering? %>
|
||||||
|
<li class="answers icon icon-comment"><%= l(:label_questions_answers, :count => question.answers_count) %></li>
|
||||||
|
<% end %>
|
||||||
|
<li class="views icon icon-view"><%= l(:label_questions_views, :count => question.views ) %></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if @topic_pages %>
|
||||||
|
<% params[:controller] = 'questions'
|
||||||
|
params[:action] = 'topics'
|
||||||
|
%>
|
||||||
|
<p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<p style="display: inline-block"></p>
|
||||||
|
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||||
|
<% end %>
|
|
@ -1,35 +0,0 @@
|
||||||
<%
|
|
||||||
limit = 30
|
|
||||||
|
|
||||||
scope = RedmineCrm::Tag.where({})
|
|
||||||
scope = scope.where("#{Project.table_name}.id = ?", @project) if @project
|
|
||||||
scope = scope.where(Project.allowed_to_condition(User.current, :view_messages))
|
|
||||||
|
|
||||||
|
|
||||||
join = []
|
|
||||||
join << "JOIN #{RedmineCrm::Tagging.table_name} ON #{RedmineCrm::Tagging.table_name}.tag_id = #{RedmineCrm::Tag.table_name}.id "
|
|
||||||
join << "JOIN #{Message.table_name} ON #{Message.table_name}.id = #{RedmineCrm::Tagging.table_name}.taggable_id AND #{RedmineCrm::Tagging.table_name}.taggable_type = '#{Message.name}' "
|
|
||||||
join << "JOIN #{Board.table_name} ON #{Board.table_name}.id = #{Message.table_name}.board_id"
|
|
||||||
join << "JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Board.table_name}.project_id"
|
|
||||||
|
|
||||||
group_fields = ""
|
|
||||||
group_fields << ", #{RedmineCrm::Tag.table_name}.created_at" if RedmineCrm::Tag.respond_to?(:created_at)
|
|
||||||
group_fields << ", #{RedmineCrm::Tag.table_name}.updated_at" if RedmineCrm::Tag.respond_to?(:updated_at)
|
|
||||||
|
|
||||||
scope = scope.joins(join.join(' '))
|
|
||||||
scope = scope.select("#{RedmineCrm::Tag.table_name}.*, COUNT(DISTINCT #{RedmineCrm::Tagging.table_name}.taggable_id) AS count")
|
|
||||||
scope = scope.group("#{RedmineCrm::Tag.table_name}.id, #{RedmineCrm::Tag.table_name}.name #{group_fields} HAVING COUNT(*) > 0")
|
|
||||||
scope = scope.order("#{RedmineCrm::Tag.table_name}.name")
|
|
||||||
scope = scope.limit(limit) if limit
|
|
||||||
@available_tags = scope
|
|
||||||
%>
|
|
||||||
|
|
||||||
<h3><%= l(:label_questions_tags) %></h3>
|
|
||||||
<ul class="questions-tags">
|
|
||||||
<% @available_tags.each do |tag| %>
|
|
||||||
<li>
|
|
||||||
<%= link_to tag, {:controller => "questions", :action => "topics", :project_id => @project, :tag => tag.name} %>
|
|
||||||
<span class="count"><%= tag.count %></span>
|
|
||||||
</li>
|
|
||||||
<% end if @available_tags %>
|
|
||||||
</ul>
|
|
|
@ -1,16 +1,16 @@
|
||||||
<%
|
<%
|
||||||
scope = Message.scoped({})
|
# scope = Message.scoped({})
|
||||||
scope = scope.where("#{Message.table_name}.parent_id IS NULL")
|
# scope = scope.where("#{Message.table_name}.parent_id IS NULL")
|
||||||
scope = scope.where(["#{Board.table_name}.project_id = ?", @project.id]) if @project
|
# scope = scope.where(["#{Board.table_name}.project_id = ?", @project.id]) if @project
|
||||||
scope = scope.where(["#{Message.table_name}.board_id = ?", @board.id]) if @board
|
# scope = scope.where(["#{Message.table_name}.board_id = ?", @board.id]) if @board
|
||||||
scope = scope.where(:sticky => true)
|
# scope = scope.where(:featured => true)
|
||||||
@sticky_topics = scope.visible.includes(:board).order("#{Message.table_name}.cached_votes_up DESC").limit(10)
|
# @featured_topics = scope.visible.includes(:board).order("#{Message.table_name}.cached_votes_up DESC").limit(10)
|
||||||
%>
|
%>
|
||||||
<h3><%= l(:label_questions_most_voted) %></h3>
|
<h3><%= l(:label_questions_most_voted) %></h3>
|
||||||
<ul class="related-topics">
|
<ul class="related-topics">
|
||||||
<% @sticky_topics.each do |topic| %>
|
<% @featured_questions.each do |question| %>
|
||||||
<li class="related-topic">
|
<li class="related-topic">
|
||||||
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => topic.board, :id => topic } %>
|
<%= link_to h(question.subject), { :controller => 'questions', :action => 'show', :project_id => question.project, :id => question } %>
|
||||||
</li>
|
</li>
|
||||||
<% end unless @sticky_topics.blank? %>
|
<% end unless @featured_topics.blank? %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<% if @topics && @topics.any? %>
|
<% if @topics && @topics.any? %>
|
||||||
<% unless params[:tag].blank? %>
|
<% unless params[:tag].blank? %>
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<h4><%= l(:label_questions_tagged_by, :count => @topics.size, :tag => params[:tag]) %></h4>
|
<h4><%= l(:label_questions_tagged_by, :count => @topics.size, :tag => params[:tag]) %></h4>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="topics_container">
|
<div id="topics_container">
|
||||||
<% @topics.each do |topic| %>
|
<% @topics.each do |topic| %>
|
||||||
|
@ -26,5 +26,5 @@
|
||||||
<p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
|
<p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<%
|
|
||||||
scope = Message.where({})
|
|
||||||
scope = scope.where("#{Message.table_name}.parent_id IS NULL")
|
|
||||||
scope = scope.where(["#{Board.table_name}.project_id = ?", @project.id]) if @project
|
|
||||||
scope = scope.where(["#{Message.table_name}.board_id = ?", @board.id]) if @board
|
|
||||||
scope = scope.where("#{Message.table_name}.cached_votes_up > 0")
|
|
||||||
@most_voted_topics = scope.visible.includes(:board).order("#{Message.table_name}.cached_votes_up DESC").limit(5)
|
|
||||||
%>
|
|
||||||
<h3><%= l(:label_questions_most_voted) %></h3>
|
|
||||||
<ul class="related-topics">
|
|
||||||
<% @most_voted_topics.each do |topic| %>
|
|
||||||
<li class="related-topic">
|
|
||||||
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => topic.board, :id => topic } %>
|
|
||||||
</li>
|
|
||||||
<% end unless @most_voted_topics.blank? %>
|
|
||||||
</ul>
|
|
|
@ -0,0 +1 @@
|
||||||
|
<%= render :partial => "questions/question_list" %>
|
|
@ -1 +0,0 @@
|
||||||
<%= render :partial => "questions/topic_list" %>
|
|
15
plugins/redmine_questions/app/views/questions/edit.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
<h2><%= avatar(@question_item.author, :size => "24") %><%=h @question_item.subject %></h2>
|
||||||
|
|
||||||
|
<%= form_for @question_item, { :url => question_path(@question_item), :html => {:multipart => true,
|
||||||
|
:id => 'question_form', :method => :put}} do |f| %>
|
||||||
|
<%= back_url_hidden_field_tag %>
|
||||||
|
<div id="all_attributes">
|
||||||
|
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||||
|
</div>
|
||||||
|
<%= submit_tag l(:button_save) %>
|
||||||
|
<%= preview_link({:controller => 'questions', :action => 'preview', :question_id => @question_item}, 'question_form') %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
<div id="preview" class="wiki"></div>
|
||||||
|
|
35
plugins/redmine_questions/app/views/questions/index.html.erb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<% html_title l(:label_questions) %>
|
||||||
|
<div class="questions" >
|
||||||
|
<div class="contextual">
|
||||||
|
<%= link_to(l(:label_questions_new),
|
||||||
|
{:controller => 'questions', :action => 'new', :section_id => @section},
|
||||||
|
:class => 'icon icon-add') if User.current.allowed_to?(:add_questions, @project) %>
|
||||||
|
<%= link_to(l(:label_questions_section_edit),
|
||||||
|
{:controller => 'questions_sections', :action => 'edit', :id => @section},
|
||||||
|
:class => 'icon icon-edit') if @section && User.current.allowed_to?(:manage_sections, @project) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= question_breadcrumb @section %>
|
||||||
|
<h2 class="section-title">
|
||||||
|
<%= @section ? @section.name : l(:label_questions)%>
|
||||||
|
</h2>
|
||||||
|
<% if @section && !@section.description.blank? %>
|
||||||
|
<em class="info"><%= @section.description %></em>
|
||||||
|
<% end %>
|
||||||
|
<div class="filters">
|
||||||
|
<%= form_tag({:controller => "questions", :action => "index"}, :method => :get, :id => "query_form") do %>
|
||||||
|
<%= text_field_tag(:topic_search, params[:topic_search], :autocomplete => "off", :class => "questions-search", :placeholder => l(:label_questions_search) ) %>
|
||||||
|
<%= javascript_tag "observeSearchfield('topic_search', 'topics_list', '#{ escape_javascript(autocomplete_for_subject_questions_path(:project_id => @project, :section_id => @section)) }')" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="topics_list" >
|
||||||
|
<%= render :partial => "questions/question_list" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<%= render :partial => "questions/latest_topics" %>
|
||||||
|
<%= render :partial => "questions/popular_topics" %>
|
||||||
|
<% end %>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
<h2><%= l(:label_message_new) %></h2>
|
<h2><%= l(:label_message_new) %></h2>
|
||||||
|
|
||||||
<%= form_for @message, :url => {:controller => "messages", :action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
|
<%= form_for @question_item, { :url => questions_path, :html => {:multipart => true,
|
||||||
<%= render :partial => 'messages/form', :locals => {:f => f} %>
|
:id => 'question_form'}} do |f| %>
|
||||||
|
<div id="all_attributes">
|
||||||
|
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||||
|
</div>
|
||||||
<%= submit_tag l(:button_create) %>
|
<%= submit_tag l(:button_create) %>
|
||||||
<%# preview_link({:controller => 'messages', :action => 'preview', :board_id => @board}, 'message-form') %>
|
<% preview_link({:controller => 'questions', :action => 'preview', :id => @question_item}, 'question_form') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div id="preview" class="wiki"></div>
|
<div id="preview" class="wiki"></div>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<%= question_breadcrumb @question_item %>
|
||||||
|
|
||||||
|
|
||||||
|
<%= render :partial => 'question' if QA_VERSION_TYPE.match(/Light/) %>
|
||||||
|
|
||||||
|
<% html_title @question_item.subject %>
|
|
@ -1,31 +0,0 @@
|
||||||
|
|
||||||
<% if @board %>
|
|
||||||
<%= board_breadcrumb(@board) %>
|
|
||||||
<div class="board details">
|
|
||||||
<h2><%=h @board.name %></h2>
|
|
||||||
<p class="subtitle"><%=h @board.description %></p>
|
|
||||||
<% else %>
|
|
||||||
<h2><%= l(:label_questions) %></h2>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="filters">
|
|
||||||
<%= form_tag({:controller => "questions", :action => "topics"}, :method => :get, :id => "query_form") do %>
|
|
||||||
<%= hidden_field_tag('project_id', @project.to_param) if @project %>
|
|
||||||
<%= hidden_field_tag('board_id', @board.to_param) if @board %>
|
|
||||||
<%= text_field_tag(:topic_search, params[:topic_search], :autocomplete => "off", :class => "questions-search", :placeholder => l(:label_questions_search) ) %>
|
|
||||||
<%= javascript_tag "observeSearchfield('topic_search', 'topics_list', '#{ escape_javascript(autocomplete_for_topic_questions_path(:project_id => @project, :board_id => @board)) }')" %>
|
|
||||||
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<% if @board %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div id="topics_list" >
|
|
||||||
<%= render :partial => "questions/topic_list" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% content_for :sidebar do %>
|
|
||||||
<%= render :partial => "questions/latest_topics" %>
|
|
||||||
<% end %>
|
|
|
@ -0,0 +1 @@
|
||||||
|
$('#all_attributes').html('<%= escape_javascript(render :partial => 'form') %>');
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="contextual">
|
||||||
|
<%= link_to(l(:button_edit), edit_questions_answer_path(question_item), :class => 'icon icon-edit') if question_item.editable_by?(User.current) %>
|
||||||
|
<%= link_to(l(:button_delete), questions_answer_path(question_item), :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del') if question_item.destroyable_by?(User.current)
|
||||||
|
%>
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="question answer<%= ' votable' if question_item.allow_voting? %> div-table" id="questions_answer_<%= question_item.id %>">
|
||||||
|
<a href="#<%= question_item.id %>" class="wiki-anchor"></a>
|
||||||
|
<% if question_item.allow_voting? && User.current.allowed_to?(:vote_questions, @project) %>
|
||||||
|
<div class="vote" >
|
||||||
|
<%= render :partial => 'questions_votes/question_item_vote', :locals => {:question_item => question_item } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="question-container div-table-cell">
|
||||||
|
<%= render :partial => "questions_answers/actions", :locals => { :question_item => question_item } %>
|
||||||
|
<%= avatar(question_item.author, :size => "32") %>
|
||||||
|
<p class="author">
|
||||||
|
<%= link_to_user question_item.author %><br>
|
||||||
|
<%= l(:label_questions_added_time, :value => time_tag(question_item.created_on)).html_safe %>
|
||||||
|
</p>
|
||||||
|
<div class="wiki">
|
||||||
|
<%= textilizable(question_item, :content) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= link_to_attachments question_item, :author => false %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<%= error_messages_for @answer %>
|
||||||
|
<div class="box">
|
||||||
|
<%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'answer_content', :no_label => true %>
|
||||||
|
<%= wikitoolbar_for 'answer_content' %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= l(:label_attachment_plural) %><br />
|
||||||
|
<%= render :partial => 'attachments/form', :locals => {:container => @answer} %>
|
||||||
|
</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
<h2><%= avatar(@answer.author, :size => "24") %><%=h @answer.question.subject %></h2>
|
||||||
|
|
||||||
|
<%= form_for @answer, :as => :answer, :url => questions_answer_path(@answer), :html => {:multipart => true, :id => 'answer-form', :method => :put} do |f| %>
|
||||||
|
<%= back_url_hidden_field_tag %>
|
||||||
|
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||||
|
<%= submit_tag l(:button_save) %>
|
||||||
|
<%= preview_link(preview_questions_answers_path(@answer), 'answer-form') %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
<div id="preview" class="wiki"></div>
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<div class="comment" id="comment_<%= comment.id %>">
|
||||||
|
<div class="contextual">
|
||||||
|
<%= link_to(
|
||||||
|
"",
|
||||||
|
{:controller => 'questions_comments', :action => 'edit', :source_id => comment.commented, :source_type => comment.commented.class.name.underscore, :id => comment.id},
|
||||||
|
:class => 'icon icon-edit',
|
||||||
|
:method => :get
|
||||||
|
) if (User.current.allowed_to?(:edit_question_comments, comment.commented.project) || (comment.author == User.current && User.current.allowed_to?(:edit_own_question_comments, comment.commented.project)))
|
||||||
|
%>
|
||||||
|
<%= link_to(
|
||||||
|
"",
|
||||||
|
{:controller => 'questions_comments', :action => 'destroy', :source_id => comment.commented, :id => comment, :source_type => comment.commented.class.name.underscore}, :class => 'icon icon-del',
|
||||||
|
:data => {:confirm => l(:text_are_you_sure)},
|
||||||
|
:method => :delete,
|
||||||
|
:title => l(:button_delete)
|
||||||
|
) if (User.current.allowed_to?(:edit_question_comments, comment.commented.project) || (comment.author == User.current && User.current.allowed_to?(:edit_own_question_comments, comment.commented.project)))
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
<div class="author">
|
||||||
|
<%= link_to_user comment.author %>
|
||||||
|
<%= time_tag(comment.created_on) %>
|
||||||
|
</div>
|
||||||
|
<div class="wiki-content">
|
||||||
|
<%= textilizable(comment.comments) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<% if question_item.commentable? %>
|
||||||
|
<%= form_tag({:controller => 'questions_comments', :action => 'create', :id => question_item, :source_id => question_item, :source_type => question_item.class.name.underscore}, :class => "add-comment-form", :remote => true) do %>
|
||||||
|
<%= text_area 'comment', @comment.respond_to?(:content) ? 'content' : 'comments', :cols => 80, :rows => 5, :id => "comments_for_#{question_item.class.name.underscore}_#{question_item.id}" %>
|
||||||
|
<%= submit_tag l(:button_add), :class => "button-small" %>
|
||||||
|
<%= link_to l(:button_cancel), {}, :onclick => "$('#add_#{question_item.class.name.underscore}_comments_#{question_item.id}').hide(); return false;" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<% if question_item.comments.any? %>
|
||||||
|
<div class="question-comments">
|
||||||
|
<% question_item.comments.each do |comment| %>
|
||||||
|
<% next if comment.new_record? %>
|
||||||
|
<%= render :partial => "questions_comments/comment", :locals => {:comment => comment, :comment_source => question_item} %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="add_comments" id="add_<%= question_item.class.name.underscore %>_comments_<%= question_item.id %>" style="display:none;">
|
||||||
|
<%= render :partial => "questions_comments/comment_form", :locals => {:question_item => question_item} %>
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<% if question_item.commentable? %>
|
||||||
|
<%= link_to l(:label_questions_comment), "#", :onclick => "$('#add_comments_#{question_item.id}').toggle(); showAndScrollTo('add_#{question_item.class.name.underscore}_comments_#{question_item.id}', 'comments_for_#{question_item.class.name.underscore}_#{question_item.id}'); return false;", :class => 'icon icon-comment add-comment-link' %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="comments_container">
|
||||||
|
<%= render :partial => 'questions_comments/comment_list', :locals => {:question_item => question_item} %>
|
||||||
|
</div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
$("#<%= @comment_source.class.name.underscore %>_<%= @comment_source.id %> .comments_container").html('<%= escape_javascript(render :partial => "questions_comments/comment_list", :locals => {:question_item => @comment_source}) %>');
|
||||||
|
$(".comment#comment_<%= @comment.id %>").effect('highlight', {}, 1000);
|
||||||
|
$("textarea#comments_for_<%= @comment_source.class.name.underscore %>_<%= @comment_source.id %>").val("");
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
<h2><%= avatar(@comment.author, :size => "24") %></h2>
|
||||||
|
<%= form_tag({:controller => 'questions_comments', :action => 'update', :source_id => @comment_source, :source_type => @comment_source.class.name.underscore, :id => @comment, :method => :put}) do %>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<%= text_area 'comment', 'comments', :cols => 80, :rows => 5%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><%= submit_tag l(:button_update) %></p>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
<div id="preview" class="wiki"></div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<%= error_messages_for @section %>
|
||||||
|
<p>
|
||||||
|
<%= f.text_field :name, :required => true %>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<%= f.text_area :description, :rows => 5 %>
|
||||||
|
</p>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<h3 class="title"><%=l(:label_questions_section_new) %></h3>
|
||||||
|
|
||||||
|
<%= labelled_form_for((@project ? [@project, @section] : @section), :remote => true, :html => {:class => 'tabular'}) do |f|%>
|
||||||
|
<%= render :partial => 'form', :locals => { :f => f} %>
|
||||||
|
<p>
|
||||||
|
<%= f.submit l(:button_create) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<% previous_group = false %>
|
||||||
|
<div class="section-list">
|
||||||
|
<% @sections.each do |section| %>
|
||||||
|
<% if @project.blank? && (group = section.project) != previous_group %>
|
||||||
|
<% reset_cycle %>
|
||||||
|
</div>
|
||||||
|
<% if group %>
|
||||||
|
<div class="project-forums">
|
||||||
|
<h3>
|
||||||
|
<%= group.name %>
|
||||||
|
<%= link_to " \xc2\xbb", project_questions_sections_path(:project_id => group.identifier) %>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="section-list">
|
||||||
|
<% previous_group = group %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<a href="<%= url_for({:controller => "questions", :action => 'index', :section_id => section, :project_id => section.project}) %>" id="section_<%= section.id %>" class="section-tile">
|
||||||
|
<h4>
|
||||||
|
<%= section.name %>
|
||||||
|
<span class="topic-count"><%= "(#{section.questions_count})" %></span>
|
||||||
|
</h4>
|
||||||
|
<div class="description">
|
||||||
|
<%= section.description %>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
|
@ -0,0 +1,8 @@
|
||||||
|
hideModal();
|
||||||
|
<% select = content_tag('select', content_tag('option') + options_from_collection_for_select(QuestionsSection.where(:project_id => @project), :id, :name, @section.id.to_s), :id => 'question_section_id', :name => 'question[section_id]') %>
|
||||||
|
$('#question_section_id').replaceWith('<%= escape_javascript(select) %>');
|
||||||
|
$.ajax({
|
||||||
|
url: '<%= escape_javascript update_form_questions_path(:id => @question_item, :format => 'js', :project_id => @project) %>',
|
||||||
|
type: 'put',
|
||||||
|
data: $('#question_form').serialize()
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
<h2><%=l(:label_questions_section)%></h2>
|
||||||
|
|
||||||
|
<%= labelled_form_for (@project ? [@project, @section] : @section), :method => "PUT", :html => {:class => 'tabular'} do |f|%>
|
||||||
|
<div class="box tabular">
|
||||||
|
<%= back_url_hidden_field_tag %>
|
||||||
|
<%= render :partial => 'form', :locals => { :f => f} %>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<%= f.submit l(:button_save) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<% html_title l(:label_questions) %>
|
||||||
|
<div class="contextual">
|
||||||
|
<%= link_to(l(:label_questions_new),
|
||||||
|
{:controller => 'questions', :action => 'new', :section_id => @section},
|
||||||
|
:class => 'icon icon-add') if User.current.allowed_to?(:add_questions, @project) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h2 class=""><%= l(:label_questions) %></h2>
|
||||||
|
<div class="filters">
|
||||||
|
<%= form_tag({:controller => "questions", :action => "index"}, :method => :get, :id => "query_form") do %>
|
||||||
|
<%= text_field_tag(:topic_search, params[:topic_search], :autocomplete => "off", :class => "questions-search", :placeholder => l(:label_questions_search) ) %>
|
||||||
|
<%= javascript_tag "observeSearchfield('topic_search', 'forum_list', '#{ escape_javascript(autocomplete_for_subject_questions_path(:project_id => @project, :section_id => @section)) }')" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="forum_list">
|
||||||
|
<%= render :partial => 'tiles' %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<%= render :partial => "questions/latest_topics" %>
|
||||||
|
<%= render :partial => "questions/popular_topics" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% content_for :header_tags do %>
|
||||||
|
<%= javascript_include_tag :questions, :plugin => 'redmine_questions' %>
|
||||||
|
<% end %>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<h2><%=l(:label_questions_section_new)%></h2>
|
||||||
|
|
||||||
|
<%= labelled_form_for((@project ? [@project, @section] : @section), :html => {:class => 'tabular'}) do |f|%>
|
||||||
|
<div class="box tabular">
|
||||||
|
<%= back_url_hidden_field_tag %>
|
||||||
|
<%= render :partial => 'form', :locals => { :f => f} %>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<%= f.submit l(:button_create) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,2 @@
|
||||||
|
$('#ajax-modal').html('<%= escape_javascript(render :partial => 'new_modal') %>');
|
||||||
|
showModal('ajax-modal', '600px');
|
|
@ -0,0 +1,14 @@
|
||||||
|
<%= error_messages_for 'questions_status' %>
|
||||||
|
|
||||||
|
<div class="box tabular">
|
||||||
|
<p><%= f.text_field :name, :required => true %></p>
|
||||||
|
<p><%= f.select :color, options_for_select(QuestionsSettings::IDEA_COLORS, @questions_status.color), {:label => l(:field_color)} %></p>
|
||||||
|
<p><%= f.check_box :is_closed, :label => :label_questions_status_closed %></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= javascript_tag "$('#questions_status_color').simplecolorpicker({picker: true});"%>
|
||||||
|
<% content_for :header_tags do %>
|
||||||
|
<%= javascript_include_tag 'jquery.simplecolorpicker.js', :plugin => "redmine_questions" %>
|
||||||
|
<%= stylesheet_link_tag 'jquery.simplecolorpicker.css', :plugin => 'redmine_questions' %>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<h2><%= link_to l(:label_questions_status_plural), :action =>"plugin", :id => "redmine_questions", :controller => "settings", :tab => 'questions_statuses' %> » <%=h @questions_status %></h2>
|
||||||
|
|
||||||
|
<%= labelled_form_for @questions_status do |f| %>
|
||||||
|
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||||
|
<%= submit_tag l(:button_save) %>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,10 @@
|
||||||
|
api.array :questions_statuses do
|
||||||
|
@questions_statuses.each do |status|
|
||||||
|
api.questions_status do
|
||||||
|
api.id status.id
|
||||||
|
api.name status.name
|
||||||
|
api.color status.color
|
||||||
|
api.is_closed status.is_closed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
<h2><%= link_to l(:label_questions_status_plural), :action =>"plugin", :id => "redmine_questions", :controller => "settings", :tab => 'questions_statuses' %> » <%=l(:label_questions_status_new)%></h2>
|
||||||
|
|
||||||
|
<%= labelled_form_for @questions_status do |f| %>
|
||||||
|
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||||
|
<%= submit_tag l(:button_create) %>
|
||||||
|
<% end %>
|
|
@ -1,3 +0,0 @@
|
||||||
<p><label for="settings_sidebar_message"><%= l(:label_questions_sidebar_message) %></label>
|
|
||||||
<%= text_area_tag 'settings[sidebar_message]', @settings[:sidebar_message], :class => 'wiki-edit', :rows => 5 %>
|
|
||||||
</p>
|
|
8
plugins/redmine_questions/assets/images/answered.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="35" height="35" viewBox="0, 0, 35, 35">
|
||||||
|
<g id="answered">
|
||||||
|
<path d="M17.5,0 C27.165,0 35,7.835 35,17.5 C35,27.165 27.165,35 17.5,35 C7.835,35 0,27.165 0,17.5 C0,7.835 7.835,0 17.5,0 z M17.5,2 C8.94,2 2,8.94 2,17.5 C2,26.06 8.94,33 17.5,33 C26.06,33 33,26.06 33,17.5 C33,8.94 26.06,2 17.5,2 z" fill="#5DBA7D"/>
|
||||||
|
<path d="M10.135,15.26 L6.628,19.186 L14.979,26.648 L29.232,13.863 L25.775,9.894 L14.979,19.651 z" fill="#5EBA7D"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 703 B |
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="35" height="35" viewBox="0, 0, 35, 35">
|
||||||
|
<g id="answered">
|
||||||
|
<path d="M-0.077,10.673 L-0.077,23.495 L13.026,35 L35.077,15.186 L35.077,3.042 L13.026,22.856 z" fill="#5EBA7D"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 446 B |
BIN
plugins/redmine_questions/assets/images/book_open.png
Normal file
After Width: | Height: | Size: 622 B |
15
plugins/redmine_questions/assets/images/downvote.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="35" height="35" viewBox="0, 0, 35, 35">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="Clip_1">
|
||||||
|
<path d="M35,-1 L-1,-1 L-1,35 L35,35 z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_1">
|
||||||
|
<g clip-path="url(#Clip_1)" id="Layer_1">
|
||||||
|
<path d="M5.126,29.874 C-1.709,23.04 -1.709,11.96 5.126,5.126 C11.96,-1.709 23.04,-1.709 29.874,5.126 C36.709,11.96 36.709,23.04 29.874,29.874 C23.04,36.709 11.96,36.709 5.126,29.874" fill="#F2F2F2"/>
|
||||||
|
<path d="M10.033,13 L17.5,24.4 L24.967,13 z M10.033,13.2" fill="#AAAAAA"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 780 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 35 18" width="35pt" height="18pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.5.3 <dc:date>2016-07-07 08:57:24 +0000</dc:date></metadata><defs/><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><path d="M .21475331 .19262309 L 35 .19262309 L 17.607377 17.585246 Z" fill="#9a9a9a"/></g></g></svg>
|
After Width: | Height: | Size: 681 B |
BIN
plugins/redmine_questions/assets/images/upvote.graffle
Normal file
15
plugins/redmine_questions/assets/images/upvote.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="35" height="35" viewBox="0, 0, 35, 35">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="Clip_1">
|
||||||
|
<path d="M0,36 L36,36 L36,0 L0,0 z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_1">
|
||||||
|
<g clip-path="url(#Clip_1)" id="Layer_1">
|
||||||
|
<path d="M29.874,5.126 C36.709,11.96 36.709,23.04 29.874,29.874 C23.04,36.709 11.96,36.709 5.126,29.874 C-1.709,23.04 -1.709,11.96 5.126,5.126 C11.96,-1.709 23.04,-1.709 29.874,5.126" fill="#F2F2F2"/>
|
||||||
|
<path d="M24.967,22 L17.5,10.6 L10.033,22 z M24.967,21.8" fill="#AAAAAA"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 776 B |
3
plugins/redmine_questions/assets/images/upvote_arrow.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 35 18" width="35pt" height="18pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.5.3 <dc:date>2016-07-07 08:55:07 +0000</dc:date></metadata><defs/><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><path d="M 34.785247 17.992623 L 71054274e-22 17.992623 L 17.392623 .6 Z" fill="#9a9a9a"/></g></g></svg>
|
After Width: | Height: | Size: 684 B |
26
plugins/redmine_questions/assets/images/voting.svg
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="140" height="105" viewBox="0, 0, 140, 105">
|
||||||
|
<g id="answered">
|
||||||
|
<path d="M17.5,0 C27.165,0 35,7.835 35,17.5 C35,27.165 27.165,35 17.5,35 C7.835,35 0,27.165 0,17.5 C0,7.835 7.835,0 17.5,0 z M17.5,2 C8.94,2 2,8.94 2,17.5 C2,26.06 8.94,33 17.5,33 C26.06,33 33,26.06 33,17.5 C33,8.94 26.06,2 17.5,2 z" fill="#5DBA7D"/>
|
||||||
|
<path d="M10.135,15.26 L6.628,19.186 L14.979,26.648 L29.232,13.863 L25.775,9.894 L14.979,19.651 z" fill="#5EBA7D"/>
|
||||||
|
<path d="M7.947,84.081 L3.454,89.11 L14.15,98.667 L32.405,82.292 L27.978,77.208 L14.15,89.705 z" fill="#5EBA7D"/>
|
||||||
|
<path d="M52.5,79.594 C57.108,79.594 60.844,83.329 60.844,87.937 C60.844,92.546 57.108,96.281 52.5,96.281 C47.892,96.281 44.156,92.546 44.156,87.937 C44.156,83.329 47.892,79.594 52.5,79.594 z M52.5,81.094 C48.72,81.094 45.656,84.158 45.656,87.937 C45.656,91.717 48.72,94.781 52.5,94.781 C56.28,94.781 59.344,91.717 59.344,87.937 C59.344,84.158 56.28,81.094 52.5,81.094 z" fill="#5DBA7D"/>
|
||||||
|
<path d="M48.989,86.869 L47.316,88.741 L51.298,92.299 L58.094,86.204 L56.446,84.311 L51.298,88.963 z" fill="#5EBA7D"/>
|
||||||
|
<g>
|
||||||
|
<path d="M99.874,5.126 C106.709,11.96 106.709,23.04 99.874,29.874 C93.04,36.709 81.96,36.709 75.126,29.874 C68.291,23.04 68.291,11.96 75.126,5.126 C81.96,-1.709 93.04,-1.709 99.874,5.126" fill="#EFEFEF"/>
|
||||||
|
<path d="M94.967,21.625 L87.5,10.225 L80.033,21.625 z M94.967,21.425" fill="#9A9A9A"/>
|
||||||
|
</g>
|
||||||
|
<path d="M99.874,40.126 C106.709,46.96 106.709,58.04 99.874,64.874 C93.04,71.709 81.96,71.709 75.126,64.874 C68.291,58.04 68.291,46.96 75.126,40.126 C81.96,33.291 93.04,33.291 99.874,40.126" fill="#D6F5E6"/>
|
||||||
|
<path d="M94.967,56.625 L87.5,45.225 L80.033,56.625 z M94.967,56.425" fill="#9A9A9A"/>
|
||||||
|
<g>
|
||||||
|
<path d="M40.126,29.874 C33.291,23.04 33.291,11.96 40.126,5.126 C46.96,-1.709 58.04,-1.709 64.874,5.126 C71.709,11.96 71.709,23.04 64.874,29.874 C58.04,36.709 46.96,36.709 40.126,29.874" fill="#EFEFEF"/>
|
||||||
|
<path d="M45.033,13.375 L52.5,24.775 L59.967,13.375 z M45.033,13.575" fill="#9A9A9A"/>
|
||||||
|
</g>
|
||||||
|
<path d="M40.126,64.874 C33.291,58.04 33.291,46.96 40.126,40.126 C46.96,33.291 58.04,33.291 64.874,40.126 C71.709,46.96 71.709,58.04 64.874,64.874 C58.04,71.709 46.96,71.709 40.126,64.874" fill="#F9DEDE"/>
|
||||||
|
<path d="M45.033,48.375 L52.5,59.775 L59.967,48.375 z M45.033,48.575" fill="#9A9A9A"/>
|
||||||
|
<path d="M4.75,45.839 L4.75,56.07 L14.255,65.25 L30.25,49.44 L30.25,39.75 L14.255,55.56 z" fill="#5EBA7D"/>
|
||||||
|
<path d="M108,60 L137,60 L122.5,45 z" fill="#AAAAAA"/>
|
||||||
|
<path d="M137,10.771 L108,10.771 L122.5,25.771 z" fill="#AAAAAA"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,235 @@
|
||||||
|
/*
|
||||||
|
* Very simple jQuery Color Picker
|
||||||
|
* https://github.com/tkrotoff/jquery-simplecolorpicker
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2013 Tanguy Krotoff <tkrotoff@gmail.com>
|
||||||
|
*
|
||||||
|
* Licensed under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
var SimpleColorPicker = function(select, options) {
|
||||||
|
this.init('simplecolorpicker', select, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SimpleColorPicker class.
|
||||||
|
*/
|
||||||
|
SimpleColorPicker.prototype = {
|
||||||
|
constructor: SimpleColorPicker,
|
||||||
|
|
||||||
|
init: function(type, select, options) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.type = type;
|
||||||
|
|
||||||
|
self.$select = $(select);
|
||||||
|
self.$select.hide();
|
||||||
|
|
||||||
|
self.options = $.extend({}, $.fn.simplecolorpicker.defaults, options);
|
||||||
|
|
||||||
|
self.$colorList = null;
|
||||||
|
|
||||||
|
if (self.options.picker === true) {
|
||||||
|
var selectText = self.$select.find('> option:selected').text();
|
||||||
|
self.$icon = $('<span class="simplecolorpicker button"'
|
||||||
|
+ ' title="' + selectText + '"'
|
||||||
|
+ ' style="background-color: ' + self.$select.val() + ';"'
|
||||||
|
+ ' role="button" tabindex="0">'
|
||||||
|
+ '</span>').insertAfter(self.$select);
|
||||||
|
self.$icon.on('click.' + self.type, $.proxy(self.showPicker, self));
|
||||||
|
|
||||||
|
self.$picker = $('<span class="simplecolorpicker picker ' + self.options.theme + '"></span>').appendTo(document.body);
|
||||||
|
self.$colorList = self.$picker;
|
||||||
|
|
||||||
|
// Hide picker when clicking outside
|
||||||
|
$(document).on('mousedown.' + self.type, $.proxy(self.hidePicker, self));
|
||||||
|
self.$picker.on('mousedown.' + self.type, $.proxy(self.mousedown, self));
|
||||||
|
} else {
|
||||||
|
self.$inline = $('<span class="simplecolorpicker inline ' + self.options.theme + '"></span>').insertAfter(self.$select);
|
||||||
|
self.$colorList = self.$inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the list of colors
|
||||||
|
// <span class="color selected" title="Green" style="background-color: #7bd148;" role="button"></span>
|
||||||
|
self.$select.find('> option').each(function() {
|
||||||
|
var $option = $(this);
|
||||||
|
var color = $option.val();
|
||||||
|
|
||||||
|
var isSelected = $option.is(':selected');
|
||||||
|
var isDisabled = $option.is(':disabled');
|
||||||
|
|
||||||
|
var selected = '';
|
||||||
|
if (isSelected === true) {
|
||||||
|
selected = ' data-selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
var disabled = '';
|
||||||
|
if (isDisabled === true) {
|
||||||
|
disabled = ' data-disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = '';
|
||||||
|
if (isDisabled === false) {
|
||||||
|
title = ' title="' + $option.text() + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
var role = '';
|
||||||
|
if (isDisabled === false) {
|
||||||
|
role = ' role="button" tabindex="0"';
|
||||||
|
}
|
||||||
|
|
||||||
|
var $colorSpan = $('<span class="color"'
|
||||||
|
+ title
|
||||||
|
+ ' style="background-color: ' + color + ';"'
|
||||||
|
+ ' data-color="' + color + '"'
|
||||||
|
+ selected
|
||||||
|
+ disabled
|
||||||
|
+ role + '>'
|
||||||
|
+ '</span>');
|
||||||
|
|
||||||
|
self.$colorList.append($colorSpan);
|
||||||
|
$colorSpan.on('click.' + self.type, $.proxy(self.colorSpanClicked, self));
|
||||||
|
|
||||||
|
var $next = $option.next();
|
||||||
|
if ($next.is('optgroup') === true) {
|
||||||
|
// Vertical break, like hr
|
||||||
|
self.$colorList.append('<span class="vr"></span>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the selected color.
|
||||||
|
*
|
||||||
|
* @param color the hexadecimal color to select, ex: '#fbd75b'
|
||||||
|
*/
|
||||||
|
selectColor: function(color) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var $colorSpan = self.$colorList.find('> span.color').filter(function() {
|
||||||
|
return $(this).data('color').toLowerCase() === color.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($colorSpan.length > 0) {
|
||||||
|
self.selectColorSpan($colorSpan);
|
||||||
|
} else {
|
||||||
|
console.error("The given color '" + color + "' could not be found");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showPicker: function() {
|
||||||
|
var pos = this.$icon.offset();
|
||||||
|
this.$picker.css({
|
||||||
|
// Remove some pixels to align the picker icon with the icons inside the dropdown
|
||||||
|
left: pos.left - 1,
|
||||||
|
top: pos.top - 4//+ this.$icon.outerHeight()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$picker.show(this.options.pickerDelay);
|
||||||
|
},
|
||||||
|
|
||||||
|
hidePicker: function() {
|
||||||
|
this.$picker.hide(this.options.pickerDelay);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the given span inside $colorList.
|
||||||
|
*
|
||||||
|
* The given span becomes the selected one.
|
||||||
|
* It also changes the HTML select value, this will emit the 'change' event.
|
||||||
|
*/
|
||||||
|
selectColorSpan: function($colorSpan) {
|
||||||
|
var color = $colorSpan.data('color');
|
||||||
|
var title = $colorSpan.prop('title');
|
||||||
|
|
||||||
|
// Mark this span as the selected one
|
||||||
|
$colorSpan.siblings().removeAttr('data-selected');
|
||||||
|
$colorSpan.attr('data-selected', '');
|
||||||
|
|
||||||
|
if (this.options.picker === true) {
|
||||||
|
this.$icon.css('background-color', color);
|
||||||
|
this.$icon.prop('title', title);
|
||||||
|
this.hidePicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change HTML select value
|
||||||
|
this.$select.val(color);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user clicked on a color inside $colorList.
|
||||||
|
*/
|
||||||
|
colorSpanClicked: function(e) {
|
||||||
|
// When a color is clicked, make it the new selected one (unless disabled)
|
||||||
|
if ($(e.target).is('[data-disabled]') === false) {
|
||||||
|
this.selectColorSpan($(e.target));
|
||||||
|
this.$select.trigger('change');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the mousedown event from "eating" the click event.
|
||||||
|
*/
|
||||||
|
mousedown: function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function() {
|
||||||
|
if (this.options.picker === true) {
|
||||||
|
this.$icon.off('.' + this.type);
|
||||||
|
this.$icon.remove();
|
||||||
|
$(document).off('.' + this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$colorList.off('.' + this.type);
|
||||||
|
this.$colorList.remove();
|
||||||
|
|
||||||
|
this.$select.removeData(this.type);
|
||||||
|
this.$select.show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin definition.
|
||||||
|
* How to use: $('#id').simplecolorpicker()
|
||||||
|
*/
|
||||||
|
$.fn.simplecolorpicker = function(option) {
|
||||||
|
var args = $.makeArray(arguments);
|
||||||
|
args.shift();
|
||||||
|
|
||||||
|
// For HTML element passed to the plugin
|
||||||
|
return this.each(function() {
|
||||||
|
var $this = $(this),
|
||||||
|
data = $this.data('simplecolorpicker'),
|
||||||
|
options = typeof option === 'object' && option;
|
||||||
|
if (data === undefined) {
|
||||||
|
$this.data('simplecolorpicker', (data = new SimpleColorPicker(this, options)));
|
||||||
|
}
|
||||||
|
if (typeof option === 'string') {
|
||||||
|
data[option].apply(data, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default options.
|
||||||
|
*/
|
||||||
|
$.fn.simplecolorpicker.defaults = {
|
||||||
|
// No theme by default
|
||||||
|
theme: '',
|
||||||
|
|
||||||
|
// Show the picker or make it inline
|
||||||
|
picker: false,
|
||||||
|
|
||||||
|
// Animation delay in milliseconds
|
||||||
|
pickerDelay: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
|
@ -1,392 +0,0 @@
|
||||||
/*
|
|
||||||
* jQuery UI Tag-it!
|
|
||||||
*
|
|
||||||
* @version v2.0 (06/2011)
|
|
||||||
*
|
|
||||||
* Copyright 2011, Levy Carneiro Jr.
|
|
||||||
* Released under the MIT license.
|
|
||||||
* http://aehlke.github.com/tag-it/LICENSE
|
|
||||||
*
|
|
||||||
* Homepage:
|
|
||||||
* http://aehlke.github.com/tag-it/
|
|
||||||
*
|
|
||||||
* Authors:
|
|
||||||
* Levy Carneiro Jr.
|
|
||||||
* Martin Rehfeld
|
|
||||||
* Tobias Schmidt
|
|
||||||
* Skylar Challand
|
|
||||||
* Alex Ehlke
|
|
||||||
*
|
|
||||||
* Maintainer:
|
|
||||||
* Alex Ehlke - Twitter: @aehlke
|
|
||||||
*
|
|
||||||
* Dependencies:
|
|
||||||
* jQuery v1.4+
|
|
||||||
* jQuery UI v1.8+
|
|
||||||
*/
|
|
||||||
(function($) {
|
|
||||||
|
|
||||||
$.widget('ui.tagit', {
|
|
||||||
options: {
|
|
||||||
itemName : 'item',
|
|
||||||
fieldName : 'tags',
|
|
||||||
availableTags : [],
|
|
||||||
tagSource : null,
|
|
||||||
removeConfirmation: false,
|
|
||||||
caseSensitive : true,
|
|
||||||
placeholderText : null,
|
|
||||||
|
|
||||||
// When enabled, quotes are not neccesary
|
|
||||||
// for inputting multi-word tags.
|
|
||||||
allowSpaces: false,
|
|
||||||
|
|
||||||
// Whether to animate tag removals or not.
|
|
||||||
animate: true,
|
|
||||||
|
|
||||||
// The below options are for using a single field instead of several
|
|
||||||
// for our form values.
|
|
||||||
//
|
|
||||||
// When enabled, will use a single hidden field for the form,
|
|
||||||
// rather than one per tag. It will delimit tags in the field
|
|
||||||
// with singleFieldDelimiter.
|
|
||||||
//
|
|
||||||
// The easiest way to use singleField is to just instantiate tag-it
|
|
||||||
// on an INPUT element, in which case singleField is automatically
|
|
||||||
// set to true, and singleFieldNode is set to that element. This
|
|
||||||
// way, you don't need to fiddle with these options.
|
|
||||||
singleField: false,
|
|
||||||
|
|
||||||
singleFieldDelimiter: ',',
|
|
||||||
|
|
||||||
// Set this to an input DOM node to use an existing form field.
|
|
||||||
// Any text in it will be erased on init. But it will be
|
|
||||||
// populated with the text of tags as they are created,
|
|
||||||
// delimited by singleFieldDelimiter.
|
|
||||||
//
|
|
||||||
// If this is not set, we create an input node for it,
|
|
||||||
// with the name given in settings.fieldName,
|
|
||||||
// ignoring settings.itemName.
|
|
||||||
singleFieldNode: null,
|
|
||||||
|
|
||||||
// Optionally set a tabindex attribute on the input that gets
|
|
||||||
// created for tag-it.
|
|
||||||
tabIndex: null,
|
|
||||||
|
|
||||||
|
|
||||||
// Event callbacks.
|
|
||||||
onTagAdded : null,
|
|
||||||
onTagRemoved: null,
|
|
||||||
onTagClicked: null
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
_create: function() {
|
|
||||||
// for handling static scoping inside callbacks
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
// There are 2 kinds of DOM nodes this widget can be instantiated on:
|
|
||||||
// 1. UL, OL, or some element containing either of these.
|
|
||||||
// 2. INPUT, in which case 'singleField' is overridden to true,
|
|
||||||
// a UL is created and the INPUT is hidden.
|
|
||||||
if (this.element.is('input')) {
|
|
||||||
this.tagList = $('<ul></ul>').insertAfter(this.element);
|
|
||||||
this.options.singleField = true;
|
|
||||||
this.options.singleFieldNode = this.element;
|
|
||||||
this.element.css('display', 'none');
|
|
||||||
} else {
|
|
||||||
this.tagList = this.element.find('ul, ol').andSelf().last();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._tagInput = $('<input type="text" />').addClass('ui-widget-content');
|
|
||||||
if (this.options.tabIndex) {
|
|
||||||
this._tagInput.attr('tabindex', this.options.tabIndex);
|
|
||||||
}
|
|
||||||
if (this.options.placeholderText) {
|
|
||||||
this._tagInput.attr('placeholder', this.options.placeholderText);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options.tagSource = this.options.tagSource || function(search, showChoices) {
|
|
||||||
var filter = search.term.toLowerCase();
|
|
||||||
var choices = $.grep(this.options.availableTags, function(element) {
|
|
||||||
// Only match autocomplete options that begin with the search term.
|
|
||||||
// (Case insensitive.)
|
|
||||||
return (element.toLowerCase().indexOf(filter) === 0);
|
|
||||||
});
|
|
||||||
showChoices(this._subtractArray(choices, this.assignedTags()));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind tagSource callback functions to this context.
|
|
||||||
if ($.isFunction(this.options.tagSource)) {
|
|
||||||
this.options.tagSource = $.proxy(this.options.tagSource, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tagList
|
|
||||||
.addClass('tagit')
|
|
||||||
.addClass('ui-widget ui-widget-content ui-corner-all')
|
|
||||||
// Create the input field.
|
|
||||||
.append($('<li class="tagit-new"></li>').append(this._tagInput))
|
|
||||||
.click(function(e) {
|
|
||||||
var target = $(e.target);
|
|
||||||
if (target.hasClass('tagit-label')) {
|
|
||||||
that._trigger('onTagClicked', e, target.closest('.tagit-choice'));
|
|
||||||
} else {
|
|
||||||
// Sets the focus() to the input field, if the user
|
|
||||||
// clicks anywhere inside the UL. This is needed
|
|
||||||
// because the input field needs to be of a small size.
|
|
||||||
that._tagInput.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add existing tags from the list, if any.
|
|
||||||
this.tagList.children('li').each(function() {
|
|
||||||
if (!$(this).hasClass('tagit-new')) {
|
|
||||||
that.createTag($(this).html(), $(this).attr('class'));
|
|
||||||
$(this).remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Single field support.
|
|
||||||
if (this.options.singleField) {
|
|
||||||
if (this.options.singleFieldNode) {
|
|
||||||
// Add existing tags from the input field.
|
|
||||||
var node = $(this.options.singleFieldNode);
|
|
||||||
var tags = node.val().split(this.options.singleFieldDelimiter);
|
|
||||||
node.val('');
|
|
||||||
$.each(tags, function(index, tag) {
|
|
||||||
that.createTag(tag);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Create our single field input after our list.
|
|
||||||
this.options.singleFieldNode = this.tagList.after('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events.
|
|
||||||
this._tagInput
|
|
||||||
.keydown(function(event) {
|
|
||||||
// Backspace is not detected within a keypress, so it must use keydown.
|
|
||||||
if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') {
|
|
||||||
var tag = that._lastTag();
|
|
||||||
if (!that.options.removeConfirmation || tag.hasClass('remove')) {
|
|
||||||
// When backspace is pressed, the last tag is deleted.
|
|
||||||
that.removeTag(tag);
|
|
||||||
} else if (that.options.removeConfirmation) {
|
|
||||||
tag.addClass('remove ui-state-highlight');
|
|
||||||
}
|
|
||||||
} else if (that.options.removeConfirmation) {
|
|
||||||
that._lastTag().removeClass('remove ui-state-highlight');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comma/Space/Enter are all valid delimiters for new tags,
|
|
||||||
// except when there is an open quote or if setting allowSpaces = true.
|
|
||||||
// Tab will also create a tag, unless the tag input is empty, in which case it isn't caught.
|
|
||||||
if (
|
|
||||||
// event.which == $.ui.keyCode.COMMA ||
|
|
||||||
event.which == $.ui.keyCode.ENTER ||
|
|
||||||
(
|
|
||||||
event.which == $.ui.keyCode.TAB &&
|
|
||||||
that._tagInput.val() !== ''
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
event.which == $.ui.keyCode.SPACE &&
|
|
||||||
that.options.allowSpaces !== true &&
|
|
||||||
(
|
|
||||||
$.trim(that._tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
|
|
||||||
(
|
|
||||||
$.trim(that._tagInput.val()).charAt(0) == '"' &&
|
|
||||||
$.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' &&
|
|
||||||
$.trim(that._tagInput.val()).length - 1 !== 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
that.createTag(that._cleanedInput());
|
|
||||||
|
|
||||||
// The autocomplete doesn't close automatically when TAB is pressed.
|
|
||||||
// So let's ensure that it closes.
|
|
||||||
that._tagInput.autocomplete('close');
|
|
||||||
}
|
|
||||||
}).blur(function(e){
|
|
||||||
// Create a tag when the element loses focus (unless it's empty).
|
|
||||||
that.createTag(that._cleanedInput());
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Autocomplete.
|
|
||||||
if (this.options.availableTags || this.options.tagSource) {
|
|
||||||
this._tagInput.autocomplete({
|
|
||||||
source: this.options.tagSource,
|
|
||||||
select: function(event, ui) {
|
|
||||||
// Delete the last tag if we autocomplete something despite the input being empty
|
|
||||||
// This happens because the input's blur event causes the tag to be created when
|
|
||||||
// the user clicks an autocomplete item.
|
|
||||||
// The only artifact of this is that while the user holds down the mouse button
|
|
||||||
// on the selected autocomplete item, a tag is shown with the pre-autocompleted text,
|
|
||||||
// and is changed to the autocompleted text upon mouseup.
|
|
||||||
if (that._tagInput.val() === '') {
|
|
||||||
that.removeTag(that._lastTag(), false);
|
|
||||||
}
|
|
||||||
that.createTag(ui.item.value);
|
|
||||||
// Preventing the tag input to be updated with the chosen value.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_cleanedInput: function() {
|
|
||||||
// Returns the contents of the tag input, cleaned and ready to be passed to createTag
|
|
||||||
return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1'));
|
|
||||||
},
|
|
||||||
|
|
||||||
_lastTag: function() {
|
|
||||||
return this.tagList.children('.tagit-choice:last');
|
|
||||||
},
|
|
||||||
|
|
||||||
assignedTags: function() {
|
|
||||||
// Returns an array of tag string values
|
|
||||||
var that = this;
|
|
||||||
var tags = [];
|
|
||||||
if (this.options.singleField) {
|
|
||||||
tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
|
|
||||||
if (tags[0] === '') {
|
|
||||||
tags = [];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.tagList.children('.tagit-choice').each(function() {
|
|
||||||
tags.push(that.tagLabel(this));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return tags;
|
|
||||||
},
|
|
||||||
|
|
||||||
_updateSingleTagsField: function(tags) {
|
|
||||||
// Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
|
|
||||||
$(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter));
|
|
||||||
},
|
|
||||||
|
|
||||||
_subtractArray: function(a1, a2) {
|
|
||||||
var result = [];
|
|
||||||
for (var i = 0; i < a1.length; i++) {
|
|
||||||
if ($.inArray(a1[i], a2) == -1) {
|
|
||||||
result.push(a1[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
tagLabel: function(tag) {
|
|
||||||
// Returns the tag's string label.
|
|
||||||
if (this.options.singleField) {
|
|
||||||
return $(tag).children('.tagit-label').text();
|
|
||||||
} else {
|
|
||||||
return $(tag).children('input').val();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_isNew: function(value) {
|
|
||||||
var that = this;
|
|
||||||
var isNew = true;
|
|
||||||
this.tagList.children('.tagit-choice').each(function(i) {
|
|
||||||
if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) {
|
|
||||||
isNew = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return isNew;
|
|
||||||
},
|
|
||||||
|
|
||||||
_formatStr: function(str) {
|
|
||||||
if (this.options.caseSensitive) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
return $.trim(str.toLowerCase());
|
|
||||||
},
|
|
||||||
|
|
||||||
createTag: function(value, additionalClass) {
|
|
||||||
var that = this;
|
|
||||||
// Automatically trims the value of leading and trailing whitespace.
|
|
||||||
value = $.trim(value);
|
|
||||||
|
|
||||||
if (!this._isNew(value) || value === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
|
|
||||||
|
|
||||||
// Create tag.
|
|
||||||
var tag = $('<li></li>')
|
|
||||||
.addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
|
|
||||||
.addClass(additionalClass)
|
|
||||||
.append(label);
|
|
||||||
|
|
||||||
// Button for removing the tag.
|
|
||||||
var removeTagIcon = $('<span></span>')
|
|
||||||
.addClass('ui-icon ui-icon-close');
|
|
||||||
var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
|
|
||||||
.addClass('tagit-close')
|
|
||||||
.append(removeTagIcon)
|
|
||||||
.click(function(e) {
|
|
||||||
// Removes a tag when the little 'x' is clicked.
|
|
||||||
that.removeTag(tag);
|
|
||||||
});
|
|
||||||
tag.append(removeTag);
|
|
||||||
|
|
||||||
// Unless options.singleField is set, each tag has a hidden input field inline.
|
|
||||||
if (this.options.singleField) {
|
|
||||||
var tags = this.assignedTags();
|
|
||||||
tags.push(value);
|
|
||||||
this._updateSingleTagsField(tags);
|
|
||||||
} else {
|
|
||||||
var escapedValue = label.html();
|
|
||||||
tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.itemName + '[' + this.options.fieldName + '][]" />');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._trigger('onTagAdded', null, tag);
|
|
||||||
|
|
||||||
// Cleaning the input.
|
|
||||||
this._tagInput.val('');
|
|
||||||
|
|
||||||
// insert tag
|
|
||||||
this._tagInput.parent().before(tag);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeTag: function(tag, animate) {
|
|
||||||
animate = animate || this.options.animate;
|
|
||||||
|
|
||||||
tag = $(tag);
|
|
||||||
|
|
||||||
this._trigger('onTagRemoved', null, tag);
|
|
||||||
|
|
||||||
if (this.options.singleField) {
|
|
||||||
var tags = this.assignedTags();
|
|
||||||
var removedTagLabel = this.tagLabel(tag);
|
|
||||||
tags = $.grep(tags, function(el){
|
|
||||||
return el != removedTagLabel;
|
|
||||||
});
|
|
||||||
this._updateSingleTagsField(tags);
|
|
||||||
}
|
|
||||||
// Animate the removal.
|
|
||||||
if (animate) {
|
|
||||||
tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){
|
|
||||||
tag.remove();
|
|
||||||
}).dequeue();
|
|
||||||
} else {
|
|
||||||
tag.remove();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeAll: function() {
|
|
||||||
// Removes all tags.
|
|
||||||
var that = this;
|
|
||||||
this.tagList.children('.tagit-choice').each(function(index, tag) {
|
|
||||||
that.removeTag(tag, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
})(jQuery);
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Very simple jQuery Color Picker
|
||||||
|
* https://github.com/tkrotoff/jquery-simplecolorpicker
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2013 Tanguy Krotoff <tkrotoff@gmail.com>
|
||||||
|
*
|
||||||
|
* Licensed under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspired by Bootstrap Twitter.
|
||||||
|
* See https://github.com/twbs/bootstrap/blob/master/less/navbar.less
|
||||||
|
* See https://github.com/twbs/bootstrap/blob/master/less/dropdowns.less
|
||||||
|
*/
|
||||||
|
|
||||||
|
.simplecolorpicker.picker {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1051; /* Above Bootstrap modal (@zindex-modal = 1050) */
|
||||||
|
display: none;
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
min-width: 160px;
|
||||||
|
max-width: 283px; /* @popover-max-width = 276px + 7 */
|
||||||
|
|
||||||
|
padding: 5px 0 0 5px;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
list-style: none;
|
||||||
|
background-color: #fff; /* @dropdown-bg */
|
||||||
|
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker.inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker span {
|
||||||
|
margin: 0 5px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker.button,
|
||||||
|
.simplecolorpicker span.color {
|
||||||
|
display: inline-block;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker.button {
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker.button:after,
|
||||||
|
.simplecolorpicker span.color:after {
|
||||||
|
content: '\00a0\00a0\00a0\00a0'; /* Spaces */
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker span.color[data-disabled]:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker span.color:hover,
|
||||||
|
.simplecolorpicker span.color[data-selected],
|
||||||
|
.simplecolorpicker span.color[data-selected]:hover {
|
||||||
|
border: 1px solid #222; /* @gray-dark */
|
||||||
|
}
|
||||||
|
.simplecolorpicker span.color[data-selected]:after {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical separator, replaces optgroup. */
|
||||||
|
.simplecolorpicker span.vr {
|
||||||
|
border-left: 1px solid #222; /* @gray-dark */
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecolorpicker span.color[data-selected]:after {
|
||||||
|
/*font-family: 'FontAwesome';*/
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
|
content: '\2714'; /* Ok/check mark */
|
||||||
|
|
||||||
|
margin-right: 2px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
ul.tagit {
|
|
||||||
padding: 1px 5px;
|
|
||||||
overflow: auto;
|
|
||||||
margin-left: inherit; /* usually we don't want the regular ul margins. */
|
|
||||||
margin-right: inherit;
|
|
||||||
}
|
|
||||||
ul.tagit li {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
margin: 2px 5px 2px 0;
|
|
||||||
}
|
|
||||||
ul.tagit li.tagit-choice {
|
|
||||||
padding: .2em 18px .2em .5em;
|
|
||||||
position: relative;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
ul.tagit li.tagit-new {
|
|
||||||
padding: .25em 4px .25em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.tagit li.tagit-choice a.tagit-label {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
ul.tagit li.tagit-choice .tagit-close {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
right: .1em;
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* used for some custom themes that don't need image icons */
|
|
||||||
ul.tagit li.tagit-choice .tagit-close .text-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.tagit li.tagit-choice input {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
margin: 2px 5px 2px 0;
|
|
||||||
}
|
|
||||||
ul.tagit input[type="text"] {
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: inherit;
|
|
||||||
background-color: inherit;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
|
@ -1,239 +0,0 @@
|
||||||
#forum_list div.list-item {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list div.list-item .last-author {
|
|
||||||
font-size: 80%;
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list div.list-item .last-author a {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list > ul > li {
|
|
||||||
float: left;
|
|
||||||
width: 44%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 15px 3% 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list div.project-forums {
|
|
||||||
border-bottom: 1px solid #CCC;
|
|
||||||
padding-top: 20px;
|
|
||||||
clear: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list > ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0px;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list > ul > li.even {
|
|
||||||
margin-right: 0;
|
|
||||||
width: 46%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list > ul > li.odd {
|
|
||||||
clear: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forum_list > ul > li.odd, #forum_list > ul > li.even {
|
|
||||||
background-color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topic {
|
|
||||||
padding: 10px 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.topic p {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.topic h3.subject {
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.topic ul.meta {
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.topic ul.meta li {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #999;
|
|
||||||
margin-right: 8px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#topics_list div.title-bar h4 {
|
|
||||||
padding: 15px 0;
|
|
||||||
border-bottom: 1px dotted #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.questions-search {
|
|
||||||
background: url(/images/magnifier.png) no-repeat 6px 50%;
|
|
||||||
border: 1px solid #D7D7D7;
|
|
||||||
background-color: white;
|
|
||||||
padding-left: 30px;
|
|
||||||
border-radius: 3px;
|
|
||||||
height: 1.5em;
|
|
||||||
width: 94%;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.questions-search.ajax-loading {
|
|
||||||
background-image: url(/images/loading.gif);
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.reply {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.reply div.avatar {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.reply div.reply-details.use-avatar {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.reply .author{
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.reply .wiki > p:first-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.reply .contextual .icon.vote {
|
|
||||||
position: relative;
|
|
||||||
bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.details img.gravatar {
|
|
||||||
float: left;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.details p.author {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.message.details .wiki {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-top: 1px dotted #BBB;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Question meta */
|
|
||||||
|
|
||||||
#sidebar ul.question-meta, #sidebar ul.related-topics {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar ul.question-meta li {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar ul.related-topics li {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tags cloud */
|
|
||||||
|
|
||||||
#sidebar ul.questions-tags {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar ul.questions-tags li {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar ul.questions-tags span.count {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**********************************************************************/
|
|
||||||
/* TAGS
|
|
||||||
/**********************************************************************/
|
|
||||||
.message-tags-edit ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove {
|
|
||||||
background-color: #E5E5E5;
|
|
||||||
text-decoration: none;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit {
|
|
||||||
border: 1px solid #D7D7D7;
|
|
||||||
-moz-border-radius: 0px;
|
|
||||||
-webkit-border-radius: 0px;
|
|
||||||
-khtml-border-radius: 0px;
|
|
||||||
border-radius: 0px;
|
|
||||||
background: white;
|
|
||||||
padding: 0px;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit li.tagit-choice {
|
|
||||||
font-weight: normal;
|
|
||||||
-moz-border-radius: 0px;
|
|
||||||
-webkit-border-radius: 0px;
|
|
||||||
border-radius: 0px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: inherit;
|
|
||||||
padding-top: 0px;
|
|
||||||
padding-bottom: 0px;
|
|
||||||
background-color: #F7F7F7;
|
|
||||||
margin: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit li.tagit-choice {
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 11px;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit li.tagit-choice a.tagit-close {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit li.tagit-choice .tagit-close .text-icon {
|
|
||||||
display: inline;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit li.tagit-choice .ui-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit li.tagit-new {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-tags-edit ul.tagit li.tagit-new input {
|
|
||||||
font-size: 11px;
|
|
||||||
background: white;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
margin-left: 2px;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**********************************************************************/
|
|
||||||
/* ICONS
|
|
||||||
/**********************************************************************/
|
|
||||||
|
|
||||||
.icon-vote { background-image: url(../images/thumb_up.png); }
|
|
||||||
.icon-unvote { background-image: url(../images/unvote.png); }
|
|
||||||
.icon-view { background-image: url(../images/eye.png); }
|
|
||||||
.icon-calendar { background-image: url(/images/calendar.png); }
|
|
||||||
.icon-tag { background-image: url(../images/tag_blue.png); }
|
|
|
@ -0,0 +1,402 @@
|
||||||
|
/**********************************************************************/
|
||||||
|
/* SECTION list
|
||||||
|
/**********************************************************************/
|
||||||
|
|
||||||
|
.section-list {margin-top: 10px}
|
||||||
|
.section-list .section-tile {
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
width: 29%;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
margin: 0 .8% 20px;
|
||||||
|
padding: 1.5em 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-transition: background .15s;
|
||||||
|
-moz-transition: background .15s;
|
||||||
|
-o-transition: background .15s;
|
||||||
|
transition: background .15s;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-list .section-tile:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background: #f8f8f8
|
||||||
|
}
|
||||||
|
.section-list .section-tile .description {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
/**********************************************************************/
|
||||||
|
/* QUESTION index
|
||||||
|
/**********************************************************************/
|
||||||
|
|
||||||
|
.questions h2.section-title {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions .filters {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions-filters {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions-filters > ul > li {
|
||||||
|
list-style-type: none;
|
||||||
|
float: left;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions-filters > ul > li:not(:last-child):after {
|
||||||
|
content: " |"
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions-filters > ul > li a.selected {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list div.list-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list div.list-item .last-author {
|
||||||
|
font-size: 80%;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list div.list-item .last-author a {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list > ul > li {
|
||||||
|
float: left;
|
||||||
|
width: 44%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 15px 3% 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list div.project-forums {
|
||||||
|
border-bottom: 1px solid #CCC;
|
||||||
|
padding-top: 20px;
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list > ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list > ul > li.even {
|
||||||
|
margin-right: 0;
|
||||||
|
width: 46%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list > ul > li.odd {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forum_list > ul > li.odd, #forum_list > ul > li.even {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic {
|
||||||
|
padding: 20px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_container .topic{
|
||||||
|
padding: 5px 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.topic p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.topic h3.subject {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.topic ul.meta {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.topic ul.meta li {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-right: 8px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.topic ul.meta li a {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topics_container.votable .topic-vote {float: left; width: 60px; text-align: center;}
|
||||||
|
#topics_container.votable .topic-content {padding-left: 60px;}
|
||||||
|
#topics_container.votable .topic-vote .vote-score {display: block; font-size: 24px;}
|
||||||
|
#topics_container.votable .topic-vote .vote-score {display: block; font-size: 24px;}
|
||||||
|
#topics_container.votable .topic-vote label {color: #999; line-height: 1; font-size: 12px; margin-bottom: 3px; display: block;}
|
||||||
|
#topics_container.votable .topic-vote .status-answered {
|
||||||
|
height: 30px;
|
||||||
|
width: 35px;
|
||||||
|
background-position: -35px -75px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topics_list div.title-bar h4 {
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px dotted #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.questions-search {
|
||||||
|
background: url(/images/magnifier.png) no-repeat 6px 50%;
|
||||||
|
border: 1px solid #D7D7D7;
|
||||||
|
background-color: white;
|
||||||
|
padding-left: 30px !important;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 1.5em;
|
||||||
|
width: 94%;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.questions-search.ajax-loading {
|
||||||
|
background-image: url(/images/loading.gif);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**********************************************************************/
|
||||||
|
/* QUESTION show
|
||||||
|
/**********************************************************************/
|
||||||
|
|
||||||
|
h1.question-title {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.question.answer {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question a[disabled] {
|
||||||
|
color: #aaa;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.question img.gravatar {
|
||||||
|
float: left;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.question p.author {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-status-tag {
|
||||||
|
font-family: Verdana, sans-serif;
|
||||||
|
background-color: #759FCF;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: white;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Question vote*/
|
||||||
|
|
||||||
|
div.question {display: table; width: 100%;}
|
||||||
|
div.question.votable .vote {display: table-cell; padding: 0px 12px;}
|
||||||
|
div.question.votable .question-container {display: table-cell;vertical-align: top;}
|
||||||
|
div.question.votable .question-container .contextual {margin-top: 0px;}
|
||||||
|
|
||||||
|
.question .vote a.disabled {pointer-events: none;opacity: 0.5}
|
||||||
|
.question .vote {font-size: 24px; width: 35px;}
|
||||||
|
|
||||||
|
.question .vote .vote-up,
|
||||||
|
.question .vote .vote-down,
|
||||||
|
.question .vote .vote-count,
|
||||||
|
.question .vote .accepted {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question .vote .vote-up,
|
||||||
|
.question .vote .vote-down,
|
||||||
|
.question .vote .accepted,
|
||||||
|
#topics_container.votable .topic-vote .status-answered {
|
||||||
|
background-image: url(../images/voting.svg);
|
||||||
|
background-size: initial;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question .vote .accepted {cursor: default; background-position: 0px -69px;}
|
||||||
|
.question .vote .vote-up {background-position: -70px 0px;}
|
||||||
|
.question .vote .vote-down {background-position: -35px 0px;}
|
||||||
|
.question .vote .vote-count {height: 32px;}
|
||||||
|
/*.question .vote .vote-up:hover {background-position: -70px -35px;}
|
||||||
|
.question .vote .vote-down:hover {background-position: -35px -35px;}
|
||||||
|
*/
|
||||||
|
.question div.attachments {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Question meta */
|
||||||
|
|
||||||
|
#sidebar ul.question-meta, #sidebar ul.related-topics {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul.question-meta li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul.related-topics li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags cloud */
|
||||||
|
|
||||||
|
#sidebar ul.questions-tags {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul.questions-tags li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar ul.questions-tags span.count {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************************************************/
|
||||||
|
/* SOLUTION show
|
||||||
|
/**********************************************************************/
|
||||||
|
.question.solution > h2 {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question.solution .liking {
|
||||||
|
padding: 10px 0px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
margin: 10px 0px
|
||||||
|
}
|
||||||
|
|
||||||
|
.question.solution .liking > a {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.question.solution .liking .author {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**********************************************************************/
|
||||||
|
/* COMMENTS
|
||||||
|
/**********************************************************************/
|
||||||
|
|
||||||
|
.comments_container {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 10px 0px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-comments .comment .contextual {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-comments .comment:hover .contextual {
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-comments .comment .contextual:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.question-comments .comment {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px dashed #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-comments .comment:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-comments .comment .author {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-comments .comment .wiki-content {
|
||||||
|
color: #707070;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.comments_container .comment .wiki-content p {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-comments .comment .wiki-content p:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-comment-link {
|
||||||
|
font-size: 0.9em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add_comments {padding: 10px 10px 10px;}
|
||||||
|
.add_comments:not(:first-child) {border-top: 1px dashed #ccc;}
|
||||||
|
|
||||||
|
.add_comments textarea {
|
||||||
|
width: 98%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Answers*/
|
||||||
|
|
||||||
|
#answers {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accepted_answer{
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**********************************************************************/
|
||||||
|
/* ICONS
|
||||||
|
/**********************************************************************/
|
||||||
|
|
||||||
|
.icon-vote { background-image: url(../images/thumb_up.png); }
|
||||||
|
.icon-unvote { background-image: url(../images/unvote.png); }
|
||||||
|
.icon-downvote { background-image: url(../images/thumb_down.png); }
|
||||||
|
.icon-view { background-image: url(../images/eye.png); }
|
||||||
|
.icon-calendar { background-image: url(/images/calendar.png); }
|
||||||
|
.icon-tag { background-image: url(../images/tag_blue.png); }
|
||||||
|
.icon-question { background-image: url(../../../images/help.png); }
|
||||||
|
.icon-solution { background-image: url(../images/book_open.png); }
|
|
@ -6,13 +6,13 @@ en:
|
||||||
label_questions_new: New question
|
label_questions_new: New question
|
||||||
label_questions_message: Message
|
label_questions_message: Message
|
||||||
label_questions_added_time: "Added %{value} ago"
|
label_questions_added_time: "Added %{value} ago"
|
||||||
label_questions_related_messages: Related messages
|
label_questions_related_questions: Related questions
|
||||||
label_questions_latest_messages: Latest messages
|
label_questions_latest_messages: Latest messages
|
||||||
label_questions_views: '%{count} views'
|
label_questions_views: '%{count} views'
|
||||||
label_questions_votes: '%{count} votes'
|
label_questions_votes: '%{count} votes'
|
||||||
label_questions_answers: '%{count} answers'
|
label_questions_answers: '%{count} answers'
|
||||||
label_questions_tags: Tags
|
label_questions_tags: Tags
|
||||||
label_questions_tagged_by: '%{count} topic(s) tagged by %{tag}'
|
label_questions_tagged_by: '%{count} item(s) tagged by %{tag}'
|
||||||
label_questions_sidebar_message: Sidebar message
|
label_questions_sidebar_message: Sidebar message
|
||||||
label_questions_notice: Notice
|
label_questions_notice: Notice
|
||||||
label_questions_most_voted: Most voted
|
label_questions_most_voted: Most voted
|
||||||
|
@ -24,9 +24,31 @@ en:
|
||||||
|
|
||||||
field_questions_tags: Tags
|
field_questions_tags: Tags
|
||||||
|
|
||||||
permission_view_questions: View Help & Support
|
label_questions_add_comment: Add comment
|
||||||
permission_edit_messages_tags: Edit tags
|
label_question_comment_successful_added: Comment successful added
|
||||||
permission_edit_vote_messages: Vote messages
|
label_question_successful_update: Question successful updated
|
||||||
|
label_answer_successful_update: Answer successful updated
|
||||||
|
label_answer_successful_added: Answer successful added
|
||||||
|
lebel_questions_new_comment: New comment
|
||||||
|
|
||||||
|
button_questions_to_issue: Convert to issue
|
||||||
|
button_questions_issue_to_question: Convert to question
|
||||||
|
label_questions_actions: Actions
|
||||||
|
label_questions_accept: Accept
|
||||||
|
|
||||||
|
label_questions_section_type_questions: Questions
|
||||||
|
label_questions_section_type_solutions: Solutions
|
||||||
|
label_questions_section_type_ideas: Ideas
|
||||||
|
|
||||||
|
label_questions_section: Section
|
||||||
|
label_questions_sections_plural: Sections
|
||||||
|
label_questions_section_new: New section
|
||||||
|
label_questions_section_type: Section type
|
||||||
|
label_questions_topic: Topic
|
||||||
|
label_questions_featured: Featured
|
||||||
|
label_questions_locked: Locked
|
||||||
|
|
||||||
|
project_module_questions: Questions
|
||||||
|
|
||||||
i18n:
|
i18n:
|
||||||
transliterate:
|
transliterate:
|
||||||
|
@ -105,4 +127,46 @@ en:
|
||||||
"Ь": ""
|
"Ь": ""
|
||||||
"Э": "E"
|
"Э": "E"
|
||||||
"Ю": "YU"
|
"Ю": "YU"
|
||||||
"Я": "YA"
|
"Я": "YA"
|
||||||
|
label_questions_comment: Comment
|
||||||
|
label_questions_your_answer: Your answer
|
||||||
|
label_questions_answer_plural: Answers
|
||||||
|
label_questions_x_votes:
|
||||||
|
one: "vote"
|
||||||
|
other: "votes"
|
||||||
|
label_questions_wasthishelpful: Was this helpful?
|
||||||
|
|
||||||
|
label_question_plural: Questions
|
||||||
|
label_questions_related_solutions: Related solutions
|
||||||
|
label_questions_section_edit: Edit section
|
||||||
|
label_soluition_plural: Solutions
|
||||||
|
label_questions_add_tag: '+ add tag'
|
||||||
|
label_questions_vote_added: Vote added
|
||||||
|
label_questions_vote_removed: Vote removed
|
||||||
|
label_questions_vote_own: Vote own questions
|
||||||
|
label_questions_show_popular: Show popular articles in section
|
||||||
|
label_questions_most_popular: Most popular
|
||||||
|
label_questions_status_plural: Idea statuses
|
||||||
|
label_questions_status_closed: Closed
|
||||||
|
label_questions_status_new: New status
|
||||||
|
label_questions_newest: Newest
|
||||||
|
label_questions_active: Active
|
||||||
|
label_questions_voted: Voted
|
||||||
|
label_questions_unanswered: Unanswered
|
||||||
|
|
||||||
|
permission_view_questions: View questions
|
||||||
|
permission_create_tags: Create new tags
|
||||||
|
permission_edit_vote_messages: Vote questions
|
||||||
|
permission_add_answers: Add answers
|
||||||
|
permission_delete_answers: Delete answers
|
||||||
|
permission_edit_question_comments: Edit comments
|
||||||
|
permission_edit_own_question_comments: Edit own comments
|
||||||
|
permission_comment_question: Add comments
|
||||||
|
permission_add_questions: Add questions
|
||||||
|
permission_edit_questions: Edit questions
|
||||||
|
permission_edit_own_questions: Edit own questions
|
||||||
|
permission_delete_questions: Delete questions
|
||||||
|
permission_vote_questions: Vote questions
|
||||||
|
permission_accept_answers: Accept answers
|
||||||
|
permission_manage_sections: Manage sections
|
||||||
|
permission_create_tags: Create new tags
|
|
@ -4,7 +4,7 @@ es:
|
||||||
label_questions_new: Nueva pregunta
|
label_questions_new: Nueva pregunta
|
||||||
label_questions_message: Mensaje
|
label_questions_message: Mensaje
|
||||||
label_questions_added_time: "Agregado hace %{value}"
|
label_questions_added_time: "Agregado hace %{value}"
|
||||||
label_questions_related_messages: Mensajes relacionados
|
label_questions_related_questions: Mensajes relacionados
|
||||||
label_questions_latest_messages: Últimos mensajes
|
label_questions_latest_messages: Últimos mensajes
|
||||||
label_questions_views: '%{count} vistas'
|
label_questions_views: '%{count} vistas'
|
||||||
label_questions_votes: '%{count} votos'
|
label_questions_votes: '%{count} votos'
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
# Russian strings go here for Rails i18n
|
# Russian strings go here for Rails i18n
|
||||||
|
# encoding: utf-8
|
||||||
|
# English strings go here for Rails i18n
|
||||||
ru:
|
ru:
|
||||||
label_questions: 'Поддержка'
|
label_questions: 'Вопросы/Ответы'
|
||||||
label_questions_search: 'Поиск вопросов и ответов ...'
|
label_questions_search: 'Поиск вопросов и ответов ...'
|
||||||
label_questions_new: Новая тема
|
label_questions_new: Новая тема
|
||||||
label_questions_message: Сообщение
|
label_questions_message: Сообщение
|
||||||
label_questions_added_time: "Добавлена %{value} назад"
|
label_questions_added_time: "Добавлена %{value} назад"
|
||||||
label_questions_related_messages: Связанные темы
|
label_questions_related_questions: Связанные вопросы
|
||||||
label_questions_latest_messages: Последние сообщения
|
label_questions_latest_messages: Последние сообщения
|
||||||
label_questions_views: '%{count} просмотров'
|
label_questions_views: '%{count} просмотров'
|
||||||
label_questions_votes: '%{count} голосов'
|
label_questions_votes: '%{count} голосов'
|
||||||
|
@ -23,6 +25,73 @@ ru:
|
||||||
|
|
||||||
field_questions_tags: Тэги
|
field_questions_tags: Тэги
|
||||||
|
|
||||||
|
|
||||||
|
label_questions_add_comment: Комментировать
|
||||||
|
label_question_comment_successful_added: Комментарий добавлен
|
||||||
|
label_question_successful_update: Вопрос обновлен
|
||||||
|
label_answer_successful_update: Ответ обновлен
|
||||||
|
label_answer_successful_added: Ответ добавлен
|
||||||
|
lebel_questions_new_comment: Новый комментарий
|
||||||
|
|
||||||
|
button_questions_to_issue: Конв. в задачу
|
||||||
|
button_questions_issue_to_question: Конв. в вопрос
|
||||||
|
label_questions_actions: Действия
|
||||||
|
label_questions_accept: Принять
|
||||||
|
|
||||||
|
label_questions_section_type_questions: Вопросы
|
||||||
|
label_questions_section_type_solutions: Решения
|
||||||
|
label_questions_section_type_ideas: Идеи
|
||||||
|
|
||||||
|
label_questions_section: Раздел
|
||||||
|
label_questions_sections_plural: Разделы
|
||||||
|
label_questions_section_new: Новый раздел
|
||||||
|
label_questions_section_type: Тип раздела
|
||||||
|
label_questions_featured: Выделено
|
||||||
|
label_questions_locked: Заблокировано
|
||||||
|
|
||||||
|
project_module_questions: Вопросы/Ответы
|
||||||
|
|
||||||
|
label_questions_comment: Комментарий
|
||||||
|
label_questions_your_answer: Ваш ответ
|
||||||
|
label_questions_answer_plural: Оветы
|
||||||
|
label_questions_x_votes:
|
||||||
|
one: "голос"
|
||||||
|
few: "голоса"
|
||||||
|
many: "голосов"
|
||||||
|
other: "голоса"
|
||||||
|
label_questions_wasthishelpful: Полезно ли Вам было?
|
||||||
|
|
||||||
|
label_question_plural: Вопросы
|
||||||
|
label_questions_related_solutions: Связанные решения
|
||||||
|
label_questions_section_edit: Редактировать раздел
|
||||||
|
label_soluition_plural: Решения
|
||||||
|
label_questions_add_tag: '+ добавить тэг'
|
||||||
|
label_questions_vote_added: Голос добавлен
|
||||||
|
label_questions_vote_removed: Голос удален
|
||||||
|
label_questions_vote_own: Голосовать за свои вопросы
|
||||||
|
label_questions_show_popular: Показыть популярные вопросы на списке секций
|
||||||
|
label_questions_most_popular: Популярные
|
||||||
|
label_questions_status_plural: Статусы идеи
|
||||||
|
label_questions_status_closed: Закрыто
|
||||||
|
label_questions_status_new: Новый статус
|
||||||
|
label_questions_newest: Новейшие
|
||||||
|
label_questions_active: Активные
|
||||||
|
label_questions_voted: Голоса
|
||||||
|
label_questions_unanswered: Неотвеченные
|
||||||
|
|
||||||
permission_view_questions: Просматривать вопросы ответы
|
permission_view_questions: Просматривать вопросы ответы
|
||||||
permission_edit_messages_tags: Редактирвоать тэги
|
permission_create_tags: Создавать тэги
|
||||||
permission_edit_vote_messages: Голосовать
|
permission_edit_vote_messages: Голосовать
|
||||||
|
permission_add_answers: Добавить ответы
|
||||||
|
permission_delete_answers: Удалить ответы
|
||||||
|
permission_edit_question_comments: Редактировать комментарии
|
||||||
|
permission_edit_own_question_comments: Редактировать свои комментарии
|
||||||
|
permission_comment_question: Добавить комментарии
|
||||||
|
permission_add_questions: Добавить вопросы
|
||||||
|
permission_edit_questions: Редактировать вопросы
|
||||||
|
permission_edit_own_questions: Редактировать свои вопросы
|
||||||
|
permission_delete_questions: Удалить вопросы
|
||||||
|
permission_vote_questions: Голосовать за вопросы
|
||||||
|
permission_accept_answers: Принимать ответы
|
||||||
|
permission_manage_sections: Управлять секциями
|
||||||
|
permission_create_tags: Создавать новые тэги
|
|
@ -2,11 +2,10 @@
|
||||||
# Simplified Chinese strings go here for Rails i18n
|
# Simplified Chinese strings go here for Rails i18n
|
||||||
# Author: Salivaxiu@163.com
|
# Author: Salivaxiu@163.com
|
||||||
# Based on file: en.yml
|
# Based on file: en.yml
|
||||||
|
|
||||||
zh:
|
zh:
|
||||||
label_questions: '帮助与支持'
|
label_questions: '帮助与支持'
|
||||||
label_questions_search: '搜索特定的问题或回答或帖子...'
|
label_questions_search: '搜索特定的问题或回答或帖子...'
|
||||||
label_questions_new: 新问题
|
label_questions_new: 新问答
|
||||||
label_questions_message: 消息
|
label_questions_message: 消息
|
||||||
label_questions_added_time: "在 %{value} 之前增加"
|
label_questions_added_time: "在 %{value} 之前增加"
|
||||||
label_questions_related_messages: 相关的消息
|
label_questions_related_messages: 相关的消息
|
||||||
|
@ -16,7 +15,7 @@ zh:
|
||||||
label_questions_answers: '回答 %{count} 次'
|
label_questions_answers: '回答 %{count} 次'
|
||||||
label_questions_tags: 标签
|
label_questions_tags: 标签
|
||||||
label_questions_tagged_by: '%{count} 个主题包含标签 %{tag}'
|
label_questions_tagged_by: '%{count} 个主题包含标签 %{tag}'
|
||||||
label_questions_notice_message: 通知消息
|
label_questions_sidebar_message: 侧边栏信息
|
||||||
label_questions_notice: 通知
|
label_questions_notice: 通知
|
||||||
label_questions_most_voted: 大多数投票
|
label_questions_most_voted: 大多数投票
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,65 @@
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
# Plugin's routes
|
# Plugin's routes
|
||||||
# See: http://guides.rubyonrails.org/routing.html
|
# See: http://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
|
# match '/news/:id/comments', :to => 'comments#create', :via => :post
|
||||||
|
# match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
|
||||||
resources :questions do
|
resources :questions do
|
||||||
collection do
|
collection do
|
||||||
get :autocomplete_for_topic
|
put :preview
|
||||||
|
put :update_form
|
||||||
|
# match :preview, :to => 'questions#preview', :via => [:get, :put, :post]
|
||||||
|
get :autocomplete_for_subject
|
||||||
get :topics
|
get :topics
|
||||||
|
get :index_public
|
||||||
end
|
end
|
||||||
member do
|
member do
|
||||||
get :vote
|
get :from_issue
|
||||||
|
# post :new_comment
|
||||||
|
end
|
||||||
|
resources :questions_answers, :as => :answers
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :questions_answers, :except => [:show, :index] do
|
||||||
|
collection do
|
||||||
|
put :preview
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
match "issues/:issue_id/move_to_forum/:board_id" => "questions#convert_issue", :via => [:get, :post]
|
match "questions_votes", :to => 'questions_votes#create', :via => [:get, :post], :as => 'questions_votes'
|
||||||
|
|
||||||
|
resources :questions_comments do
|
||||||
|
member do
|
||||||
|
post :update
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :questions_sections
|
||||||
|
resources :questions_statuses, :except => :show
|
||||||
|
|
||||||
|
resources :projects do
|
||||||
|
resources :questions_sections
|
||||||
|
resources :questions
|
||||||
|
end
|
||||||
|
|
||||||
|
match "projects/:project_id/questions/questions_sections/:section_id" => "questions#index", :via => [:get]
|
||||||
|
match "questions/questions_sections/:section_id" => "questions#index", :via => [:get]
|
||||||
|
match 'auto_completes/questions_tags' => 'auto_completes#questions_tags', :via => :get, :as => 'auto_complete_questions_tags'
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
class ActsAsVotableMigration < ActiveRecord::Migration
|
# 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 ActsAsVotableMigration < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
|
||||||
def self.up
|
def self.up
|
||||||
ActiveRecord::Base.create_votable_table
|
ActiveRecord::Base.create_votable_table
|
||||||
add_column :messages, :cached_votes_total, :integer, :default => 0
|
|
||||||
add_column :messages, :cached_votes_up, :integer, :default => 0
|
|
||||||
add_column :messages, :cached_votes_down, :integer, :default => 0
|
|
||||||
add_index :messages, :cached_votes_total
|
|
||||||
add_index :messages, :cached_votes_up
|
|
||||||
add_index :messages, :cached_votes_down
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
ActiveRecord::Base.drop_votable_table
|
|
||||||
remove_column :messages, :cached_votes_total
|
|
||||||
remove_column :messages, :cached_votes_up
|
|
||||||
remove_column :messages, :cached_votes_down
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,29 @@
|
||||||
class AddViewingMigration < ActiveRecord::Migration
|
# 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 AddViewingMigration < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
|
||||||
def self.up
|
def self.up
|
||||||
ActiveRecord::Base.create_viewings_table
|
unless table_exists?(:viewings)
|
||||||
|
ActiveRecord::Base.create_viewings_table
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
ActiveRecord::Base.drop_viewings_table
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,23 @@
|
||||||
class AddTaggingMigration < ActiveRecord::Migration
|
# 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 AddTaggingMigration < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
|
||||||
def up
|
def up
|
||||||
ActiveRecord::Base.create_taggable_table
|
ActiveRecord::Base.create_taggable_table
|
||||||
end
|
end
|
||||||
|
|
39
plugins/redmine_questions/db/migrate/004_create_questions.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# 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 CreateQuestions < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
|
||||||
|
def change
|
||||||
|
create_table :questions do |t|
|
||||||
|
t.string :subject
|
||||||
|
t.text :content
|
||||||
|
t.references :section, :index => true
|
||||||
|
t.references :status, :index => true
|
||||||
|
t.references :author, :index => true
|
||||||
|
t.boolean :featured, :default => false
|
||||||
|
t.boolean :locked, :default => false
|
||||||
|
t.integer :cached_weighted_score, :default => 0
|
||||||
|
t.integer :comments_count, :default => 0
|
||||||
|
t.integer :answers_count, :default => 0
|
||||||
|
t.integer :views, :default => 0
|
||||||
|
t.integer :total_views, :default => 0
|
||||||
|
t.datetime :created_on
|
||||||
|
t.datetime :updated_on
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
# 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 CreateQuestionsSections < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
|
||||||
|
def change
|
||||||
|
create_table :questions_sections do |t|
|
||||||
|
t.string :name
|
||||||
|
t.text :description
|
||||||
|
t.references :project, :index => true
|
||||||
|
t.string :section_type
|
||||||
|
t.integer :position
|
||||||
|
end
|
||||||
|
add_index :questions_sections, :position
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,35 @@
|
||||||
|
# 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 CreateQuestionsAnswers < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
|
||||||
|
def change
|
||||||
|
create_table :questions_answers do |t|
|
||||||
|
t.text :content
|
||||||
|
t.references :author, :index => true
|
||||||
|
t.references :question, :index => true
|
||||||
|
t.boolean :accepted, :default => false
|
||||||
|
t.integer :cached_weighted_score, :default => 0
|
||||||
|
t.integer :comments_count, :default => 0
|
||||||
|
t.datetime :created_on
|
||||||
|
t.datetime :updated_on
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :questions_answers, :accepted
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
# 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 CreateQuestionsStatuses < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
|
||||||
|
def change
|
||||||
|
create_table :questions_statuses do |t|
|
||||||
|
t.string :name
|
||||||
|
t.boolean :is_closed, :default => false
|
||||||
|
t.string :color
|
||||||
|
t.integer :position
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :questions_statuses, :is_closed
|
||||||
|
add_index :questions_statuses, :position
|
||||||
|
end
|
||||||
|
end
|
14
plugins/redmine_questions/doc/CHANGELOG
Normal file → Executable file
|
@ -1,13 +1,19 @@
|
||||||
== Redmine Q&A plugin changelog
|
== Redmine Q&A plugin changelog
|
||||||
|
|
||||||
Redmine Q&A plugin
|
Redmine Q&A plugin
|
||||||
Copyright (C) 2011-2016 Kirill Bezrukov
|
Copyright (C) 2011-2018 RedmineUP
|
||||||
http://www.redminecrm.com/
|
https://www.redmineup.com/
|
||||||
|
|
||||||
== 2016-01-18 v0.0.7
|
== 2018-08-31 v1.0.0
|
||||||
|
|
||||||
|
* Separate models for questions, answers and comments
|
||||||
|
* Voting for questions and answers
|
||||||
|
* Comments for questions and asnwers
|
||||||
|
* Section types: Question, Idea, Solution
|
||||||
|
* Different main page styles
|
||||||
|
* Answered question status
|
||||||
|
* Idea statutes
|
||||||
* Chinese translation (zhoutt)
|
* Chinese translation (zhoutt)
|
||||||
* Redmine 3+ support
|
|
||||||
|
|
||||||
== 2014-02-04 v0.0.6
|
== 2014-02-04 v0.0.6
|
||||||
|
|
||||||
|
|
|
@ -336,4 +336,4 @@ This General Public License does not permit incorporating your program into
|
||||||
proprietary programs. If your program is a subroutine library, you may
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
consider it more useful to permit linking proprietary applications with the
|
consider it more useful to permit linking proprietary applications with the
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License.
|
Public License instead of this License.
|