Nuevo plugin Redmine Wiki Lists 0.0.9

This commit is contained in:
Manuel Cillero 2019-02-06 19:04:50 +01:00
parent 9ea9aa9cb1
commit ae790c4ff9
8 changed files with 1130 additions and 0 deletions

View file

@ -0,0 +1,3 @@
module RedmineWikiLists
# config will be here
end

View file

@ -0,0 +1,44 @@
module RedmineWikiLists::IssueNameLink
Redmine::WikiFormatting::Macros.register do
desc 'Make a link of a issue by its subject.'
macro :issue_name_link do |obj, args|
out = ''
begin
raise '- no parameters' if args.count.zero?
raise '- too many parameters' if args.count > 1
arg = args.shift
arg.strip!
if arg =~ /\A([^:]*):([^:]*)\z/
prj = Project.find_by_identifier($1)
prj ||= Project.find_by_name($1)
raise "- project:#{$1} is not found." unless prj
arg = $2
else
prj = obj.project
end
if arg =~ /\A([^\|]*)\|([^\|]*)\z/
name = $1
disp = $2
else
name = arg
disp = arg
end
issue = Issue.where(project_id: prj.id, subject: name).first
raise "- issue:#{name} is not found in prj:#{prj.to_s}" unless issue
Issue.find_by_subject(name)
out << link_to("#{disp}", issue_path(issue))
rescue => err_msg
raise <<-TEXT.html_safe
Parameter error: #{err_msg}<br>
Usage: {{issue_name_link([project_name:]issue_subject[|description])}}
TEXT
end
out.html_safe
end
end
end

View file

@ -0,0 +1,225 @@
module RedmineWikiLists::RefIssues
Redmine::WikiFormatting::Macros.register do
desc 'Displays a list of referer issues.'
macro :ref_issues do |obj, args|
parser = nil
begin
parser = RedmineWikiLists::RefIssues::Parser.new(obj, args, @project)
rescue => err_msg
attributes = IssueQuery.available_columns
msg = <<-TEXT
- <br>parameter error: #{err_msg}<br>
#{err_msg.backtrace[0]}<br><br>
usage: {{ref_issues([option].., [column]..)}}<br>
<br>[options]<br>
-i=CustomQueryID : specify custom query by id<br>
-q=CustomQueryName : specify custom query by name<br>
-p[=identifier] : restrict project<br>
-f:FILTER[=WORD[|WORD...]] : additional filter<br>
-t[=column] : display text<br>
-l[=column] : display linked text<br>
-c : count issues<br>
-0 : no display if no issues
<br>[columns]<br> {
TEXT
while attributes
attributes[0...5].each do |a|
msg += a.name.to_s + ', '
end
attributes = attributes[5..-1]
msg += '<br>' if attributes
end
msg += 'cf_* }<br/>'
raise msg.html_safe
end
begin
unless parser.has_search_conditions? # 検索条件がなにもなかったら
# 検索するキーワードを取得する
parser.search_words_w << parser.default_words(obj)
end
@query = parser.query @project
extend SortHelper
extend QueriesHelper
extend IssuesHelper
sort_clear
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
#@issue_count_by_group = @query.issue_count_by_group
parser.search_words_s.each do |words|
@query.add_filter('subject', '~', words)
end
parser.search_words_d.each do |words|
@query.add_filter('description', '~', words)
end
parser.search_words_w.each do |words|
@query.add_filter('subjectdescription', '~', words)
end
models =
{'tracker' => Tracker,
'category' => IssueCategory,
'status' => IssueStatus,
'assigned_to' => User,
'author' => User,
'version' => Version,
'project' => Project}
ids =
{'tracker' => 'tracker_id',
'category' => 'category_id',
'status' => 'status_id',
'assigned_to' => 'assigned_to_id',
'author' => 'author_id',
'version' => 'fixed_version_id',
'project' => 'project_id'}
attributes =
{'tracker' => 'name',
'category' => 'name',
'status' => 'name',
'assigned_to' => 'login',
'author' => 'login',
'version' => 'name',
'project' => 'name'}
parser.additional_filter.each do |filter_set|
filter = filter_set[:filter]
operator = filter_set[:operator]
values = filter_set[:values]
if models.has_key?(filter)
unless values.nil?
tgt_objs = []
values.each do |value|
tgt_obj = models[filter].find_by(attributes[filter]=>value)
raise "- can not resolve '#{value}' in #{models[filter].to_s}.#{attributes[filter]} " if tgt_obj.nil?
tgt_objs << tgt_obj.id.to_s
end
values = tgt_objs
end
filter = ids[filter]
end
res = @query.add_filter(filter , operator, values)
if res.nil?
filter_str = filter_set[:filter] + filter_set[:operator] + filter_set[:values].join('|')
cr_count = 0
msg = "- failed add_filter: #{filter_str}<br><br>[FILTER]<br>"
@query.available_filters.each do |k,f|
if cr_count >= 5
msg += '<br>'
cr_count = 0
end
msg += k.to_s + ', '
cr_count += 1
end
models.each do |k, _m|
if cr_count >= 5
msg += '<br>'
cr_count = 0
end
msg += k.to_s + ', '
cr_count += 1
end
msg += '<br><br>[OPERATOR]<br>'
cr_count = 0
Query.operators_labels.each do |k, l|
if cr_count >= 5
msg += '<br>'
cr_count = 0
end
msg += k + ':' + l + ', '
cr_count += 1
end
msg += '<br>'
raise msg.html_safe
end
end
@query.column_names = parser.columns unless parser.columns.empty?
@issues = @query.issues(order: sort_clause)
if parser.zero_flag && @issues.size == 0
disp = ''
elsif parser.only_text || parser.only_link
disp = ''
atr = parser.only_text if parser.only_text
atr = parser.only_link if parser.only_link
word = nil
@issues.each do |issue|
if issue.attributes.has_key?(atr)
word = issue.attributes[atr].to_s
else
issue.custom_field_values.each do |cf|
if 'cf_'+cf.custom_field.id.to_s == atr || cf.custom_field.name == atr
word = cf.value
end
end
end
if word.nil?
msg = 'attributes:'
issue.attributes.each do |a|
msg += a.to_s + ', '
end
raise msg.html_safe
break
end
disp << ' ' if disp.size!=0
if parser.only_link
disp << link_to("#{word}", issue_path(issue))
else
disp << textilizable(word, object: issue)
end
end
elsif parser.count_flag
disp = @issues.size.to_s
else
if params[:format] == 'pdf'
disp = render(partial: 'issues/list.html', locals: {issues: @issues, query: @query})
else
if method(:context_menu).parameters.size > 0
disp = context_menu(issues_context_menu_path) # < redmine 3.3.x
else
disp = context_menu.to_s # >= redmine 3.4.0
end
disp << render(partial: 'issues/list', locals: {issues: @issues, query: @query})
end
end
disp.html_safe
rescue => err_msg
msg = "#{err_msg}"
if msg[0] != '-'
err_msg.backtrace.each do |backtrace|
msg << "<br>#{backtrace}"
end
end
raise msg.html_safe
end
end
end
end

View file

@ -0,0 +1,297 @@
# To change this template, choose Tools | Templates
# and open the template in the editor.
module RedmineWikiLists
module RefIssues
class Parser
attr_reader :search_words_s, :search_words_d, :search_words_w, :columns,
:custom_query_name, :custom_query_id, :additional_filter, :only_text, :only_link, :count_flag, :zero_flag
def initialize(obj, args = nil, project = nil)
parse_args(obj, args, project) if args
end
def parse_args(obj, args, project)
args ||= []
@project = project
@search_words_s = []
@search_words_d = []
@search_words_w = []
@columns = []
@restrict_project = nil
@additional_filter = []
@only_link = nil
@only_text = nil
@count_flag = nil
@zero_flag = nil
args.each do |arg|
arg.strip!
arg.gsub!('&gt;', '>')
arg.gsub!('&lt;', '<')
if arg=~/\A\-([^\=:]*)\s*([\=:])\s*(.*)\z/
opt = $1.strip
sep = $2.strip
words = $3.strip
elsif arg=~/\A\-([^\=:]*)\z/
opt = $1.strip
sep = nil
words = default_words(obj).join('|')
else
@columns << get_column(arg)
next
end
case opt
when 's','sw','Dw','sDw','Dsw'
@search_words_s.push words_to_word_array(obj, words)
when 'd','dw','Sw','Sdw','dSw'
@search_words_d.push words_to_word_array(obj, words)
when 'w','sdw'
@search_words_w.push words_to_word_array(obj, words)
when 'q'
if sep
@custom_query_name = words
else
raise "- no CustomQuery name:#{arg}"
end
when 'i'
if sep
@custom_query_id = words
else
raise "- no CustomQuery ID:#{arg}"
end
when 'p'
@restrict_project = sep ? Project.find(words) : project
when 'f'
if sep
filter = ''
operator = ''
values = nil
if words =~ /\A([^\s]*)\s+([^\s]*)\z/
filter = $1
operator = refer_field(obj, $2)
elsif words =~ /\A([^\s]*)\s+([^\s]*)\s+(.*)\z/
filter = $1
operator = refer_field(obj, $2)
values = words_to_word_array(obj, $3)
elsif words =~ /\A(.*)=(.*)\z/
filter = $1
operator = '='
values = words_to_word_array(obj, $2)
else
filter = words
operator = '='
values = default_words(obj)
end
@additional_filter << {:filter=>filter, :operator=>operator, :values=>values}
else
raise "- no additional filter:#{arg}"
end
when 't'
@only_text = sep ? words : 'subject'
when 'l'
@only_link = sep ? words : 'subject'
when 'c'
@count_flag = true
when '0'
@zero_flag = true
else
raise "- unknown option:#{arg}"
end
end
end
def has_search_conditions?
return true if @custom_query_id
return true if @custom_query_name
return true if @search_words_s.present?
return true if @search_words_d.present?
return true if @search_words_w.present?
return true if @additional_filter.present?
false
end
def query(project)
# オプションにカスタムクエリがあればカスタムクエリを名前から取得
if @custom_query_id
@query = IssueQuery.visible.find_by_id(@custom_query_id)
raise "- can not find CustomQuery ID:'#{@custom_query_id}'" unless @query
elsif @custom_query_name then
cond = 'project_id IS NULL'
cond << " OR project_id = #{project.id}" if project
cond = "(#{cond}) AND name = '#{@custom_query_name}'"
@query = IssueQuery.where(cond).where(user_id: User.current.id).first
@query = IssueQuery.where(cond).where(visibility: Query::VISIBILITY_PUBLIC).first unless @query
raise "- can not find CustomQuery Name:'#{@custom_query_name}'" unless @query
else
@query = IssueQuery.new(name: '_', filters: {})
end
@query.user = User.current
if @restrict_project
@query.project = @restrict_project
end
# Queryモデルを拡張
overwrite_sql_for_field(@query)
@query.available_filters['description'] = {type: :text, order: 8}
@query.available_filters['subjectdescription'] = {type: :text, order: 8}
@query.available_filters['fixed_version_id'] = {type: :int}
@query.available_filters['category_id'] = {type: :int}
@query.available_filters['parent_id'] = {type: :int}
@query.available_filters['id'] = {type: :int}
@query.available_filters['treated'] = {type: :date}
@query
end
def default_words(obj)
words = []
if obj.class == WikiContent # Wikiの場合はページ名および別名を検索ワードにする
words.push(obj.page.title) #ページ名
redirects = WikiRedirect.where(redirects_to: obj.page.title) #別名query
redirects.each do |redirect|
words << redirect.title #別名
end
elsif obj.class == Issue # チケットの場合はチケットsubjectを検索ワードにする
words << obj.subject
elsif obj.class == Journal && obj.journalized_type == 'Issue'
# チケットコメントの場合もチケット番号表記を検索ワードにする
words << '#'+obj.journalized_id.to_s
end
words
end
private
def get_column(name)
name_sym = name.to_sym
IssueQuery.available_columns.each do |col|
return name_sym if name_sym == col.name.to_sym
end
return :assigned_to if name_sym == :assigned
return :updated_on if name_sym == :updated
return :created_on if name_sym == :created
return name_sym if name =~ /\Acf_/
raise "- unknown column:#{name}"
end
# @todo Стремный патч, который сделан из-за отсутствия поминимания как работать с Query. По сути, надо патчить IssueQuery
def overwrite_sql_for_field(query)
def query.sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
if operator == '~'
# monkey patched for ref_issues: originally treat single value -> extend multiple value
if db_field=='subjectdescription'
sql = '('
value.each do |v|
sql << ' OR ' if sql != '('
sql << "LOWER(#{db_table}.subject) LIKE '%#{self.class.connection.quote_string(v.to_s.downcase)}%'"
sql << " OR LOWER(#{db_table}.description) LIKE '%#{self.class.connection.quote_string(v.to_s.downcase)}%'"
end
sql << ')'
return sql
else
sql = '('
value.each do |v|
sql << ' OR ' if sql != '('
sql << "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(v.to_s.downcase)}%'"
end
sql << ')'
return sql
end
elsif operator == '=='
sql = '('
value.each do |v|
sql << ' OR ' if sql != '('
sql << "LOWER(#{db_table}.#{db_field}) = '#{self.class.connection.quote_string(v.to_s.downcase)}'"
end
sql << ')'
return sql
elsif db_field == 'treated'
raise "- too many values for treated" if value.length > 2
raise "- too few values for treated" if value.length < 2
start_date = value[0]
end_date = value[1]
if operator =~ /^\d+$/
user = operator
else
user_obj = User.find_by_login(operator)
raise "- can not find user <#{operator}>" if user_obj.nil?
user = user_obj.id.to_s
end
sql = '('
sql << " (issues.author_id = #{user}"
sql << " AND (CAST(issues.created_on AS DATE) BETWEEN '#{start_date}' AND '#{end_date}'))"
sql << " OR ("
sql << " (select count(*) from journals where journalized_type = 'Issue' AND journalized_id = issues.id"
sql << " AND journals.user_id = #{user}"
sql << " AND (CAST(journals.created_on AS DATE) BETWEEN '#{start_date}' AND '#{end_date}')"
sql << " ) > 0"
sql << " )"
sql << ')'
return sql
end
return super(field, operator, value, db_table, db_field, is_custom_filter)
end
end
def words_to_word_array(obj, words)
words.split('|').collect do |word|
word.strip!
refer_field(obj, word)
end
end
def refer_field(obj, word)
if word =~ /\[current_user\]/
return User.current.login
end
if word =~ /\[current_user_id\]/
return User.current.id.to_s
end
if word =~ /\[current_project_id\]/
return @project.id.to_s
end
if word =~ /\[(.*)days_ago\]/
return (Date.today - $1.to_i).strftime("%Y-%m-%d")
end
if word =~ /\A\[(.*)\]\z/
raise "- can not use reference '#{word}' except for issues." if obj.class != Issue
atr = $1
if obj.attributes.has_key?(atr)
word = obj.attributes[atr]
else
obj.custom_field_values.each do |cf|
if 'cf_' + cf.custom_field.id.to_s == atr || cf.custom_field.name == atr
word = cf.value
end
end
end
end
return word.to_s
end
end
end
end

View file

@ -0,0 +1,187 @@
module RedmineWikiLists::WikiList
Redmine::WikiFormatting::Macros.register do
desc 'Displays a list of wiki pages with text elements (only inside wiki-pages).'
macro :wiki_list do |obj, args|
# 引数をパース
cond = ''
joins = ''
table_width = ''
column_keys = []
column_names = []
begin
raise '- no parameters' if args.count.zero?
args.each do |arg|
arg.strip!
if arg =~ /\A\-([^\=]*)(\=.*)?\z/ # オプション表記発見
case $1
when 'c' # リストアップの対象を子ページに限定する場合
cond << ' AND ' if cond != ''
cond << "parent_id = #{obj.page.id}"
when 'p' # リストアップの対象を特定の別プロジェクトのWikiに限定する場合
if arg=~/\A[^\=]+\=(.*)\z/ then # プロジェクト名を指定
name = $1.strip
prj = Project.find_by_name(name)
cond << ' AND ' if cond != ''
cond << "project_id = #{prj.id}"
else # プロジェクト名の指定が無い場合は当該WIKIのPJに限定
cond << ' AND ' if cond != ''
cond << "project_id = #{obj.project.id}"
end
joins << 'INNER JOIN wikis ON wiki_pages.wiki_id = wikis.id'
when 'w' # 表の横幅
if arg =~ /\A[^\=]+\=(.*)\z/ # 幅を取得
width = $1.strip
table_width = 'width="' + width + '"'
end
else
raise "- unknown option: #{arg}"
end
else # オプションでない場合はカラム指定
if arg =~ /\A(.*)\|(.*)\|(.*)\z/ # 抽出キーワードと別にカラム表示名と列幅の指定がある場合
column_keys.push($1.strip)
column_names.push($2.strip + '|' + $3.strip)
elsif arg =~ /\A(.*)\|(.*)\z/ # 抽出キーワードと別にカラム表示名の指定がある場合
column_keys.push($1.strip)
column_names.push($2.strip)
else # カラム表示名の指定が無い場合は抽出キーワードをカラム表示名にする
column_keys.push(arg.strip)
column_names.push(arg.strip)
end
end
end
rescue => err_msg
msg = <<-TEXT
- parameter error: #{err_msg}<br>
usage: {{wiki_list([option]*,[column]*)}}<br>
[option]<br>
-c : search child pages<br>
-p=[PROJECT NAME] : restrict search pages by project<br>
-w=[WIDTH] : table width<br>
[column]<br>
+title[| COLUMN_NAME] -> show page title<br>
+alias[| COLUMN_NAME] -> show page aliases<br>
KEYWORD[| COLUMN_NAME] -> scan KEYWORD and show following words to EOL<br>
KEYWORD\\TERMINATOR[| COLUMN_NAME] -> scan KEYWORD and show following words to TERMINATOR
TEXT
raise msg.html_safe
end
if column_keys.count.zero?
column_names.push 'title'
column_keys.push '+title'
end
disp = "<table #{table_width}><tr>"
# カラム名(最初の行)を作成
column_names.each do |column_name|
if column_name =~ /\A(.*)\|(.*)\z/ then
disp << '<th width="'+$2+'">' + $1 + '</th>'
else
disp << "<th>#{column_name}</th>"
end
end
disp << '</tr>'
# Wikiページの抽出
wiki_pages = WikiPage.joins(joins).where(cond)
wiki_pages.each do |wiki_page| #---------------- Wikiページ毎の処理
next unless wiki_page.visible?
# 1ページに抽出キーワードが複数あった場合に複数行表示するため一旦表示行を配列に記憶する
lines_by_page = [[]] # 最初は1ページ1行からスタート
column_num = 0
column_keys.each do |column_key| #---------------- カラム毎の処理
case column_key
when '+title' # Wikiページ名
html =
link_to(wiki_page.title,
controller: 'wiki', action: 'show',
project_id: wiki_page.project, id: wiki_page.title)
RedmineWikiLists::WikiList.set_lines(lines_by_page, column_num, html)
when '+alias' # Wikiページの別名
redirects = WikiRedirect.where(wiki_id: wiki_page.wiki_id, redirects_to: wiki_page.title)
html = ''
redirects.each do |redirect|
html << '<br>' if html.present?
html << redirect.title
end
RedmineWikiLists::WikiList.set_lines(lines_by_page, column_num, html)
when '+project' # Wikiページのプロジェクト名
RedmineWikiLists::WikiList.set_lines(lines_by_page, column_num, wiki_page.project.to_s)
else # それ以外はWikiページの中からキーワードで表示要素を抽出する
new_lines = [] # カラムキーワードが抽出される毎にこの変数に表示行を追加する
if column_key =~ /\A(.*)\\(.*)\z/
keyword = Regexp.escape($1.strip)
terminator = Regexp.escape($2.strip)
matches = wiki_page.text.scan /#{keyword}[\s\S]*?#{terminator}/ # キーワードから終端文字列までを抽出
else
keyword = Regexp.escape(column_key)
terminator = false
matches = wiki_page.text.scan /#{keyword}.*\z/ # キーワードから行末までを抽出
end
matches.each do |match| # 抽出されたキーワード毎の処理
# キーワードの後ろの文字列を抽出
match =~
if terminator
/\A#{keyword}([\s\S]*)#{terminator}/
else
/\A#{keyword}(.*)\z/
end
if $1
html = textilizable($1.strip) # 前後の空白を覗いてWiki表記解釈
# Wikiページ内のこれまでのカラム処理で生成されたlinesに表示内容を記入
RedmineWikiLists::WikiList.set_lines(lines_by_page, column_num, html)
lines_by_page.each do |line|
new_lines.push(line.dup) # 本カラムによって生成される行にコピーを追加
end
end
end
if new_lines.length.zero? # キーワードが1つも抽出されていなかったら空文字を入れておく
RedmineWikiLists::WikiList.set_lines(lines_by_page, column_num, '')
else # 抽出があった場合は本カラムで作られた新しい表示行をページ表示行にする
lines_by_page=new_lines
end
end # case columnKey
column_num += 1
end # カラム毎の処理
# 配列に記憶されたページ内の表示内容をHTMLに吐き出す
lines_by_page.each do |line|
disp << '<tr>'
line.each do |column|
disp << "<td>#{column}</td>"
end
disp << '</tr>'
end
end # Wikiページ毎の処理
disp << '</table>'
disp.html_safe
end
end
# 配列の全ての要素配列のcolumn_num番目にstrを書きこむ
def set_lines(lines, column_num, str)
lines.each do |line|
line[column_num] = str
end
end
module_function :set_lines
end