Redmine 4.1.1
This commit is contained in:
parent
33e7b881a5
commit
3d976f1b3b
1593 changed files with 36180 additions and 19489 deletions
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -54,7 +56,9 @@ module Redmine
|
|||
end
|
||||
|
||||
def position_scope_was
|
||||
build_position_scope {|c| send("#{c}_was")}
|
||||
# this can be called in after_update or after_destroy callbacks
|
||||
# with different methods in Rails 5 for retrieving the previous value
|
||||
build_position_scope {|c| send(destroyed? ? "#{c}_was" : "#{c}_before_last_save")}
|
||||
end
|
||||
|
||||
def build_position_scope
|
||||
|
@ -75,8 +79,8 @@ module Redmine
|
|||
if !new_record? && position_scope_changed?
|
||||
remove_position
|
||||
insert_position
|
||||
elsif position_changed?
|
||||
if position_was.nil?
|
||||
elsif saved_change_to_position?
|
||||
if position_before_last_save.nil?
|
||||
insert_position
|
||||
else
|
||||
shift_positions
|
||||
|
@ -89,16 +93,19 @@ module Redmine
|
|||
end
|
||||
|
||||
def remove_position
|
||||
position_scope_was.where("position >= ? AND id <> ?", position_was, id).update_all("position = position - 1")
|
||||
# this can be called in after_update or after_destroy callbacks
|
||||
# with different methods in Rails 5 for retrieving the previous value
|
||||
previous = destroyed? ? position_was : position_before_last_save
|
||||
position_scope_was.where("position >= ? AND id <> ?", previous, id).update_all("position = position - 1")
|
||||
end
|
||||
|
||||
def position_scope_changed?
|
||||
(changed & self.class.positioned_options[:scope].map(&:to_s)).any?
|
||||
(saved_changes.keys & self.class.positioned_options[:scope].map(&:to_s)).any?
|
||||
end
|
||||
|
||||
def shift_positions
|
||||
offset = position_was <=> position
|
||||
min, max = [position, position_was].sort
|
||||
offset = position_before_last_save <=> position
|
||||
min, max = [position, position_before_last_save].sort
|
||||
r = position_scope.where("id <> ? AND position BETWEEN ? AND ?", id, min, max).update_all("position = position + #{offset}")
|
||||
if r != max - min
|
||||
reset_positions_in_list
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Redmine
|
||||
module CodesetUtil
|
||||
|
||||
def self.replace_invalid_utf8(str)
|
||||
return str if str.nil?
|
||||
return nil if str.nil?
|
||||
str = str.dup
|
||||
str.force_encoding('UTF-8')
|
||||
if ! str.valid_encoding?
|
||||
str = str.encode("UTF-16LE", :invalid => :replace,
|
||||
|
@ -13,14 +15,14 @@ module Redmine
|
|||
end
|
||||
|
||||
def self.to_utf8(str, encoding)
|
||||
return str if str.nil?
|
||||
str.force_encoding("ASCII-8BIT")
|
||||
return if str.nil?
|
||||
str = str.b
|
||||
if str.empty?
|
||||
str.force_encoding("UTF-8")
|
||||
return str
|
||||
end
|
||||
enc = encoding.blank? ? "UTF-8" : encoding
|
||||
if enc.upcase != "UTF-8"
|
||||
if enc.casecmp("UTF-8") != 0
|
||||
str.force_encoding(enc)
|
||||
str = str.encode("UTF-8", :invalid => :replace,
|
||||
:undef => :replace, :replace => '?')
|
||||
|
@ -31,15 +33,16 @@ module Redmine
|
|||
end
|
||||
|
||||
def self.to_utf8_by_setting(str)
|
||||
return str if str.nil?
|
||||
return if str.nil?
|
||||
str = str.dup
|
||||
self.to_utf8_by_setting_internal(str).force_encoding('UTF-8')
|
||||
end
|
||||
|
||||
def self.to_utf8_by_setting_internal(str)
|
||||
return str if str.nil?
|
||||
str.force_encoding('ASCII-8BIT')
|
||||
return if str.nil?
|
||||
str = str.b
|
||||
return str if str.empty?
|
||||
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
|
||||
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match?(str) # for us-ascii
|
||||
str.force_encoding('UTF-8')
|
||||
encodings = Setting.repositories_encodings.split(',').collect(&:strip)
|
||||
encodings.each do |encoding|
|
||||
|
@ -55,9 +58,11 @@ module Redmine
|
|||
end
|
||||
|
||||
def self.from_utf8(str, encoding)
|
||||
return if str.nil?
|
||||
str = str.dup
|
||||
str ||= ''
|
||||
str.force_encoding('UTF-8')
|
||||
if encoding.upcase != 'UTF-8'
|
||||
if encoding.casecmp('UTF-8') != 0
|
||||
str = str.encode(encoding, :invalid => :replace,
|
||||
:undef => :replace, :replace => '?')
|
||||
else
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -20,6 +22,7 @@ module Redmine
|
|||
|
||||
# Configuration default values
|
||||
@defaults = {
|
||||
'avatar_server_url' => 'https://www.gravatar.com',
|
||||
'email_delivery' => nil,
|
||||
'max_concurrent_ajax_uploads' => 2
|
||||
}
|
||||
|
@ -53,6 +56,12 @@ module Redmine
|
|||
if @config['email_delivery']
|
||||
ActionMailer::Base.perform_deliveries = true
|
||||
@config['email_delivery'].each do |k, v|
|
||||
# Comprehensive error message for those who used async_smtp and async_sendmail
|
||||
# delivery methods that are removed in Redmine 4.0.
|
||||
if k == 'delivery_method' && v.to_s =~ /\Aasync_(.+)/
|
||||
abort "Redmine now uses ActiveJob to send emails asynchronously and the :#{v} delivery method is no longer available.\n" +
|
||||
"Please update your config/configuration.yml to use :#$1 delivery method instead."
|
||||
end
|
||||
v.symbolize_keys! if v.respond_to?(:symbolize_keys!)
|
||||
ActionMailer::Base.send("#{k}=", v)
|
||||
end
|
||||
|
@ -114,7 +123,7 @@ module Redmine
|
|||
# Checks the validness of regular expressions set for repository paths at startup
|
||||
def check_regular_expressions
|
||||
@config.each do |name, value|
|
||||
if value.present? && name =~ /^scm_.+_path_regexp$/
|
||||
if value.present? && /^scm_.+_path_regexp$/.match?(name)
|
||||
begin
|
||||
Regexp.new value.to_s.strip
|
||||
rescue => e
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -19,7 +21,7 @@ class DateValidator < ActiveModel::EachValidator
|
|||
def validate_each(record, attribute, value)
|
||||
before_type_cast = record.attributes_before_type_cast[attribute.to_s]
|
||||
if before_type_cast.is_a?(String) && before_type_cast.present?
|
||||
unless before_type_cast =~ /\A\d{4}-\d{2}-\d{2}( 00:00:00)?\z/ && value
|
||||
unless /\A\d{4}-\d{2}-\d{2}( 00:00:00)?\z/.match?(before_type_cast) && value
|
||||
record.errors.add attribute, :not_a_date
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
require File.dirname(__FILE__) + '/date/calculations'
|
||||
|
||||
class Date #:nodoc:
|
||||
include Redmine::CoreExtensions::Date::Calculations
|
||||
end
|
|
@ -1,35 +0,0 @@
|
|||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Date #:nodoc:
|
||||
# Custom date calculations
|
||||
module Calculations
|
||||
# Returns difference with specified date in months
|
||||
def months_ago(date = self.class.today)
|
||||
(date.year - self.year)*12 + (date.month - self.month)
|
||||
end
|
||||
|
||||
# Returns difference with specified date in weeks
|
||||
def weeks_ago(date = self.class.today)
|
||||
(date.year - self.year)*52 + (date.cweek - self.cweek)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require File.dirname(__FILE__) + '/string/conversions'
|
||||
require File.dirname(__FILE__) + '/string/inflections'
|
||||
|
||||
class String #:nodoc:
|
||||
# @private
|
||||
class String
|
||||
include Redmine::CoreExtensions::String::Conversions
|
||||
include Redmine::CoreExtensions::String::Inflections
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -15,10 +17,13 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module String #:nodoc:
|
||||
module Redmine
|
||||
# @private
|
||||
module CoreExtensions
|
||||
# @private
|
||||
module String
|
||||
# Custom string conversions
|
||||
# @private
|
||||
module Conversions
|
||||
# Parses hours format and returns a float
|
||||
def to_hours
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -15,10 +17,13 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module String #:nodoc:
|
||||
module Redmine
|
||||
# @private
|
||||
module CoreExtensions
|
||||
# @private
|
||||
module String
|
||||
# Custom string inflections
|
||||
# @private
|
||||
module Inflections
|
||||
def with_leading_slash
|
||||
starts_with?('/') ? self : "/#{ self }"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -21,7 +23,7 @@ module Redmine
|
|||
class << self
|
||||
# Returns true if the database is PostgreSQL
|
||||
def postgresql?
|
||||
(ActiveRecord::Base.connection.adapter_name =~ /postgresql/i).present?
|
||||
/postgresql/i.match?(ActiveRecord::Base.connection.adapter_name)
|
||||
end
|
||||
|
||||
# Returns the PostgreSQL version or nil if another DBMS is used
|
||||
|
@ -46,7 +48,7 @@ module Redmine
|
|||
|
||||
# Returns true if the database is MySQL
|
||||
def mysql?
|
||||
(ActiveRecord::Base.connection.adapter_name =~ /mysql/i).present?
|
||||
/mysql/i.match?(ActiveRecord::Base.connection.adapter_name)
|
||||
end
|
||||
|
||||
# Returns a SQL statement for case/accent (if possible) insensitive match
|
||||
|
@ -64,6 +66,27 @@ module Redmine
|
|||
end
|
||||
end
|
||||
|
||||
# Returns a SQL statement to cast a timestamp column to a date given a time zone
|
||||
# Returns nil if not implemented for the current database
|
||||
def timestamp_to_date(column, time_zone)
|
||||
if postgresql?
|
||||
if time_zone
|
||||
identifier = ActiveSupport::TimeZone.find_tzinfo(time_zone.name).identifier
|
||||
"(#{column}::timestamptz AT TIME ZONE '#{identifier}')::date"
|
||||
else
|
||||
"#{column}::date"
|
||||
end
|
||||
elsif mysql?
|
||||
if time_zone
|
||||
user_identifier = ActiveSupport::TimeZone.find_tzinfo(time_zone.name).identifier
|
||||
local_identifier = ActiveSupport::TimeZone.find_tzinfo(Time.zone.name).identifier
|
||||
"DATE(CONVERT_TZ(#{column},'#{local_identifier}', '#{user_identifier}'))"
|
||||
else
|
||||
"DATE(#{column})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Resets database information
|
||||
def reset
|
||||
@postgresql_unaccent = nil
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -17,7 +19,7 @@
|
|||
|
||||
module Redmine
|
||||
module DefaultData
|
||||
class DataAlreadyLoaded < Exception; end
|
||||
class DataAlreadyLoaded < StandardError; end
|
||||
|
||||
module Loader
|
||||
include Redmine::I18n
|
||||
|
@ -48,9 +50,10 @@ module Redmine
|
|||
manager.permissions = manager.setable_permissions.collect {|p| p.name}
|
||||
manager.save!
|
||||
|
||||
developer = Role.create! :name => l(:default_role_developer),
|
||||
:position => 2,
|
||||
:permissions => [:manage_versions,
|
||||
developer = Role.create!(
|
||||
:name => l(:default_role_developer),
|
||||
:position => 2,
|
||||
:permissions => [:manage_versions,
|
||||
:manage_categories,
|
||||
:view_issues,
|
||||
:add_issues,
|
||||
|
@ -80,11 +83,11 @@ module Redmine
|
|||
:browse_repository,
|
||||
:view_changesets,
|
||||
:commit_access,
|
||||
:manage_related_issues]
|
||||
|
||||
reporter = Role.create! :name => l(:default_role_reporter),
|
||||
:position => 3,
|
||||
:permissions => [:view_issues,
|
||||
:manage_related_issues])
|
||||
reporter = Role.create!(
|
||||
:name => l(:default_role_reporter),
|
||||
:position => 3,
|
||||
:permissions => [:view_issues,
|
||||
:add_issues,
|
||||
:add_issue_notes,
|
||||
:save_queries,
|
||||
|
@ -102,7 +105,7 @@ module Redmine
|
|||
:edit_own_messages,
|
||||
:view_files,
|
||||
:browse_repository,
|
||||
:view_changesets]
|
||||
:view_changesets])
|
||||
|
||||
Role.non_member.update_attribute :permissions, [:view_issues,
|
||||
:add_issues,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# encoding: utf-8
|
||||
#
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -31,15 +31,16 @@ module Redmine
|
|||
|
||||
class << self
|
||||
|
||||
def generate(&block)
|
||||
def generate(options = {}, &block)
|
||||
col_sep = l(:general_csv_separator)
|
||||
encoding = l(:general_csv_encoding)
|
||||
encoding = Encoding.find(options[:encoding]) rescue Encoding.find(l(:general_csv_encoding))
|
||||
|
||||
str = ''.force_encoding(encoding)
|
||||
if encoding == 'UTF-8'
|
||||
# BOM
|
||||
str = "\xEF\xBB\xBF".force_encoding(encoding)
|
||||
end
|
||||
str =
|
||||
if encoding == Encoding::UTF_8
|
||||
+"\xEF\xBB\xBF" # BOM
|
||||
else
|
||||
(+'').force_encoding(encoding)
|
||||
end
|
||||
|
||||
super(str, :col_sep => col_sep, :encoding => encoding, &block)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -51,6 +52,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def SetFont(family, style='', size=0, fontfile='')
|
||||
style = +style
|
||||
# FreeSerif Bold Thai font has problem.
|
||||
style.delete!('B') if family.to_s.casecmp('freeserif') == 0
|
||||
# DejaVuSans Italic Arabic and Persian font has problem.
|
||||
|
@ -69,7 +71,7 @@ module Redmine
|
|||
Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
|
||||
end
|
||||
|
||||
def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
|
||||
def RDMCell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link='')
|
||||
cell(w, h, txt, border, ln, align, fill, link)
|
||||
end
|
||||
|
||||
|
@ -91,7 +93,7 @@ module Redmine
|
|||
</style>'
|
||||
|
||||
# Strip {{toc}} tags
|
||||
txt.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '')
|
||||
txt = txt.gsub(/<p>\{\{((<|<)|(>|>))?toc\}\}<\/p>/i, '')
|
||||
writeHTMLCell(w, h, x, y, css_tag + txt, border, ln, fill)
|
||||
end
|
||||
|
||||
|
@ -133,15 +135,13 @@ module Redmine
|
|||
class RDMPdfEncoding
|
||||
def self.rdm_from_utf8(txt, encoding)
|
||||
txt ||= ''
|
||||
txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
|
||||
txt.force_encoding('ASCII-8BIT')
|
||||
txt
|
||||
Redmine::CodesetUtil.from_utf8(txt, encoding).b
|
||||
end
|
||||
|
||||
def self.attach(attachments, filename, encoding)
|
||||
filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
|
||||
atta = nil
|
||||
if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
|
||||
if /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i.match?(filename_utf8)
|
||||
atta = Attachment.latest_attach(attachments, filename_utf8)
|
||||
end
|
||||
if atta && atta.readable? && atta.visible?
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# encoding: utf-8
|
||||
#
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -45,35 +45,31 @@ module Redmine
|
|||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
|
||||
pdf.ln
|
||||
|
||||
|
||||
left = []
|
||||
left << [l(:field_status), issue.status]
|
||||
left << [l(:field_priority), issue.priority]
|
||||
left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
|
||||
left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
|
||||
left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
|
||||
|
||||
|
||||
right = []
|
||||
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
|
||||
right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
|
||||
right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
|
||||
right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
|
||||
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
|
||||
|
||||
|
||||
rows = left.size > right.size ? left.size : right.size
|
||||
while left.size < rows
|
||||
left << nil
|
||||
end
|
||||
while right.size < rows
|
||||
right << nil
|
||||
end
|
||||
left << nil while left.size < rows
|
||||
right << nil while right.size < rows
|
||||
|
||||
custom_field_values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?}
|
||||
half = (custom_field_values.size / 2.0).ceil
|
||||
custom_field_values.each_with_index do |custom_value, i|
|
||||
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
|
||||
end
|
||||
|
||||
|
||||
if pdf.get_rtl
|
||||
border_first_top = 'RT'
|
||||
border_last_top = 'LT'
|
||||
|
@ -85,7 +81,7 @@ module Redmine
|
|||
border_first = 'L'
|
||||
border_last = 'R'
|
||||
end
|
||||
|
||||
|
||||
rows = left.size > right.size ? left.size : right.size
|
||||
rows.times do |i|
|
||||
heights = []
|
||||
|
@ -100,34 +96,36 @@ module Redmine
|
|||
item = right[i]
|
||||
heights << pdf.get_string_height(60, item ? item.last.to_s : "")
|
||||
height = heights.max
|
||||
|
||||
|
||||
item = left[i]
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
|
||||
pdf.SetFontStyle('',9)
|
||||
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
|
||||
|
||||
|
||||
item = right[i]
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
|
||||
pdf.SetFontStyle('',9)
|
||||
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
|
||||
|
||||
|
||||
pdf.set_x(base_x)
|
||||
end
|
||||
|
||||
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
|
||||
pdf.SetFontStyle('',9)
|
||||
|
||||
|
||||
# Set resize image scale
|
||||
pdf.set_image_scale(1.6)
|
||||
text = textilizable(issue, :description,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
text =
|
||||
textilizable(
|
||||
issue, :description,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
|
||||
|
||||
custom_field_values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?}
|
||||
|
@ -157,7 +155,7 @@ module Redmine
|
|||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
relations = issue.relations.select { |r| r.other_issue(issue).visible? }
|
||||
unless relations.empty?
|
||||
truncate_length = (!is_cjk? ? 80 : 60)
|
||||
|
@ -185,7 +183,7 @@ module Redmine
|
|||
end
|
||||
pdf.RDMCell(190,5, "", "T")
|
||||
pdf.ln
|
||||
|
||||
|
||||
if issue.changesets.any? &&
|
||||
User.current.allowed_to?(:view_changesets, issue.project)
|
||||
pdf.SetFontStyle('B',9)
|
||||
|
@ -199,13 +197,14 @@ module Redmine
|
|||
pdf.ln
|
||||
unless changeset.comments.blank?
|
||||
pdf.SetFontStyle('',8)
|
||||
pdf.RDMwriteHTMLCell(190,5,'','',
|
||||
pdf.RDMwriteHTMLCell(
|
||||
190,5,'','',
|
||||
changeset.comments.to_s, issue.attachments, "")
|
||||
end
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if assoc[:journals].present?
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_history), "B")
|
||||
|
@ -213,7 +212,7 @@ module Redmine
|
|||
assoc[:journals].each do |journal|
|
||||
pdf.SetFontStyle('B',8)
|
||||
title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
|
||||
title << " (#{l(:field_private_notes)})" if journal.private_notes?
|
||||
title += " (#{l(:field_private_notes)})" if journal.private_notes?
|
||||
pdf.RDMCell(190,5, title)
|
||||
pdf.ln
|
||||
pdf.SetFontStyle('I',8)
|
||||
|
@ -223,18 +222,20 @@ module Redmine
|
|||
if journal.notes?
|
||||
pdf.ln unless journal.details.empty?
|
||||
pdf.SetFontStyle('',8)
|
||||
text = textilizable(journal, :notes,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
text =
|
||||
textilizable(
|
||||
journal, :notes,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "")
|
||||
end
|
||||
pdf.ln
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if issue.attachments.any?
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
|
||||
|
@ -261,7 +262,7 @@ module Redmine
|
|||
pdf.footer_date = format_date(User.current.today)
|
||||
pdf.set_auto_page_break(false)
|
||||
pdf.add_page("L")
|
||||
|
||||
|
||||
# Landscape A4 = 210 x 297 mm
|
||||
page_height = pdf.get_page_height # 210
|
||||
page_width = pdf.get_page_width # 297
|
||||
|
@ -269,7 +270,7 @@ module Redmine
|
|||
right_margin = pdf.get_original_margins['right'] # 10
|
||||
bottom_margin = pdf.get_footer_margin
|
||||
row_height = 4
|
||||
|
||||
|
||||
# column widths
|
||||
table_width = page_width - right_margin - left_margin
|
||||
col_width = []
|
||||
|
@ -277,13 +278,13 @@ module Redmine
|
|||
col_width = calc_col_width(issues, query, table_width, pdf)
|
||||
table_width = col_width.inject(0, :+)
|
||||
end
|
||||
|
||||
# use full width if the description or last_notes are displayed
|
||||
if table_width > 0 && (query.has_column?(:description) || query.has_column?(:last_notes))
|
||||
|
||||
# use full width if the query has block columns (description, last_notes or full width custom fieds)
|
||||
if table_width > 0 && query.block_columns.any?
|
||||
col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
|
||||
table_width = col_width.inject(0, :+)
|
||||
end
|
||||
|
||||
|
||||
# title
|
||||
pdf.SetFontStyle('B',11)
|
||||
pdf.RDMCell(190, 8, title)
|
||||
|
@ -303,9 +304,9 @@ module Redmine
|
|||
|
||||
issue_list(issues) do |issue, level|
|
||||
if query.grouped? &&
|
||||
(group = query.group_by_column.value(issue)) != previous_group
|
||||
(group = query.group_by_column.group_value(issue)) != previous_group
|
||||
pdf.SetFontStyle('B',10)
|
||||
group_label = group.blank? ? 'None' : group.to_s.dup
|
||||
group_label = group.blank? ? +'None' : group.to_s.dup
|
||||
group_label << " (#{result_count_by_group[group]})"
|
||||
pdf.bookmark group_label, 0, -1
|
||||
pdf.RDMCell(table_width, row_height * 2, group_label, 'LR', 1, 'L')
|
||||
|
@ -317,10 +318,10 @@ module Redmine
|
|||
end
|
||||
previous_group = group
|
||||
end
|
||||
|
||||
|
||||
# fetch row values
|
||||
col_values = fetch_row_values(issue, query, level)
|
||||
|
||||
|
||||
# make new page if it doesn't fit on the current one
|
||||
base_y = pdf.get_y
|
||||
max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width)
|
||||
|
@ -330,26 +331,30 @@ module Redmine
|
|||
render_table_header(pdf, query, col_width, row_height, table_width)
|
||||
base_y = pdf.get_y
|
||||
end
|
||||
|
||||
|
||||
# write the cells on page
|
||||
issues_to_pdf_write_cells(pdf, col_values, col_width, max_height)
|
||||
pdf.set_y(base_y + max_height)
|
||||
|
||||
if query.has_column?(:description) && issue.description?
|
||||
pdf.set_x(10)
|
||||
pdf.set_auto_page_break(true, bottom_margin)
|
||||
pdf.RDMwriteHTMLCell(0, 5, 10, '', issue.description.to_s, issue.attachments, "LRBT")
|
||||
pdf.set_auto_page_break(false)
|
||||
end
|
||||
|
||||
if query.has_column?(:last_notes) && issue.last_notes.present?
|
||||
pdf.set_x(10)
|
||||
pdf.set_auto_page_break(true, bottom_margin)
|
||||
pdf.RDMwriteHTMLCell(0, 5, 10, '', issue.last_notes.to_s, [], "LRBT")
|
||||
pdf.set_auto_page_break(false)
|
||||
query.block_columns.each do |column|
|
||||
if column.is_a?(QueryCustomFieldColumn)
|
||||
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
|
||||
text = show_value(cv, false)
|
||||
else
|
||||
text = issue.send(column.name)
|
||||
end
|
||||
next if text.blank?
|
||||
|
||||
pdf.set_x(10)
|
||||
pdf.set_auto_page_break(true, bottom_margin)
|
||||
pdf.SetFontStyle('B',9)
|
||||
pdf.RDMCell(0, 5, column.caption, "LRT", 1)
|
||||
pdf.SetFontStyle('',9)
|
||||
pdf.RDMwriteHTMLCell(0, 5, '', '', text, [], "LRB")
|
||||
pdf.set_auto_page_break(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if issues.size == Setting.issues_export_limit.to_i
|
||||
pdf.SetFontStyle('B',10)
|
||||
pdf.RDMCell(0, row_height, '...')
|
||||
|
@ -369,25 +374,28 @@ module Redmine
|
|||
# fetch row values
|
||||
def fetch_row_values(issue, query, level)
|
||||
query.inline_columns.collect do |column|
|
||||
s = if column.is_a?(QueryCustomFieldColumn)
|
||||
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
|
||||
show_value(cv, false)
|
||||
else
|
||||
value = issue.send(column.name)
|
||||
case column.name
|
||||
when :subject
|
||||
value = " " * level + value
|
||||
when :attachments
|
||||
value = value.to_a.map {|a| a.filename}.join("\n")
|
||||
end
|
||||
if value.is_a?(Date)
|
||||
format_date(value)
|
||||
elsif value.is_a?(Time)
|
||||
format_time(value)
|
||||
s =
|
||||
if column.is_a?(QueryCustomFieldColumn)
|
||||
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
|
||||
show_value(cv, false)
|
||||
else
|
||||
value
|
||||
value = column.value_object(issue)
|
||||
case column.name
|
||||
when :subject
|
||||
value = " " * level + value
|
||||
when :attachments
|
||||
value = value.to_a.map {|a| a.filename}.join("\n")
|
||||
end
|
||||
if value.is_a?(Date)
|
||||
format_date(value)
|
||||
elsif value.is_a?(Time)
|
||||
format_time(value)
|
||||
elsif value.is_a?(Float)
|
||||
sprintf "%.2f", value
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
s.to_s
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# encoding: utf-8
|
||||
#
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -46,8 +46,9 @@ module Redmine
|
|||
pdf.footer_date = format_date(User.current.today)
|
||||
pdf.add_page
|
||||
pdf.SetFontStyle('B',11)
|
||||
pdf.RDMMultiCell(190,5,
|
||||
"#{project} - #{page.title} - # #{page.content.version}")
|
||||
pdf.RDMMultiCell(
|
||||
190,5,
|
||||
"#{project} - #{page.title} - # #{page.content.version}")
|
||||
pdf.ln
|
||||
# Set resize image scale
|
||||
pdf.set_image_scale(1.6)
|
||||
|
@ -70,12 +71,14 @@ module Redmine
|
|||
end
|
||||
|
||||
def write_wiki_page(pdf, page)
|
||||
text = textilizable(page.content, :text,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
text =
|
||||
textilizable(
|
||||
page.content, :text,
|
||||
:only_path => false,
|
||||
:edit_section_links => false,
|
||||
:headings => false,
|
||||
:inline_attachments => false
|
||||
)
|
||||
pdf.RDMwriteFormattedCell(190,5,'','', text, page.attachments, 0)
|
||||
if page.attachments.any?
|
||||
pdf.ln(5)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -155,7 +157,7 @@ module Redmine
|
|||
def target_class
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
def possible_custom_value_options(custom_value)
|
||||
possible_values_options(custom_value.custom_field, custom_value.customized)
|
||||
end
|
||||
|
@ -183,7 +185,7 @@ module Redmine
|
|||
|
||||
def parse_keyword(custom_field, keyword, &block)
|
||||
separator = Regexp.escape ","
|
||||
keyword = keyword.to_s
|
||||
keyword = keyword.dup.to_s
|
||||
|
||||
if custom_field.multiple?
|
||||
values = []
|
||||
|
@ -248,7 +250,10 @@ module Redmine
|
|||
url = url_from_pattern(custom_field, single_value, customized)
|
||||
[text, url]
|
||||
end
|
||||
links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
|
||||
links = texts_and_urls.sort_by(&:first).map do |text, url|
|
||||
css_class = (/^https?:\/\//.match?(url)) ? 'external' : nil
|
||||
view.link_to_if uri_with_safe_scheme?(url), text, url, :class => css_class
|
||||
end
|
||||
links.join(', ').html_safe
|
||||
else
|
||||
casted
|
||||
|
@ -300,8 +305,12 @@ module Redmine
|
|||
if custom_field.is_required?
|
||||
''.html_safe
|
||||
else
|
||||
view.content_tag('label',
|
||||
view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
|
||||
view.content_tag(
|
||||
'label',
|
||||
view.check_box_tag(
|
||||
tag_name,
|
||||
'__none__', (value == '__none__'), :id => nil,
|
||||
:data => {:disables => "##{tag_id}"}) + l(:button_clear),
|
||||
:class => 'inline'
|
||||
)
|
||||
end
|
||||
|
@ -320,7 +329,7 @@ module Redmine
|
|||
# Returns nil if the custom field can not be used for sorting.
|
||||
def order_statement(custom_field)
|
||||
# COALESCE is here to make sure that blank and NULL values are sorted equally
|
||||
"COALESCE(#{join_alias custom_field}.value, '')"
|
||||
Arel.sql "COALESCE(#{join_alias custom_field}.value, '')"
|
||||
end
|
||||
|
||||
# Returns a GROUP BY clause that can used to group by custom value
|
||||
|
@ -355,7 +364,7 @@ module Redmine
|
|||
def validate_single_value(custom_field, value, customized=nil)
|
||||
errs = super
|
||||
value = value.to_s
|
||||
unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
|
||||
unless custom_field.regexp.blank? or Regexp.new(custom_field.regexp).match?(value)
|
||||
errs << ::I18n.t('activerecord.errors.messages.invalid')
|
||||
end
|
||||
if custom_field.min_length && value.length < custom_field.min_length
|
||||
|
@ -437,12 +446,13 @@ module Redmine
|
|||
url = url_from_pattern(custom_field, value, customized)
|
||||
else
|
||||
url = value.to_s
|
||||
unless url =~ %r{\A[a-z]+://}i
|
||||
unless %r{\A[a-z]+://}i.match?(url)
|
||||
# no protocol found, use http by default
|
||||
url = "http://" + url
|
||||
end
|
||||
end
|
||||
view.link_to value.to_s.truncate(40), url
|
||||
css_class = (/^https?:\/\//.match?(url)) ? 'external' : nil
|
||||
view.link_to value.to_s.truncate(40), url, :class => css_class
|
||||
else
|
||||
value.to_s
|
||||
end
|
||||
|
@ -457,7 +467,7 @@ module Redmine
|
|||
# Make the database cast values into numeric
|
||||
# Postgresql will raise an error if a value can not be casted!
|
||||
# CustomValue validations should ensure that it doesn't occur
|
||||
"CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
|
||||
Arel.sql "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
|
||||
end
|
||||
|
||||
# Returns totals for the given scope
|
||||
|
@ -486,7 +496,7 @@ module Redmine
|
|||
|
||||
def validate_single_value(custom_field, value, customized=nil)
|
||||
errs = super
|
||||
errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s.strip =~ /^[+-]?\d+$/
|
||||
errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless /^[+-]?\d+$/.match?(value.to_s.strip)
|
||||
errs
|
||||
end
|
||||
|
||||
|
@ -530,7 +540,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def validate_single_value(custom_field, value, customized=nil)
|
||||
if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
|
||||
if /^\d{4}-\d{2}-\d{2}$/.match?(value) && (value.to_date rescue false)
|
||||
[]
|
||||
else
|
||||
[::I18n.t('activerecord.errors.messages.not_a_date')]
|
||||
|
@ -621,7 +631,7 @@ module Redmine
|
|||
value ||= label
|
||||
checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
|
||||
tag = view.send(tag_method, tag_name, value, checked, :id => nil)
|
||||
s << view.content_tag('label', tag + ' ' + label)
|
||||
s << view.content_tag('label', tag + ' ' + label)
|
||||
end
|
||||
if custom_value.custom_field.multiple?
|
||||
s << view.hidden_field_tag(tag_name, '', :id => nil)
|
||||
|
@ -726,7 +736,7 @@ module Redmine
|
|||
def reset_target_class
|
||||
@target_class = nil
|
||||
end
|
||||
|
||||
|
||||
def possible_custom_value_options(custom_value)
|
||||
options = possible_values_options(custom_value.custom_field, custom_value.customized)
|
||||
missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
|
||||
|
@ -753,7 +763,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def group_statement(custom_field)
|
||||
"COALESCE(#{join_alias custom_field}.value, '')"
|
||||
Arel.sql "COALESCE(#{join_alias custom_field}.value, '')"
|
||||
end
|
||||
|
||||
def join_for_order_statement(custom_field)
|
||||
|
@ -782,7 +792,7 @@ module Redmine
|
|||
class EnumerationFormat < RecordList
|
||||
add 'enumeration'
|
||||
self.form_partial = 'custom_fields/formats/enumeration'
|
||||
|
||||
|
||||
def label
|
||||
"label_field_format_enumeration"
|
||||
end
|
||||
|
@ -812,7 +822,10 @@ module Redmine
|
|||
field_attributes :user_role
|
||||
|
||||
def possible_values_options(custom_field, object=nil)
|
||||
possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
|
||||
users = possible_values_records(custom_field, object)
|
||||
options = users.map {|u| [u.name, u.id.to_s]}
|
||||
options = [["<< #{l(:label_me)} >>", User.current.id]] + options if users.include?(User.current)
|
||||
options
|
||||
end
|
||||
|
||||
def possible_values_records(custom_field, object=nil)
|
||||
|
@ -970,7 +983,7 @@ module Redmine
|
|||
end
|
||||
else
|
||||
if custom_value.value.present?
|
||||
attachment = Attachment.where(:id => custom_value.value.to_s).first
|
||||
attachment = Attachment.find_by(:id => custom_value.value.to_s)
|
||||
extensions = custom_value.custom_field.extensions_allowed
|
||||
if attachment && extensions.present? && !attachment.extension_in?(extensions)
|
||||
errors << "#{::I18n.t('activerecord.errors.messages.invalid')} (#{l(:setting_attachment_extensions_allowed)}: #{extensions})"
|
||||
|
@ -982,16 +995,16 @@ module Redmine
|
|||
end
|
||||
|
||||
def after_save_custom_value(custom_field, custom_value)
|
||||
if custom_value.value_changed?
|
||||
if custom_value.saved_change_to_value?
|
||||
if custom_value.value.present?
|
||||
attachment = Attachment.where(:id => custom_value.value.to_s).first
|
||||
attachment = Attachment.find_by(:id => custom_value.value.to_s)
|
||||
if attachment
|
||||
attachment.container = custom_value
|
||||
attachment.save!
|
||||
end
|
||||
end
|
||||
if custom_value.value_was.present?
|
||||
attachment = Attachment.where(:id => custom_value.value_was.to_s).first
|
||||
if custom_value.value_before_last_save.present?
|
||||
attachment = Attachment.find_by(:id => custom_value.value_before_last_save.to_s)
|
||||
if attachment
|
||||
attachment.destroy
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -44,7 +46,7 @@ module Redmine
|
|||
add_at = nil
|
||||
add_to = nil
|
||||
del_at = nil
|
||||
deleted = ""
|
||||
deleted = +""
|
||||
diff.each do |change|
|
||||
pos = change[1]
|
||||
if change[0] == "+"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -19,7 +21,7 @@ module Redmine
|
|||
module Helpers
|
||||
# Simple class to handle gantt chart data
|
||||
class Gantt
|
||||
class MaxLinesLimitReached < Exception
|
||||
class MaxLinesLimitReached < StandardError
|
||||
end
|
||||
|
||||
include ERB::Util
|
||||
|
@ -32,8 +34,10 @@ module Redmine
|
|||
IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
|
||||
}.freeze
|
||||
|
||||
# :nodoc:
|
||||
UNAVAILABLE_COLUMNS = [:tracker, :id, :subject]
|
||||
|
||||
# Some utility methods for the PDF export
|
||||
# @private
|
||||
class PDF
|
||||
MaxCharactorsForSubject = 45
|
||||
TotalWidth = 280
|
||||
|
@ -65,17 +69,19 @@ module Redmine
|
|||
zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
|
||||
@zoom = (zoom > 0 && zoom < 5) ? zoom : 2
|
||||
months = (options[:months] || User.current.pref[:gantt_months]).to_i
|
||||
@months = (months > 0 && months < 25) ? months : 6
|
||||
@months = (months > 0 && months < Setting.gantt_months_limit.to_i + 1) ? months : 6
|
||||
# Save gantt parameters as user preference (zoom and months count)
|
||||
if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
|
||||
@months != User.current.pref[:gantt_months]))
|
||||
if User.current.logged? &&
|
||||
(@zoom != User.current.pref[:gantt_zoom] ||
|
||||
@months != User.current.pref[:gantt_months])
|
||||
User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
|
||||
User.current.preference.save
|
||||
end
|
||||
@date_from = Date.civil(@year_from, @month_from, 1)
|
||||
@date_to = (@date_from >> @months) - 1
|
||||
@subjects = ''
|
||||
@lines = ''
|
||||
@subjects = +''
|
||||
@lines = +''
|
||||
@columns ||= {}
|
||||
@number_of_rows = nil
|
||||
@truncated = false
|
||||
if options.has_key?(:max_rows)
|
||||
|
@ -135,11 +141,17 @@ module Redmine
|
|||
@lines
|
||||
end
|
||||
|
||||
# Renders the selected column of the Gantt chart, the right side of subjects.
|
||||
def selected_column_content(options={})
|
||||
render(options.merge(:only => :selected_columns)) unless @columns.has_key?(options[:column].name)
|
||||
@columns[options[:column].name]
|
||||
end
|
||||
|
||||
# Returns issues that will be rendered
|
||||
def issues
|
||||
@issues ||= @query.issues(
|
||||
:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
|
||||
:order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
|
||||
:order => ["#{Project.table_name}.lft ASC", "#{Issue.table_name}.id ASC"],
|
||||
:limit => @max_rows
|
||||
)
|
||||
end
|
||||
|
@ -196,8 +208,9 @@ module Redmine
|
|||
:indent_increment => 20, :render => :subject,
|
||||
:format => :html}.merge(options)
|
||||
indent = options[:indent] || 4
|
||||
@subjects = '' unless options[:only] == :lines
|
||||
@lines = '' unless options[:only] == :subjects
|
||||
@subjects = +'' unless options[:only] == :lines || options[:only] == :selected_columns
|
||||
@lines = +'' unless options[:only] == :subjects || options[:only] == :selected_columns
|
||||
@columns[options[:column].name] = +'' if options[:only] == :selected_columns && @columns.has_key?(options[:column]) == false
|
||||
@number_of_rows = 0
|
||||
begin
|
||||
Project.project_tree(projects) do |project, level|
|
||||
|
@ -207,8 +220,8 @@ module Redmine
|
|||
rescue MaxLinesLimitReached
|
||||
@truncated = true
|
||||
end
|
||||
@subjects_rendered = true unless options[:only] == :lines
|
||||
@lines_rendered = true unless options[:only] == :subjects
|
||||
@subjects_rendered = true unless options[:only] == :lines || options[:only] == :selected_columns
|
||||
@lines_rendered = true unless options[:only] == :subjects || options[:only] == :selected_columns
|
||||
render_end(options)
|
||||
end
|
||||
|
||||
|
@ -254,8 +267,9 @@ module Redmine
|
|||
|
||||
def render_object_row(object, options)
|
||||
class_name = object.class.name.downcase
|
||||
send("subject_for_#{class_name}", object, options) unless options[:only] == :lines
|
||||
send("line_for_#{class_name}", object, options) unless options[:only] == :subjects
|
||||
send("subject_for_#{class_name}", object, options) unless options[:only] == :lines || options[:only] == :selected_columns
|
||||
send("line_for_#{class_name}", object, options) unless options[:only] == :subjects || options[:only] == :selected_columns
|
||||
column_content_for_issue(object, options) if options[:only] == :selected_columns && options[:column].present? && object.is_a?(Issue)
|
||||
options[:top] += options[:top_increment]
|
||||
@number_of_rows += 1
|
||||
if @max_rows && @number_of_rows >= @max_rows
|
||||
|
@ -301,9 +315,9 @@ module Redmine
|
|||
def line_for_version(version, options)
|
||||
# Skip versions that don't have a start_date
|
||||
if version.is_a?(Version) && version.due_date && version.start_date
|
||||
label = "#{h(version)} #{h(version.completed_percent.to_f.round)}%"
|
||||
label = "#{h(version)} #{h(version.visible_fixed_issues.completed_percent.to_f.round)}%"
|
||||
label = h("#{version.project} -") + label unless @project && @project == version.project
|
||||
line(version.start_date, version.due_date, version.completed_percent, true, label, options, version)
|
||||
line(version.start_date, version.due_date, version.visible_fixed_issues.completed_percent, true, label, options, version)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -323,6 +337,17 @@ module Redmine
|
|||
end
|
||||
end
|
||||
|
||||
def column_content_for_issue(issue, options)
|
||||
if options[:format] == :html
|
||||
data_options = {}
|
||||
data_options[:collapse_expand] = "issue-#{issue.id}"
|
||||
style = "position: absolute;top: #{options[:top]}px; font-size: 0.8em;"
|
||||
content = view.content_tag(:div, view.column_content(options[:column], issue), :style => style, :class => "issue_#{options[:column].name}", :id => "#{options[:column].name}_issue_#{issue.id}", :data => data_options)
|
||||
@columns[options[:column].name] << content if @columns.has_key?(options[:column].name)
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
def subject(label, options, object=nil)
|
||||
send "#{options[:format]}_subject", options, label, object
|
||||
end
|
||||
|
@ -335,7 +360,7 @@ module Redmine
|
|||
end
|
||||
|
||||
# Generates a gantt image
|
||||
# Only defined if RMagick is avalaible
|
||||
# Only defined if MiniMagick is avalaible
|
||||
def to_image(format='PNG')
|
||||
date_to = (@date_from >> @months) - 1
|
||||
show_weeks = @zoom > 1
|
||||
|
@ -348,98 +373,123 @@ module Redmine
|
|||
g_height = 20 * number_of_rows + 30
|
||||
headers_height = (show_weeks ? 2 * header_height : header_height)
|
||||
height = g_height + headers_height
|
||||
imgl = Magick::ImageList.new
|
||||
imgl.new_image(subject_width + g_width + 1, height)
|
||||
gc = Magick::Draw.new
|
||||
gc.font = Redmine::Configuration['rmagick_font_path'] || ""
|
||||
# Subjects
|
||||
gc.stroke('transparent')
|
||||
subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
|
||||
# Months headers
|
||||
month_f = @date_from
|
||||
left = subject_width
|
||||
@months.times do
|
||||
width = ((month_f >> 1) - month_f) * zoom
|
||||
gc.fill('white')
|
||||
gc.stroke('grey')
|
||||
gc.stroke_width(1)
|
||||
gc.rectangle(left, 0, left + width, height)
|
||||
gc.fill('black')
|
||||
# TODO: Remove rmagick_font_path in a later version
|
||||
Rails.logger.warn('rmagick_font_path option is deprecated. Use minimagick_font_path instead.') \
|
||||
unless Redmine::Configuration['rmagick_font_path'].nil?
|
||||
font_path = Redmine::Configuration['minimagick_font_path'].presence || Redmine::Configuration['rmagick_font_path'].presence
|
||||
img = MiniMagick::Image.create(".#{format}", false)
|
||||
MiniMagick::Tool::Convert.new do |gc|
|
||||
gc.size('%dx%d' % [subject_width + g_width + 1, height])
|
||||
gc.xc('white')
|
||||
gc.font(font_path) if font_path.present?
|
||||
# Subjects
|
||||
gc.stroke('transparent')
|
||||
gc.stroke_width(1)
|
||||
gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
|
||||
left = left + width
|
||||
month_f = month_f >> 1
|
||||
end
|
||||
# Weeks headers
|
||||
if show_weeks
|
||||
subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
|
||||
# Months headers
|
||||
month_f = @date_from
|
||||
left = subject_width
|
||||
height = header_height
|
||||
if @date_from.cwday == 1
|
||||
# date_from is monday
|
||||
week_f = date_from
|
||||
else
|
||||
# find next monday after date_from
|
||||
week_f = @date_from + (7 - @date_from.cwday + 1)
|
||||
width = (7 - @date_from.cwday + 1) * zoom
|
||||
@months.times do
|
||||
width = ((month_f >> 1) - month_f) * zoom
|
||||
gc.fill('white')
|
||||
gc.stroke('grey')
|
||||
gc.stroke_width(1)
|
||||
gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1)
|
||||
left = left + width
|
||||
end
|
||||
while week_f <= date_to
|
||||
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
|
||||
gc.fill('white')
|
||||
gc.stroke('grey')
|
||||
gc.stroke_width(1)
|
||||
gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1)
|
||||
gc.strokewidth(1)
|
||||
gc.draw('rectangle %d,%d %d,%d' % [
|
||||
left, 0, left + width, height
|
||||
])
|
||||
gc.fill('black')
|
||||
gc.stroke('transparent')
|
||||
gc.stroke_width(1)
|
||||
gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s)
|
||||
gc.strokewidth(1)
|
||||
gc.draw('text %d,%d %s' % [
|
||||
left.round + 8, 14, Redmine::Utils::Shell.shell_quote("#{month_f.year}-#{month_f.month}")
|
||||
])
|
||||
left = left + width
|
||||
week_f = week_f + 7
|
||||
month_f = month_f >> 1
|
||||
end
|
||||
end
|
||||
# Days details (week-end in grey)
|
||||
if show_days
|
||||
left = subject_width
|
||||
height = g_height + header_height - 1
|
||||
wday = @date_from.cwday
|
||||
(date_to - @date_from + 1).to_i.times do
|
||||
width = zoom
|
||||
gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
|
||||
gc.stroke('#ddd')
|
||||
gc.stroke_width(1)
|
||||
gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1)
|
||||
left = left + width
|
||||
wday = wday + 1
|
||||
wday = 1 if wday > 7
|
||||
# Weeks headers
|
||||
if show_weeks
|
||||
left = subject_width
|
||||
height = header_height
|
||||
if @date_from.cwday == 1
|
||||
# date_from is monday
|
||||
week_f = date_from
|
||||
else
|
||||
# find next monday after date_from
|
||||
week_f = @date_from + (7 - @date_from.cwday + 1)
|
||||
width = (7 - @date_from.cwday + 1) * zoom
|
||||
gc.fill('white')
|
||||
gc.stroke('grey')
|
||||
gc.strokewidth(1)
|
||||
gc.draw('rectangle %d,%d %d,%d' % [
|
||||
left, header_height, left + width, 2 * header_height + g_height - 1
|
||||
])
|
||||
left = left + width
|
||||
end
|
||||
while week_f <= date_to
|
||||
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
|
||||
gc.fill('white')
|
||||
gc.stroke('grey')
|
||||
gc.strokewidth(1)
|
||||
gc.draw('rectangle %d,%d %d,%d' % [
|
||||
left.round, header_height, left.round + width, 2 * header_height + g_height - 1
|
||||
])
|
||||
gc.fill('black')
|
||||
gc.stroke('transparent')
|
||||
gc.strokewidth(1)
|
||||
gc.draw('text %d,%d %s' % [
|
||||
left.round + 2, header_height + 14, Redmine::Utils::Shell.shell_quote(week_f.cweek.to_s)
|
||||
])
|
||||
left = left + width
|
||||
week_f = week_f + 7
|
||||
end
|
||||
end
|
||||
# Days details (week-end in grey)
|
||||
if show_days
|
||||
left = subject_width
|
||||
height = g_height + header_height - 1
|
||||
wday = @date_from.cwday
|
||||
(date_to - @date_from + 1).to_i.times do
|
||||
width = zoom
|
||||
gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
|
||||
gc.stroke('#ddd')
|
||||
gc.strokewidth(1)
|
||||
gc.draw('rectangle %d,%d %d,%d' % [
|
||||
left, 2 * header_height, left + width, 2 * header_height + g_height - 1
|
||||
])
|
||||
left = left + width
|
||||
wday = wday + 1
|
||||
wday = 1 if wday > 7
|
||||
end
|
||||
end
|
||||
# border
|
||||
gc.fill('transparent')
|
||||
gc.stroke('grey')
|
||||
gc.strokewidth(1)
|
||||
gc.draw('rectangle %d,%d %d,%d' % [
|
||||
0, 0, subject_width + g_width, headers_height
|
||||
])
|
||||
gc.stroke('black')
|
||||
gc.draw('rectangle %d,%d %d,%d' % [
|
||||
0, 0, subject_width + g_width, g_height + headers_height - 1
|
||||
])
|
||||
# content
|
||||
top = headers_height + 20
|
||||
gc.stroke('transparent')
|
||||
lines(:image => gc, :top => top, :zoom => zoom,
|
||||
:subject_width => subject_width, :format => :image)
|
||||
# today red line
|
||||
if User.current.today >= @date_from and User.current.today <= date_to
|
||||
gc.stroke('red')
|
||||
x = (User.current.today - @date_from + 1) * zoom + subject_width
|
||||
gc.draw('line %g,%g %g,%g' % [
|
||||
x, headers_height, x, headers_height + g_height - 1
|
||||
])
|
||||
end
|
||||
gc << img.path
|
||||
end
|
||||
# border
|
||||
gc.fill('transparent')
|
||||
gc.stroke('grey')
|
||||
gc.stroke_width(1)
|
||||
gc.rectangle(0, 0, subject_width + g_width, headers_height)
|
||||
gc.stroke('black')
|
||||
gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1)
|
||||
# content
|
||||
top = headers_height + 20
|
||||
gc.stroke('transparent')
|
||||
lines(:image => gc, :top => top, :zoom => zoom,
|
||||
:subject_width => subject_width, :format => :image)
|
||||
# today red line
|
||||
if User.current.today >= @date_from and User.current.today <= date_to
|
||||
gc.stroke('red')
|
||||
x = (User.current.today - @date_from + 1) * zoom + subject_width
|
||||
gc.line(x, headers_height, x, headers_height + g_height - 1)
|
||||
end
|
||||
gc.draw(imgl)
|
||||
imgl.format = format
|
||||
imgl.to_blob
|
||||
end if Object.const_defined?(:Magick)
|
||||
img.to_blob
|
||||
ensure
|
||||
img.destroy! if img
|
||||
end if Object.const_defined?(:MiniMagick)
|
||||
|
||||
def to_pdf
|
||||
pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
|
||||
|
@ -581,7 +631,7 @@ module Redmine
|
|||
coords[:bar_start] = 0
|
||||
end
|
||||
if end_date < self.date_to
|
||||
coords[:end] = end_date - self.date_from
|
||||
coords[:end] = end_date - self.date_from + 1
|
||||
coords[:bar_end] = end_date - self.date_from + 1
|
||||
else
|
||||
coords[:bar_end] = self.date_to - self.date_from + 1
|
||||
|
@ -595,11 +645,11 @@ module Redmine
|
|||
coords[:bar_progress_end] = self.date_to - self.date_from + 1
|
||||
end
|
||||
end
|
||||
if progress_date < User.current.today
|
||||
late_date = [User.current.today, end_date].min
|
||||
if progress_date <= User.current.today
|
||||
late_date = [User.current.today, end_date].min + 1
|
||||
if late_date > self.date_from && late_date > start_date
|
||||
if late_date < self.date_to
|
||||
coords[:bar_late_end] = late_date - self.date_from + 1
|
||||
coords[:bar_late_end] = late_date - self.date_from
|
||||
else
|
||||
coords[:bar_late_end] = self.date_to - self.date_from + 1
|
||||
end
|
||||
|
@ -608,7 +658,7 @@ module Redmine
|
|||
end
|
||||
end
|
||||
# Transforms dates into pixels witdh
|
||||
coords.keys.each do |key|
|
||||
coords.each_key do |key|
|
||||
coords[key] = (coords[key] * zoom).floor
|
||||
end
|
||||
coords
|
||||
|
@ -618,23 +668,26 @@ module Redmine
|
|||
start_date + (end_date - start_date + 1) * (progress / 100.0)
|
||||
end
|
||||
|
||||
def self.sort_issues!(issues)
|
||||
issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)}
|
||||
end
|
||||
# Singleton class method is public
|
||||
class << self
|
||||
def sort_issues!(issues)
|
||||
issues.sort_by! {|issue| sort_issue_logic(issue)}
|
||||
end
|
||||
|
||||
def self.sort_issue_logic(issue)
|
||||
julian_date = Date.new()
|
||||
ancesters_start_date = []
|
||||
current_issue = issue
|
||||
begin
|
||||
ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
|
||||
current_issue = current_issue.parent
|
||||
end while (current_issue)
|
||||
ancesters_start_date
|
||||
end
|
||||
def sort_issue_logic(issue)
|
||||
julian_date = Date.new()
|
||||
ancesters_start_date = []
|
||||
current_issue = issue
|
||||
begin
|
||||
ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
|
||||
current_issue = current_issue.parent
|
||||
end while (current_issue)
|
||||
ancesters_start_date
|
||||
end
|
||||
|
||||
def self.sort_versions!(versions)
|
||||
versions.sort!
|
||||
def sort_versions!(versions)
|
||||
versions.sort!
|
||||
end
|
||||
end
|
||||
|
||||
def pdf_new_page?(options)
|
||||
|
@ -650,7 +703,7 @@ module Redmine
|
|||
case object
|
||||
when Issue
|
||||
issue = object
|
||||
css_classes = ''
|
||||
css_classes = +''
|
||||
css_classes << ' issue-overdue' if issue.overdue?
|
||||
css_classes << ' issue-behind-schedule' if issue.behind_schedule?
|
||||
css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
|
||||
|
@ -661,26 +714,21 @@ module Redmine
|
|||
css_classes << ' behind-start-date' if progress_date < self.date_from
|
||||
css_classes << ' over-end-date' if progress_date > self.date_to
|
||||
end
|
||||
s = "".html_safe
|
||||
if issue.assigned_to.present?
|
||||
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
|
||||
s << view.avatar(issue.assigned_to,
|
||||
:class => 'gravatar icon-gravatar',
|
||||
:size => 13,
|
||||
:title => assigned_string).to_s.html_safe
|
||||
end
|
||||
s = (+"").html_safe
|
||||
s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar')
|
||||
s << view.link_to_issue(issue).html_safe
|
||||
s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => issue.id, :style => 'display:none;', :class => 'toggle-selection')
|
||||
view.content_tag(:span, s, :class => css_classes).html_safe
|
||||
when Version
|
||||
version = object
|
||||
html_class = ""
|
||||
html_class = +""
|
||||
html_class << 'icon icon-package '
|
||||
html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
|
||||
html_class << (version.overdue? ? 'version-overdue' : '')
|
||||
html_class << ' version-closed' unless version.open?
|
||||
if version.start_date && version.due_date && version.completed_percent
|
||||
if version.start_date && version.due_date && version.visible_fixed_issues.completed_percent
|
||||
progress_date = calc_progress_date(version.start_date,
|
||||
version.due_date, version.completed_percent)
|
||||
version.due_date, version.visible_fixed_issues.completed_percent)
|
||||
html_class << ' behind-start-date' if progress_date < self.date_from
|
||||
html_class << ' over-end-date' if progress_date > self.date_to
|
||||
end
|
||||
|
@ -688,7 +736,7 @@ module Redmine
|
|||
view.content_tag(:span, s, :class => html_class).html_safe
|
||||
when Project
|
||||
project = object
|
||||
html_class = ""
|
||||
html_class = +""
|
||||
html_class << 'icon icon-projects '
|
||||
html_class << (project.overdue? ? 'project-overdue' : '')
|
||||
s = view.link_to_project(project).html_safe
|
||||
|
@ -697,21 +745,43 @@ module Redmine
|
|||
end
|
||||
|
||||
def html_subject(params, subject, object)
|
||||
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
||||
style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
||||
content = html_subject_content(object) || subject
|
||||
tag_options = {:style => style}
|
||||
tag_options = {}
|
||||
case object
|
||||
when Issue
|
||||
tag_options[:id] = "issue-#{object.id}"
|
||||
tag_options[:class] = "issue-subject"
|
||||
tag_options[:class] = "issue-subject hascontextmenu"
|
||||
tag_options[:title] = object.subject
|
||||
children = object.children & project_issues(object.project)
|
||||
has_children = children.present? && (children.collect(&:fixed_version).uniq & [object.fixed_version]).present?
|
||||
when Version
|
||||
tag_options[:id] = "version-#{object.id}"
|
||||
tag_options[:class] = "version-name"
|
||||
has_children = object.fixed_issues.exists?
|
||||
when Project
|
||||
tag_options[:class] = "project-name"
|
||||
has_children = object.issues.exists? || object.versions.exists?
|
||||
end
|
||||
if object
|
||||
tag_options[:data] = {
|
||||
:collapse_expand => {
|
||||
:top_increment => params[:top_increment],
|
||||
:obj_id => "#{object.class}-#{object.id}".downcase,
|
||||
},
|
||||
}
|
||||
end
|
||||
if has_children
|
||||
content = view.content_tag(:span, nil, :class => 'icon icon-expended expander') + content
|
||||
tag_options[:class] += ' open'
|
||||
else
|
||||
if params[:indent]
|
||||
params = params.dup
|
||||
params[:indent] += 12
|
||||
end
|
||||
end
|
||||
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
||||
style += "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
||||
tag_options[:style] = style
|
||||
output = view.content_tag(:div, content, tag_options)
|
||||
@subjects << output
|
||||
output
|
||||
|
@ -725,7 +795,7 @@ module Redmine
|
|||
params[:pdf].RDMCell(params[:subject_width] - 15, 5,
|
||||
(" " * params[:indent]) +
|
||||
subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
|
||||
"LR")
|
||||
"LR")
|
||||
params[:pdf].SetY(params[:top])
|
||||
params[:pdf].SetX(params[:subject_width])
|
||||
params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
|
||||
|
@ -734,8 +804,10 @@ module Redmine
|
|||
def image_subject(params, subject, options={})
|
||||
params[:image].fill('black')
|
||||
params[:image].stroke('transparent')
|
||||
params[:image].stroke_width(1)
|
||||
params[:image].text(params[:indent], params[:top] + 2, subject)
|
||||
params[:image].strokewidth(1)
|
||||
params[:image].draw('text %d,%d %s' % [
|
||||
params[:indent], params[:top] + 2, Redmine::Utils::Shell.shell_quote(subject)
|
||||
])
|
||||
end
|
||||
|
||||
def issue_relations(issue)
|
||||
|
@ -749,9 +821,11 @@ module Redmine
|
|||
end
|
||||
|
||||
def html_task(params, coords, markers, label, object)
|
||||
output = ''
|
||||
|
||||
css = "task " + case object
|
||||
output = +''
|
||||
data_options = {}
|
||||
data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase if object
|
||||
css = "task " +
|
||||
case object
|
||||
when Project
|
||||
"project"
|
||||
when Version
|
||||
|
@ -761,11 +835,10 @@ module Redmine
|
|||
else
|
||||
""
|
||||
end
|
||||
|
||||
# Renders the task bar, with progress and late
|
||||
if coords[:bar_start] && coords[:bar_end]
|
||||
width = coords[:bar_end] - coords[:bar_start] - 2
|
||||
style = ""
|
||||
style = +""
|
||||
style << "top:#{params[:top]}px;"
|
||||
style << "left:#{coords[:bar_start]}px;"
|
||||
style << "width:#{width}px;"
|
||||
|
@ -773,27 +846,30 @@ module Redmine
|
|||
html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
|
||||
content_opt = {:style => style,
|
||||
:class => "#{css} task_todo",
|
||||
:id => html_id}
|
||||
:id => html_id,
|
||||
:data => {}}
|
||||
if object.is_a?(Issue)
|
||||
rels = issue_relations(object)
|
||||
if rels.present?
|
||||
content_opt[:data] = {"rels" => rels.to_json}
|
||||
end
|
||||
end
|
||||
content_opt[:data].merge!(data_options)
|
||||
output << view.content_tag(:div, ' '.html_safe, content_opt)
|
||||
if coords[:bar_late_end]
|
||||
width = coords[:bar_late_end] - coords[:bar_start] - 2
|
||||
style = ""
|
||||
style = +""
|
||||
style << "top:#{params[:top]}px;"
|
||||
style << "left:#{coords[:bar_start]}px;"
|
||||
style << "width:#{width}px;"
|
||||
output << view.content_tag(:div, ' '.html_safe,
|
||||
:style => style,
|
||||
:class => "#{css} task_late")
|
||||
:class => "#{css} task_late",
|
||||
:data => data_options)
|
||||
end
|
||||
if coords[:bar_progress_end]
|
||||
width = coords[:bar_progress_end] - coords[:bar_start] - 2
|
||||
style = ""
|
||||
style = +""
|
||||
style << "top:#{params[:top]}px;"
|
||||
style << "left:#{coords[:bar_start]}px;"
|
||||
style << "width:#{width}px;"
|
||||
|
@ -802,46 +878,51 @@ module Redmine
|
|||
output << view.content_tag(:div, ' '.html_safe,
|
||||
:style => style,
|
||||
:class => "#{css} task_done",
|
||||
:id => html_id)
|
||||
:id => html_id,
|
||||
:data => data_options)
|
||||
end
|
||||
end
|
||||
# Renders the markers
|
||||
if markers
|
||||
if coords[:start]
|
||||
style = ""
|
||||
style = +""
|
||||
style << "top:#{params[:top]}px;"
|
||||
style << "left:#{coords[:start]}px;"
|
||||
style << "width:15px;"
|
||||
output << view.content_tag(:div, ' '.html_safe,
|
||||
:style => style,
|
||||
:class => "#{css} marker starting")
|
||||
:class => "#{css} marker starting",
|
||||
:data => data_options)
|
||||
end
|
||||
if coords[:end]
|
||||
style = ""
|
||||
style = +""
|
||||
style << "top:#{params[:top]}px;"
|
||||
style << "left:#{coords[:end] + params[:zoom]}px;"
|
||||
style << "left:#{coords[:end]}px;"
|
||||
style << "width:15px;"
|
||||
output << view.content_tag(:div, ' '.html_safe,
|
||||
:style => style,
|
||||
:class => "#{css} marker ending")
|
||||
:class => "#{css} marker ending",
|
||||
:data => data_options)
|
||||
end
|
||||
end
|
||||
# Renders the label on the right
|
||||
if label
|
||||
style = ""
|
||||
style = +""
|
||||
style << "top:#{params[:top]}px;"
|
||||
style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
||||
style << "width:15px;"
|
||||
output << view.content_tag(:div, label,
|
||||
:style => style,
|
||||
:class => "#{css} label")
|
||||
:class => "#{css} label",
|
||||
:data => data_options)
|
||||
end
|
||||
# Renders the tooltip
|
||||
if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
|
||||
s = view.content_tag(:span,
|
||||
view.render_issue_tooltip(object).html_safe,
|
||||
:class => "tip")
|
||||
style = ""
|
||||
s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection')
|
||||
style = +""
|
||||
style << "position: absolute;"
|
||||
style << "top:#{params[:top]}px;"
|
||||
style << "left:#{coords[:bar_start]}px;"
|
||||
|
@ -849,7 +930,8 @@ module Redmine
|
|||
style << "height:12px;"
|
||||
output << view.content_tag(:div, s.html_safe,
|
||||
:style => style,
|
||||
:class => "tooltip")
|
||||
:class => "tooltip hascontextmenu",
|
||||
:data => data_options)
|
||||
end
|
||||
@lines << output
|
||||
output
|
||||
|
@ -863,21 +945,24 @@ module Redmine
|
|||
height /= 2 if markers
|
||||
# Renders the task bar, with progress and late
|
||||
if coords[:bar_start] && coords[:bar_end]
|
||||
width = [1, coords[:bar_end] - coords[:bar_start]].max
|
||||
params[:pdf].SetY(params[:top] + 1.5)
|
||||
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
||||
params[:pdf].SetFillColor(200, 200, 200)
|
||||
params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
||||
params[:pdf].RDMCell(width, height, "", 0, 0, "", 1)
|
||||
if coords[:bar_late_end]
|
||||
width = [1, coords[:bar_late_end] - coords[:bar_start]].max
|
||||
params[:pdf].SetY(params[:top] + 1.5)
|
||||
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
||||
params[:pdf].SetFillColor(255, 100, 100)
|
||||
params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
||||
params[:pdf].RDMCell(width, height, "", 0, 0, "", 1)
|
||||
end
|
||||
if coords[:bar_progress_end]
|
||||
width = [1, coords[:bar_progress_end] - coords[:bar_start]].max
|
||||
params[:pdf].SetY(params[:top] + 1.5)
|
||||
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
||||
params[:pdf].SetFillColor(90, 200, 90)
|
||||
params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
||||
params[:pdf].RDMCell(width, height, "", 0, 0, "", 1)
|
||||
end
|
||||
end
|
||||
# Renders the markers
|
||||
|
@ -910,23 +995,29 @@ module Redmine
|
|||
# Renders the task bar, with progress and late
|
||||
if coords[:bar_start] && coords[:bar_end]
|
||||
params[:image].fill('#aaa')
|
||||
params[:image].rectangle(params[:subject_width] + coords[:bar_start],
|
||||
params[:top],
|
||||
params[:subject_width] + coords[:bar_end],
|
||||
params[:top] - height)
|
||||
params[:image].draw('rectangle %d,%d %d,%d' % [
|
||||
params[:subject_width] + coords[:bar_start],
|
||||
params[:top],
|
||||
params[:subject_width] + coords[:bar_end],
|
||||
params[:top] - height
|
||||
])
|
||||
if coords[:bar_late_end]
|
||||
params[:image].fill('#f66')
|
||||
params[:image].rectangle(params[:subject_width] + coords[:bar_start],
|
||||
params[:top],
|
||||
params[:subject_width] + coords[:bar_late_end],
|
||||
params[:top] - height)
|
||||
params[:image].draw('rectangle %d,%d %d,%d' % [
|
||||
params[:subject_width] + coords[:bar_start],
|
||||
params[:top],
|
||||
params[:subject_width] + coords[:bar_late_end],
|
||||
params[:top] - height
|
||||
])
|
||||
end
|
||||
if coords[:bar_progress_end]
|
||||
params[:image].fill('#00c600')
|
||||
params[:image].rectangle(params[:subject_width] + coords[:bar_start],
|
||||
params[:top],
|
||||
params[:subject_width] + coords[:bar_progress_end],
|
||||
params[:top] - height)
|
||||
params[:image].draw('rectangle %d,%d %d,%d' % [
|
||||
params[:subject_width] + coords[:bar_start],
|
||||
params[:top],
|
||||
params[:subject_width] + coords[:bar_progress_end],
|
||||
params[:top] - height
|
||||
])
|
||||
end
|
||||
end
|
||||
# Renders the markers
|
||||
|
@ -935,21 +1026,31 @@ module Redmine
|
|||
x = params[:subject_width] + coords[:start]
|
||||
y = params[:top] - height / 2
|
||||
params[:image].fill('blue')
|
||||
params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
|
||||
params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [
|
||||
x - 4, y,
|
||||
x, y - 4,
|
||||
x + 4, y,
|
||||
x, y + 4
|
||||
])
|
||||
end
|
||||
if coords[:end]
|
||||
x = params[:subject_width] + coords[:end] + params[:zoom]
|
||||
x = params[:subject_width] + coords[:end]
|
||||
y = params[:top] - height / 2
|
||||
params[:image].fill('blue')
|
||||
params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
|
||||
params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [
|
||||
x - 4, y,
|
||||
x, y - 4,
|
||||
x + 4, y,
|
||||
x, y + 4
|
||||
])
|
||||
end
|
||||
end
|
||||
# Renders the label on the right
|
||||
if label
|
||||
params[:image].fill('black')
|
||||
params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,
|
||||
params[:top] + 1,
|
||||
label)
|
||||
params[:image].draw('text %d,%d %s' % [
|
||||
params[:subject_width] + (coords[:bar_end] || 0) + 5, params[:top] + 1, Redmine::Utils::Shell.shell_quote(label)
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -56,7 +58,7 @@ module Redmine
|
|||
end
|
||||
@hours << h
|
||||
end
|
||||
|
||||
|
||||
@hours.each do |row|
|
||||
case @columns
|
||||
when 'year'
|
||||
|
@ -69,13 +71,13 @@ module Redmine
|
|||
row['day'] = "#{row['spent_on']}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
min = @hours.collect {|row| row['spent_on']}.min
|
||||
@from = min ? min.to_date : User.current.today
|
||||
|
||||
max = @hours.collect {|row| row['spent_on']}.max
|
||||
@to = max ? max.to_date : User.current.today
|
||||
|
||||
|
||||
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
|
||||
|
||||
@periods = []
|
||||
|
@ -129,13 +131,13 @@ module Redmine
|
|||
}
|
||||
|
||||
# Add time entry custom fields
|
||||
custom_fields = TimeEntryCustomField.all
|
||||
custom_fields = TimeEntryCustomField.visible
|
||||
# Add project custom fields
|
||||
custom_fields += ProjectCustomField.all
|
||||
custom_fields += ProjectCustomField.visible
|
||||
# Add issue custom fields
|
||||
custom_fields += (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
|
||||
custom_fields += @project.nil? ? IssueCustomField.visible.for_all : @project.all_issue_custom_fields.visible
|
||||
# Add time entry activity custom fields
|
||||
custom_fields += TimeEntryActivityCustomField.all
|
||||
custom_fields += TimeEntryActivityCustomField.visible
|
||||
|
||||
# Add list and boolean custom fields as available criteria
|
||||
custom_fields.select {|cf| %w(list bool).include?(cf.field_format) && !cf.multiple?}.each do |cf|
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -24,10 +26,10 @@ module Redmine
|
|||
# URLs relative to the current document or document root (without a protocol
|
||||
# separator, should be harmless
|
||||
return true unless uri.to_s.include? ":"
|
||||
|
||||
|
||||
# Other URLs need to be parsed
|
||||
schemes.include? URI.parse(uri).scheme
|
||||
rescue URI::InvalidURIError
|
||||
rescue URI::Error
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -29,4 +31,4 @@ module Redmine
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -65,11 +67,11 @@ module Redmine
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def controller
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
def config
|
||||
ActionController::Base.config
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -61,7 +63,7 @@ module Redmine
|
|||
# Localizes the given args with user's language
|
||||
def lu(user, *args)
|
||||
lang = user.try(:language).presence || Setting.default_language
|
||||
ll(lang, *args)
|
||||
ll(lang, *args)
|
||||
end
|
||||
|
||||
def format_date(date)
|
||||
|
@ -77,8 +79,7 @@ module Redmine
|
|||
options = {}
|
||||
options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format)
|
||||
time = time.to_time if time.is_a?(String)
|
||||
zone = user.time_zone
|
||||
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
|
||||
local = user.convert_time_to_user_timezone(time)
|
||||
(include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options)
|
||||
end
|
||||
|
||||
|
@ -116,16 +117,18 @@ module Redmine
|
|||
# The result is cached to prevent from loading all translations files
|
||||
# unless :cache => false option is given
|
||||
def languages_options(options={})
|
||||
options = if options[:cache] == false
|
||||
valid_languages.
|
||||
select {|locale| ::I18n.exists?(:general_lang_name, locale)}.
|
||||
map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.
|
||||
sort {|x,y| x.first <=> y.first }
|
||||
else
|
||||
ActionController::Base.cache_store.fetch "i18n/languages_options/#{Redmine::VERSION}" do
|
||||
languages_options :cache => false
|
||||
options =
|
||||
if options[:cache] == false
|
||||
available_locales = ::I18n.backend.available_locales
|
||||
valid_languages.
|
||||
select {|locale| available_locales.include?(locale)}.
|
||||
map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.
|
||||
sort_by(&:first)
|
||||
else
|
||||
ActionController::Base.cache_store.fetch "i18n/languages_options/#{Redmine::VERSION}" do
|
||||
languages_options :cache => false
|
||||
end
|
||||
end
|
||||
end
|
||||
options.map {|name, lang| [name.force_encoding("UTF-8"), lang.force_encoding("UTF-8")]}
|
||||
end
|
||||
|
||||
|
@ -145,71 +148,17 @@ module Redmine
|
|||
end
|
||||
|
||||
# Custom backend based on I18n::Backend::Simple with the following changes:
|
||||
# * lazy loading of translation files
|
||||
# * available_locales are determined by looking at translation file names
|
||||
class Backend
|
||||
(class << self; self; end).class_eval { public :include }
|
||||
|
||||
class Backend < ::I18n::Backend::Simple
|
||||
module Implementation
|
||||
include ::I18n::Backend::Base
|
||||
include ::I18n::Backend::Pluralization
|
||||
|
||||
# Stores translations for the given locale in memory.
|
||||
# This uses a deep merge for the translations hash, so existing
|
||||
# translations will be overwritten by new ones only at the deepest
|
||||
# level of the hash.
|
||||
def store_translations(locale, data, options = {})
|
||||
locale = locale.to_sym
|
||||
translations[locale] ||= {}
|
||||
data = data.deep_symbolize_keys
|
||||
translations[locale].deep_merge!(data)
|
||||
end
|
||||
|
||||
# Get available locales from the translations filenames
|
||||
def available_locales
|
||||
@available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym)
|
||||
end
|
||||
|
||||
# Clean up translations
|
||||
def reload!
|
||||
@translations = nil
|
||||
@available_locales = nil
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def init_translations(locale)
|
||||
locale = locale.to_s
|
||||
paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
|
||||
load_translations(paths)
|
||||
translations[locale] ||= {}
|
||||
end
|
||||
|
||||
def translations
|
||||
@translations ||= {}
|
||||
end
|
||||
|
||||
# Looks up a translation from the translations hash. Returns nil if
|
||||
# eiher key is nil, or locale, scope or key do not exist as a key in the
|
||||
# nested translations hash. Splits keys or scopes containing dots
|
||||
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
|
||||
# <tt>%w(currency format)</tt>.
|
||||
def lookup(locale, key, scope = [], options = {})
|
||||
init_translations(locale) unless translations.key?(locale)
|
||||
keys = ::I18n.normalize_keys(locale, key, scope, options[:separator])
|
||||
|
||||
keys.inject(translations) do |result, _key|
|
||||
_key = _key.to_sym
|
||||
return nil unless result.is_a?(Hash) && result.has_key?(_key)
|
||||
result = result[_key]
|
||||
result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include Implementation
|
||||
# Adds fallback to default locale for untranslated strings
|
||||
include ::I18n::Backend::Fallbacks
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: false
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Redmine
|
||||
module Info
|
||||
class << self
|
||||
|
@ -7,13 +9,15 @@ module Redmine
|
|||
def versioned_name; "#{app_name} #{Redmine::VERSION}" end
|
||||
|
||||
def environment
|
||||
s = "Environment:\n"
|
||||
s = +"Environment:\n"
|
||||
s << [
|
||||
["Redmine version", Redmine::VERSION],
|
||||
["Ruby version", "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"],
|
||||
["Rails version", Rails::VERSION::STRING],
|
||||
["Environment", Rails.env],
|
||||
["Database adapter", ActiveRecord::Base.connection.adapter_name]
|
||||
["Database adapter", ActiveRecord::Base.connection.adapter_name],
|
||||
["Mailer queue", ActionMailer::DeliveryJob.queue_adapter.class.name],
|
||||
["Mailer delivery", ActionMailer::Base.delivery_method]
|
||||
].map {|info| " %-30s %s" % info}.join("\n") + "\n"
|
||||
|
||||
s << "SCM:\n"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -17,7 +19,8 @@
|
|||
|
||||
module Redmine
|
||||
module MenuManager
|
||||
class MenuError < StandardError #:nodoc:
|
||||
# @private
|
||||
class MenuError < StandardError
|
||||
end
|
||||
|
||||
module MenuController
|
||||
|
@ -121,7 +124,7 @@ module Redmine
|
|||
else
|
||||
caption, url, selected = extract_node_details(node, project)
|
||||
return content_tag('li',
|
||||
render_single_menu_node(node, caption, url, selected))
|
||||
render_single_menu_node(node, caption, url, selected))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -204,18 +207,19 @@ module Redmine
|
|||
|
||||
def extract_node_details(node, project=nil)
|
||||
item = node
|
||||
url = case item.url
|
||||
when Hash
|
||||
project.nil? ? item.url : {item.param => project}.merge(item.url)
|
||||
when Symbol
|
||||
if project
|
||||
send(item.url, project)
|
||||
url =
|
||||
case item.url
|
||||
when Hash
|
||||
project.nil? ? item.url : {item.param => project}.merge(item.url)
|
||||
when Symbol
|
||||
if project
|
||||
send(item.url, project)
|
||||
else
|
||||
send(item.url)
|
||||
end
|
||||
else
|
||||
send(item.url)
|
||||
item.url
|
||||
end
|
||||
else
|
||||
item.url
|
||||
end
|
||||
caption = item.caption(project)
|
||||
return [caption, url, (current_menu_item == item.name)]
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -15,8 +17,6 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'mime/types'
|
||||
|
||||
module Redmine
|
||||
module MimeType
|
||||
|
||||
|
@ -35,6 +35,7 @@ module Redmine
|
|||
'text/x-ruby' => 'rb,rbw,ruby,rake,erb',
|
||||
'text/x-csh' => 'csh',
|
||||
'text/x-sh' => 'sh',
|
||||
'text/x-textile' => 'textile',
|
||||
'text/xml' => 'xml,xsd,mxml',
|
||||
'text/yaml' => 'yml,yaml',
|
||||
'text/csv' => 'csv',
|
||||
|
@ -46,6 +47,8 @@ module Redmine
|
|||
'image/x-ms-bmp' => 'bmp',
|
||||
'application/javascript' => 'js',
|
||||
'application/pdf' => 'pdf',
|
||||
'video/mp4' => 'mp4',
|
||||
'video/webm' => 'webm',
|
||||
}.freeze
|
||||
|
||||
EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
|
||||
|
@ -60,28 +63,23 @@ module Redmine
|
|||
|
||||
# returns mime type for name or nil if unknown
|
||||
def self.of(name)
|
||||
return nil unless name.present?
|
||||
if m = name.to_s.match(/(^|\.)([^\.]+)$/)
|
||||
extension = m[2].downcase
|
||||
@known_types ||= Hash.new do |h, ext|
|
||||
type = EXTENSIONS[ext]
|
||||
type ||= MIME::Types.type_for(ext).first.to_s.presence
|
||||
h[ext] = type
|
||||
end
|
||||
@known_types[extension]
|
||||
ext = File.extname(name.to_s)[1..-1]
|
||||
if ext
|
||||
ext.downcase!
|
||||
EXTENSIONS[ext] || MiniMime.lookup_by_extension(ext)&.content_type
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the css class associated to
|
||||
# the mime type of name
|
||||
def self.css_class_of(name)
|
||||
mime = of(name)
|
||||
mime && mime.gsub('/', '-')
|
||||
mimetype = of(name)
|
||||
mimetype&.tr('/', '-')
|
||||
end
|
||||
|
||||
def self.main_mimetype_of(name)
|
||||
mimetype = of(name)
|
||||
mimetype.split('/').first if mimetype
|
||||
mimetype&.split('/')&.first
|
||||
end
|
||||
|
||||
# return true if mime-type for name is type/*
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -24,12 +26,14 @@ module Redmine
|
|||
CORE_BLOCKS = {
|
||||
'issuesassignedtome' => {:label => :label_assigned_to_me_issues},
|
||||
'issuesreportedbyme' => {:label => :label_reported_issues},
|
||||
'issuesupdatedbyme' => {:label => :label_updated_issues},
|
||||
'issueswatched' => {:label => :label_watched_issues},
|
||||
'issuequery' => {:label => :label_issue_plural, :max_occurs => 3},
|
||||
'news' => {:label => :label_news_latest},
|
||||
'calendar' => {:label => :label_calendar},
|
||||
'documents' => {:label => :label_document_plural},
|
||||
'timelog' => {:label => :label_spent_time}
|
||||
'timelog' => {:label => :label_spent_time},
|
||||
'activity' => {:label => :label_activity}
|
||||
}
|
||||
|
||||
def self.groups
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -84,19 +86,19 @@ module Redmine
|
|||
if parent
|
||||
previous_root_id = root_id
|
||||
self.root_id = parent.root_id
|
||||
|
||||
|
||||
lft_after_move = target_lft
|
||||
self.class.where(:root_id => parent.root_id).update_all([
|
||||
"lft = CASE WHEN lft >= :lft THEN lft + :shift ELSE lft END, " +
|
||||
"rgt = CASE WHEN rgt >= :lft THEN rgt + :shift ELSE rgt END",
|
||||
{:lft => lft_after_move, :shift => (rgt - lft + 1)}
|
||||
])
|
||||
|
||||
|
||||
self.class.where(:root_id => previous_root_id).update_all([
|
||||
"root_id = :root_id, lft = lft + :shift, rgt = rgt + :shift",
|
||||
{:root_id => parent.root_id, :shift => lft_after_move - lft}
|
||||
])
|
||||
|
||||
|
||||
self.lft, self.rgt = lft_after_move, (rgt - lft + lft_after_move)
|
||||
parent.send :reload_nested_set_values
|
||||
end
|
||||
|
@ -105,7 +107,7 @@ module Redmine
|
|||
def remove_from_nested_set
|
||||
self.class.where(:root_id => root_id).where("lft >= ? AND rgt <= ?", lft, rgt).
|
||||
update_all(["root_id = :id, lft = lft - :shift, rgt = rgt - :shift", {:id => id, :shift => lft - 1}])
|
||||
|
||||
|
||||
self.class.where(:root_id => root_id).update_all([
|
||||
"lft = CASE WHEN lft >= :lft THEN lft - :shift ELSE lft END, " +
|
||||
"rgt = CASE WHEN rgt >= :lft THEN rgt - :shift ELSE rgt END",
|
||||
|
@ -149,7 +151,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def lock_nested_set
|
||||
if self.class.connection.adapter_name =~ /sqlserver/i
|
||||
if /sqlserver/i.match?(self.class.connection.adapter_name)
|
||||
lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)"
|
||||
# Custom lock for SQLServer
|
||||
# This can be problematic if root_id or parent root_id changes
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -119,7 +121,7 @@ module Redmine
|
|||
|
||||
def lock_nested_set
|
||||
lock = true
|
||||
if self.class.connection.adapter_name =~ /sqlserver/i
|
||||
if /sqlserver/i.match?(self.class.connection.adapter_name)
|
||||
lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)"
|
||||
end
|
||||
self.class.order(:id).lock(lock).ids
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Redmine
|
||||
class Notifiable < Struct.new(:name, :parent)
|
||||
|
||||
|
@ -14,6 +16,7 @@ module Redmine
|
|||
notifications << Notifiable.new('issue_status_updated', 'issue_updated')
|
||||
notifications << Notifiable.new('issue_assigned_to_updated', 'issue_updated')
|
||||
notifications << Notifiable.new('issue_priority_updated', 'issue_updated')
|
||||
notifications << Notifiable.new('issue_fixed_version_updated', 'issue_updated')
|
||||
notifications << Notifiable.new('news_added')
|
||||
notifications << Notifiable.new('news_comment_added')
|
||||
notifications << Notifiable.new('document_added')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# encoding: utf-8
|
||||
#
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -159,7 +159,7 @@ module Redmine
|
|||
per_page_links = false if count.nil?
|
||||
page_param = paginator.page_param
|
||||
|
||||
html = '<ul class="pages">'
|
||||
html = +'<ul class="pages">'
|
||||
|
||||
if paginator.multiple_pages?
|
||||
# \xc2\xab(utf-8) = «
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -19,8 +21,8 @@ module Redmine
|
|||
module Platform
|
||||
class << self
|
||||
def mswin?
|
||||
(RUBY_PLATFORM =~ /(:?mswin|mingw)/) ||
|
||||
(RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
|
||||
(/(:?mswin|mingw)/.match?(RUBY_PLATFORM)) ||
|
||||
(RUBY_PLATFORM == 'java' && /windows/i.match?(ENV['OS'] || ENV['os']))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -15,9 +17,12 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine #:nodoc:
|
||||
module Redmine
|
||||
|
||||
# Exception raised when a plugin cannot be found given its id.
|
||||
class PluginNotFound < StandardError; end
|
||||
|
||||
# Exception raised when a plugin requirement is not met.
|
||||
class PluginRequirementError < StandardError; end
|
||||
|
||||
# Base class for Redmine plugins.
|
||||
|
@ -42,10 +47,14 @@ module Redmine #:nodoc:
|
|||
# In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
|
||||
#
|
||||
# When rendered, the plugin settings value is available as the local variable +settings+
|
||||
#
|
||||
# See: http://www.redmine.org/projects/redmine/wiki/Plugin_Tutorial
|
||||
class Plugin
|
||||
# Absolute path to the directory where plugins are located
|
||||
cattr_accessor :directory
|
||||
self.directory = File.join(Rails.root, 'plugins')
|
||||
|
||||
# Absolute path to the plublic directory where plugins assets are copied
|
||||
cattr_accessor :public_directory
|
||||
self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
|
||||
|
||||
|
@ -69,7 +78,17 @@ module Redmine #:nodoc:
|
|||
def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
|
||||
attr_reader :id
|
||||
|
||||
# Plugin constructor
|
||||
# Plugin constructor: instanciates a new Redmine::Plugin with given +id+
|
||||
# and make it evaluate the given +block+
|
||||
#
|
||||
# Example
|
||||
# Redmine::Plugin.register :example do
|
||||
# name 'Example plugin'
|
||||
# author 'John Smith'
|
||||
# description 'This is an example plugin for Redmine'
|
||||
# version '0.0.1'
|
||||
# requires_redmine version_or_higher: '3.0.0'
|
||||
# end
|
||||
def self.register(id, &block)
|
||||
p = new(id)
|
||||
p.instance_eval(&block)
|
||||
|
@ -79,6 +98,10 @@ module Redmine #:nodoc:
|
|||
# Set a default directory if it was not provided during registration
|
||||
p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?
|
||||
|
||||
unless File.directory?(p.directory)
|
||||
raise PluginNotFound, "Plugin not found. The directory for plugin #{p.id} should be #{p.directory}."
|
||||
end
|
||||
|
||||
# Adds plugin locales if any
|
||||
# YAML translation files should be found under <plugin>/config/locales/
|
||||
Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
|
||||
|
@ -90,10 +113,13 @@ module Redmine #:nodoc:
|
|||
ActionMailer::Base.prepend_view_path(view_path)
|
||||
end
|
||||
|
||||
# Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
|
||||
Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
|
||||
ActiveSupport::Dependencies.autoload_paths += [dir]
|
||||
end
|
||||
# Add the plugin directories to rails autoload paths
|
||||
engine_cfg = Rails::Engine::Configuration.new(p.directory)
|
||||
engine_cfg.paths.add 'lib', eager_load: true
|
||||
Rails.application.config.eager_load_paths += engine_cfg.eager_load_paths
|
||||
Rails.application.config.autoload_once_paths += engine_cfg.autoload_once_paths
|
||||
Rails.application.config.autoload_paths += engine_cfg.autoload_paths
|
||||
ActiveSupport::Dependencies.autoload_paths += engine_cfg.eager_load_paths + engine_cfg.autoload_once_paths + engine_cfg.autoload_paths
|
||||
|
||||
# Defines plugin setting if present
|
||||
if p.settings
|
||||
|
@ -170,6 +196,7 @@ module Redmine #:nodoc:
|
|||
id
|
||||
end
|
||||
|
||||
# Returns the absolute path to the plugin assets directory
|
||||
def assets_directory
|
||||
File.join(directory, 'assets')
|
||||
end
|
||||
|
@ -248,7 +275,11 @@ module Redmine #:nodoc:
|
|||
arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
|
||||
arg.assert_valid_keys(:version, :version_or_higher)
|
||||
|
||||
plugin = Plugin.find(plugin_name)
|
||||
begin
|
||||
plugin = Plugin.find(plugin_name)
|
||||
rescue PluginNotFound
|
||||
raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin")
|
||||
end
|
||||
current = plugin.version.split('.').collect(&:to_i)
|
||||
|
||||
arg.each do |k, v|
|
||||
|
@ -311,7 +342,7 @@ module Redmine #:nodoc:
|
|||
# permission :say_hello, { :example => :say_hello }, :require => :member
|
||||
def permission(name, actions, options = {})
|
||||
if @project_module
|
||||
Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
|
||||
Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map| map.permission(name, actions, options)}}
|
||||
else
|
||||
Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
|
||||
end
|
||||
|
@ -367,7 +398,7 @@ module Redmine #:nodoc:
|
|||
# * :label - label for the formatter displayed in application settings
|
||||
#
|
||||
# Examples:
|
||||
# wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter")
|
||||
# wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter")
|
||||
#
|
||||
def wiki_format_provider(name, *args)
|
||||
Redmine::WikiFormatting.register(name, *args)
|
||||
|
@ -391,7 +422,7 @@ module Redmine #:nodoc:
|
|||
base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
|
||||
begin
|
||||
FileUtils.mkdir_p(base_target_dir)
|
||||
rescue Exception => e
|
||||
rescue => e
|
||||
raise "Could not create directory #{base_target_dir}: " + e.message
|
||||
end
|
||||
end
|
||||
|
@ -402,7 +433,7 @@ module Redmine #:nodoc:
|
|||
target_dir = File.join(destination, dir.gsub(source, ''))
|
||||
begin
|
||||
FileUtils.mkdir_p(target_dir)
|
||||
rescue Exception => e
|
||||
rescue => e
|
||||
raise "Could not create directory #{target_dir}: " + e.message
|
||||
end
|
||||
end
|
||||
|
@ -413,7 +444,7 @@ module Redmine #:nodoc:
|
|||
unless File.exist?(target) && FileUtils.identical?(file, target)
|
||||
FileUtils.cp(file, target)
|
||||
end
|
||||
rescue Exception => e
|
||||
rescue => e
|
||||
raise "Could not copy #{file} to #{target}: " + e.message
|
||||
end
|
||||
end
|
||||
|
@ -449,7 +480,6 @@ module Redmine #:nodoc:
|
|||
|
||||
# Migrate this plugin to the given version
|
||||
def migrate(version = nil)
|
||||
puts "Migrating #{id} (#{name})..."
|
||||
Redmine::Plugin::Migrator.migrate_plugin(self, version)
|
||||
end
|
||||
|
||||
|
@ -469,6 +499,36 @@ module Redmine #:nodoc:
|
|||
end
|
||||
end
|
||||
|
||||
class MigrationContext < ActiveRecord::MigrationContext
|
||||
def up(target_version = nil)
|
||||
selected_migrations =
|
||||
if block_given?
|
||||
migrations.select { |m| yield m }
|
||||
else
|
||||
migrations
|
||||
end
|
||||
Migrator.new(:up, selected_migrations, target_version).migrate
|
||||
end
|
||||
|
||||
def down(target_version = nil)
|
||||
selected_migrations =
|
||||
if block_given?
|
||||
migrations.select { |m| yield m }
|
||||
else
|
||||
migrations
|
||||
end
|
||||
Migrator.new(:down, selected_migrations, target_version).migrate
|
||||
end
|
||||
|
||||
def run(direction, target_version)
|
||||
Migrator.new(direction, migrations, target_version).run
|
||||
end
|
||||
|
||||
def open
|
||||
Migrator.new(:up, migrations, nil)
|
||||
end
|
||||
end
|
||||
|
||||
class Migrator < ActiveRecord::Migrator
|
||||
# We need to be able to set the 'current' plugin being migrated.
|
||||
cattr_accessor :current_plugin
|
||||
|
@ -478,22 +538,29 @@ module Redmine #:nodoc:
|
|||
def migrate_plugin(plugin, version)
|
||||
self.current_plugin = plugin
|
||||
return if current_version(plugin) == version
|
||||
migrate(plugin.migration_directory, version)
|
||||
|
||||
MigrationContext.new(plugin.migration_directory).migrate(version)
|
||||
end
|
||||
|
||||
def current_version(plugin=current_plugin)
|
||||
def get_all_versions(plugin = current_plugin)
|
||||
# Delete migrations that don't match .. to_i will work because the number comes first
|
||||
::ActiveRecord::Base.connection.select_values(
|
||||
"SELECT version FROM #{schema_migrations_table_name}"
|
||||
).delete_if{ |v| v.match(/-#{plugin.id}$/) == nil }.map(&:to_i).max || 0
|
||||
@all_versions ||= {}
|
||||
@all_versions[plugin.id.to_s] ||= begin
|
||||
sm_table = ::ActiveRecord::SchemaMigration.table_name
|
||||
migration_versions = ActiveRecord::Base.connection.select_values("SELECT version FROM #{sm_table}")
|
||||
versions_by_plugins = migration_versions.group_by { |version| version.match(/-(.*)$/).try(:[], 1) }
|
||||
@all_versions = versions_by_plugins.transform_values! {|versions| versions.map!(&:to_i).sort! }
|
||||
@all_versions[plugin.id.to_s] || []
|
||||
end
|
||||
end
|
||||
|
||||
def current_version(plugin = current_plugin)
|
||||
get_all_versions(plugin).last || 0
|
||||
end
|
||||
end
|
||||
|
||||
def migrated
|
||||
sm_table = self.class.schema_migrations_table_name
|
||||
::ActiveRecord::Base.connection.select_values(
|
||||
"SELECT version FROM #{sm_table}"
|
||||
).delete_if{ |v| v.match(/-#{current_plugin.id}$/) == nil }.map(&:to_i).sort
|
||||
def load_migrated
|
||||
@migrated_versions = Set.new(self.class.get_all_versions(current_plugin))
|
||||
end
|
||||
|
||||
def record_version_state_after_migrating(version)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: false
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
113
lib/redmine/project_jump_box.rb
Normal file
113
lib/redmine/project_jump_box.rb
Normal file
|
@ -0,0 +1,113 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
class ProjectJumpBox
|
||||
def initialize(user)
|
||||
@user = user
|
||||
@pref_project_ids = {}
|
||||
end
|
||||
|
||||
def recent_projects_count
|
||||
@user.pref.recently_used_projects
|
||||
end
|
||||
|
||||
def recently_used_projects(query = nil)
|
||||
project_ids = recently_used_project_ids
|
||||
projects = Project.where(id: project_ids)
|
||||
if query
|
||||
projects = projects.like(query)
|
||||
end
|
||||
projects.
|
||||
index_by(&:id).
|
||||
values_at(*project_ids). # sort according to stored order
|
||||
compact
|
||||
end
|
||||
|
||||
def bookmarked_projects(query = nil)
|
||||
projects = Project.where(id: bookmarked_project_ids).visible
|
||||
if query
|
||||
projects = projects.like(query)
|
||||
end
|
||||
projects.to_a
|
||||
end
|
||||
|
||||
def project_used(project)
|
||||
return if project.blank? || project.id.blank?
|
||||
|
||||
id_array = recently_used_project_ids
|
||||
id_array.reject!{ |i| i == project.id }
|
||||
# we dont want bookmarks in the recently used list:
|
||||
id_array.unshift(project.id) unless bookmark?(project)
|
||||
self.recently_used_project_ids = id_array[0, recent_projects_count]
|
||||
end
|
||||
|
||||
def bookmark_project(project)
|
||||
self.recently_used_project_ids = recently_used_project_ids.reject{|id| id == project.id}
|
||||
self.bookmarked_project_ids = (bookmarked_project_ids << project.id)
|
||||
end
|
||||
|
||||
def delete_project_bookmark(project)
|
||||
self.bookmarked_project_ids = bookmarked_project_ids.reject do |id|
|
||||
id == project.id
|
||||
end
|
||||
end
|
||||
|
||||
def bookmark?(project)
|
||||
project && project.id && bookmarked_project_ids.include?(project.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bookmarked_project_ids
|
||||
pref_project_ids :bookmarked_project_ids
|
||||
end
|
||||
|
||||
def bookmarked_project_ids=(new_ids)
|
||||
set_pref_project_ids :bookmarked_project_ids, new_ids
|
||||
end
|
||||
|
||||
def recently_used_project_ids
|
||||
pref_project_ids(:recently_used_project_ids)[0,recent_projects_count]
|
||||
end
|
||||
|
||||
def recently_used_project_ids=(new_ids)
|
||||
set_pref_project_ids :recently_used_project_ids, new_ids
|
||||
end
|
||||
|
||||
def pref_project_ids(key)
|
||||
return [] unless @user.logged?
|
||||
|
||||
@pref_project_ids[key] ||= (@user.pref[key] || '').split(',').map(&:to_i)
|
||||
end
|
||||
|
||||
def set_pref_project_ids(key, new_values)
|
||||
return nil unless @user.logged?
|
||||
|
||||
old_value = @user.pref[key]
|
||||
new_value = new_values.uniq.join(',')
|
||||
if old_value != new_value
|
||||
@user.pref[key] = new_value
|
||||
@user.pref.save
|
||||
end
|
||||
@pref_project_ids.delete key
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -32,7 +34,7 @@ module Redmine
|
|||
@safe_attributes ||= []
|
||||
if args.empty?
|
||||
if superclass.include?(Redmine::SafeAttributes)
|
||||
@safe_attributes + superclass.safe_attributes
|
||||
@safe_attributes + superclass.safe_attributes
|
||||
else
|
||||
@safe_attributes
|
||||
end
|
||||
|
@ -80,6 +82,10 @@ module Redmine
|
|||
# Sets attributes from attrs that are safe
|
||||
# attrs is a Hash with string keys
|
||||
def safe_attributes=(attrs, user=User.current)
|
||||
if attrs.respond_to?(:to_unsafe_hash)
|
||||
attrs = attrs.to_unsafe_hash
|
||||
end
|
||||
|
||||
return unless attrs.is_a?(Hash)
|
||||
self.attributes = delete_unsafe_attributes(attrs, user)
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -21,7 +23,8 @@ require 'redmine/scm/adapters'
|
|||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class AbstractAdapter #:nodoc:
|
||||
# @private
|
||||
class AbstractAdapter
|
||||
include Redmine::Utils::Shell
|
||||
|
||||
# raised if scm command exited with error, e.g. unknown revision.
|
||||
|
@ -175,7 +178,8 @@ module Redmine
|
|||
(path[-1,1] == "/") ? path[0..-2] : path
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
def retrieve_root_url
|
||||
info = self.info
|
||||
info ? info.root_url : nil
|
||||
|
@ -183,7 +187,7 @@ module Redmine
|
|||
|
||||
def target(path, sq=true)
|
||||
path ||= ''
|
||||
base = path.match(/^\//) ? root_url : url
|
||||
base = /^\//.match?(path) ? root_url : url
|
||||
str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
|
||||
if sq
|
||||
str = shell_quote(str)
|
||||
|
@ -199,10 +203,6 @@ module Redmine
|
|||
self.class.shellout(cmd, options, &block)
|
||||
end
|
||||
|
||||
def self.logger
|
||||
Rails.logger
|
||||
end
|
||||
|
||||
# Path to the file where scm stderr output is logged
|
||||
# Returns nil if the log file is not writable
|
||||
def self.stderr_log_file
|
||||
|
@ -211,7 +211,7 @@ module Redmine
|
|||
path = Redmine::Configuration['scm_stderr_log_file'].presence
|
||||
path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
|
||||
if File.exists?(path)
|
||||
if File.file?(path) && File.writable?(path)
|
||||
if File.file?(path) && File.writable?(path)
|
||||
writable = true
|
||||
else
|
||||
logger.warn("SCM log file (#{path}) is not writable")
|
||||
|
@ -228,37 +228,41 @@ module Redmine
|
|||
end
|
||||
@stderr_log_file || nil
|
||||
end
|
||||
private_class_method :stderr_log_file
|
||||
|
||||
def self.shellout(cmd, options = {}, &block)
|
||||
if logger && logger.debug?
|
||||
logger.debug "Shelling out: #{strip_credential(cmd)}"
|
||||
# Capture stderr in a log file
|
||||
if stderr_log_file
|
||||
cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
|
||||
end
|
||||
# Singleton class method is public
|
||||
class << self
|
||||
def logger
|
||||
Rails.logger
|
||||
end
|
||||
begin
|
||||
mode = "r+"
|
||||
IO.popen(cmd, mode) do |io|
|
||||
io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
|
||||
io.close_write unless options[:write_stdin]
|
||||
block.call(io) if block_given?
|
||||
|
||||
def shellout(cmd, options = {}, &block)
|
||||
if logger && logger.debug?
|
||||
logger.debug "Shelling out: #{strip_credential(cmd)}"
|
||||
# Capture stderr in a log file
|
||||
if stderr_log_file
|
||||
cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
|
||||
end
|
||||
end
|
||||
begin
|
||||
mode = "r+"
|
||||
IO.popen(cmd, mode) do |io|
|
||||
io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
|
||||
io.close_write unless options[:write_stdin]
|
||||
yield(io) if block_given?
|
||||
end
|
||||
rescue => e
|
||||
msg = strip_credential(e.message)
|
||||
# The command failed, log it and re-raise
|
||||
logmsg = "SCM command failed, "
|
||||
logmsg += "make sure that your SCM command (e.g. svn) is "
|
||||
logmsg += "in PATH (#{ENV['PATH']})\n"
|
||||
logmsg += "You can configure your scm commands in config/configuration.yml.\n"
|
||||
logmsg += "#{strip_credential(cmd)}\n"
|
||||
logmsg += "with: #{msg}"
|
||||
logger.error(logmsg)
|
||||
raise CommandFailed.new(msg)
|
||||
end
|
||||
## If scm command does not exist,
|
||||
## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
|
||||
## in production environment.
|
||||
# rescue Errno::ENOENT => e
|
||||
rescue Exception => e
|
||||
msg = strip_credential(e.message)
|
||||
# The command failed, log it and re-raise
|
||||
logmsg = "SCM command failed, "
|
||||
logmsg += "make sure that your SCM command (e.g. svn) is "
|
||||
logmsg += "in PATH (#{ENV['PATH']})\n"
|
||||
logmsg += "You can configure your scm commands in config/configuration.yml.\n"
|
||||
logmsg += "#{strip_credential(cmd)}\n"
|
||||
logmsg += "with: #{msg}"
|
||||
logger.error(logmsg)
|
||||
raise CommandFailed.new(msg)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -267,18 +271,20 @@ module Redmine
|
|||
q = (Redmine::Platform.mswin? ? '"' : "'")
|
||||
cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
|
||||
end
|
||||
private_class_method :strip_credential
|
||||
|
||||
def strip_credential(cmd)
|
||||
self.class.strip_credential(cmd)
|
||||
end
|
||||
|
||||
def scm_iconv(to, from, str)
|
||||
return nil if str.nil?
|
||||
return if str.nil?
|
||||
return str if to == from && str.encoding.to_s == from
|
||||
str = str.dup
|
||||
str.force_encoding(from)
|
||||
begin
|
||||
str.encode(to)
|
||||
rescue Exception => err
|
||||
rescue => err
|
||||
logger.error("failed to convert from #{from} to #{to}. #{err}")
|
||||
nil
|
||||
end
|
||||
|
@ -328,11 +334,11 @@ module Redmine
|
|||
end
|
||||
|
||||
def is_file?
|
||||
'file' == self.kind
|
||||
self.kind == 'file'
|
||||
end
|
||||
|
||||
def is_dir?
|
||||
'dir' == self.kind
|
||||
self.kind == 'dir'
|
||||
end
|
||||
|
||||
def is_text?
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -43,7 +45,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def scm_command_version
|
||||
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
|
||||
scm_version = scm_version_from_command_line.b
|
||||
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||||
m[2].scan(%r{\d+}).collect(&:to_i)
|
||||
end
|
||||
|
@ -94,10 +96,9 @@ module Redmine
|
|||
cmd_args << "-r#{identifier.to_i}"
|
||||
cmd_args << bzr_target(path)
|
||||
scm_cmd(*cmd_args) do |io|
|
||||
prefix_utf8 = "#{url}/#{path}".gsub('\\', '/')
|
||||
prefix_utf8 = "#{url}/#{path}".tr('\\', '/')
|
||||
logger.debug "PREFIX: #{prefix_utf8}"
|
||||
prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8)
|
||||
prefix.force_encoding('ASCII-8BIT')
|
||||
prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8).b
|
||||
re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
|
||||
io.each_line do |line|
|
||||
next unless line =~ re
|
||||
|
@ -152,7 +153,7 @@ module Redmine
|
|||
parsing = $1
|
||||
elsif line =~ /^ (.*)$/
|
||||
if parsing == 'message'
|
||||
revision.message << "#{$1}\n"
|
||||
revision.message += "#{$1}\n"
|
||||
else
|
||||
if $1 =~ /^(.*)\s+(\S+)$/
|
||||
path_locale = $1.strip
|
||||
|
@ -247,36 +248,28 @@ module Redmine
|
|||
end
|
||||
|
||||
def self.branch_conf_path(path)
|
||||
bcp = nil
|
||||
return if path.nil?
|
||||
m = path.match(%r{^(.*[/\\])\.bzr.*$})
|
||||
if m
|
||||
bcp = m[1]
|
||||
else
|
||||
bcp = path
|
||||
end
|
||||
bcp.gsub!(%r{[\/\\]$}, "")
|
||||
if bcp
|
||||
bcp = File.join(bcp, ".bzr", "branch", "branch.conf")
|
||||
end
|
||||
bcp
|
||||
bcp = (m ? m[1] : path).gsub(%r{[\/\\]$}, "")
|
||||
File.join(bcp, ".bzr", "branch", "branch.conf")
|
||||
end
|
||||
|
||||
def append_revisions_only
|
||||
return @aro if ! @aro.nil?
|
||||
return @aro unless @aro.nil?
|
||||
@aro = false
|
||||
bcp = self.class.branch_conf_path(url)
|
||||
if bcp && File.exist?(bcp)
|
||||
begin
|
||||
f = File::open(bcp, "r")
|
||||
f = File.open(bcp, "r")
|
||||
cnt = 0
|
||||
f.each_line do |line|
|
||||
l = line.chomp.to_s
|
||||
if l =~ /^\s*append_revisions_only\s*=\s*(\w+)\s*$/
|
||||
str_aro = $1
|
||||
if str_aro.upcase == "TRUE"
|
||||
if str_aro.casecmp("TRUE") == 0
|
||||
@aro = true
|
||||
cnt += 1
|
||||
elsif str_aro.upcase == "FALSE"
|
||||
elsif str_aro.casecmp("FALSE") == 0
|
||||
@aro = false
|
||||
cnt += 1
|
||||
end
|
||||
|
@ -301,7 +294,7 @@ module Redmine
|
|||
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
|
||||
end
|
||||
ret = shellout(
|
||||
self.class.sq_bin + ' ' +
|
||||
self.class.sq_bin + ' ' +
|
||||
full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
|
||||
&block
|
||||
)
|
||||
|
@ -320,7 +313,7 @@ module Redmine
|
|||
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
|
||||
end
|
||||
ret = shellout(
|
||||
self.class.sq_bin + ' ' +
|
||||
self.class.sq_bin + ' ' +
|
||||
full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
|
||||
&block
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -18,7 +20,8 @@
|
|||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class CommandFailed < StandardError #:nodoc:
|
||||
# @private
|
||||
class CommandFailed < StandardError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -43,7 +45,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def scm_command_version
|
||||
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
|
||||
scm_version = scm_version_from_command_line.b
|
||||
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
|
||||
m[2].scan(%r{\d+}).collect(&:to_i)
|
||||
end
|
||||
|
@ -61,7 +63,7 @@ module Redmine
|
|||
# password -> unnecessary too
|
||||
def initialize(url, root_url=nil, login=nil, password=nil,
|
||||
path_encoding=nil)
|
||||
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
|
||||
@path_encoding = path_encoding.presence || 'UTF-8'
|
||||
@url = url
|
||||
# TODO: better Exception here (IllegalArgumentException)
|
||||
raise CommandFailed if root_url.blank?
|
||||
|
@ -91,7 +93,7 @@ module Redmine
|
|||
def entries(path=nil, identifier=nil, options={})
|
||||
logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
|
||||
path_locale = scm_iconv(@path_encoding, 'UTF-8', path)
|
||||
path_locale.force_encoding("ASCII-8BIT")
|
||||
path_locale = path_locale.b
|
||||
entries = Entries.new
|
||||
cmd_args = %w|-q rls -e|
|
||||
cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
|
||||
|
@ -159,7 +161,7 @@ module Redmine
|
|||
cmd_args << path_with_project_utf8
|
||||
scm_cmd(*cmd_args) do |io|
|
||||
state = "entry_start"
|
||||
commit_log = String.new
|
||||
commit_log = ""
|
||||
revision = nil
|
||||
date = nil
|
||||
author = nil
|
||||
|
@ -168,23 +170,23 @@ module Redmine
|
|||
file_state = nil
|
||||
branch_map = nil
|
||||
io.each_line() do |line|
|
||||
if state != "revision" && /^#{ENDLOG}/ =~ line
|
||||
commit_log = String.new
|
||||
if state != "revision" && /^#{ENDLOG}/.match?(line)
|
||||
commit_log = ""
|
||||
revision = nil
|
||||
state = "entry_start"
|
||||
end
|
||||
if state == "entry_start"
|
||||
branch_map = Hash.new
|
||||
branch_map = {}
|
||||
if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),v$/ =~ line
|
||||
entry_path = normalize_cvs_path($1)
|
||||
entry_name = normalize_path(File.basename($1))
|
||||
logger.debug("Path #{entry_path} <=> Name #{entry_name}")
|
||||
elsif /^head: (.+)$/ =~ line
|
||||
entry_headRev = $1 #unless entry.nil?
|
||||
elsif /^symbolic names:/ =~ line
|
||||
state = "symbolic" #unless entry.nil?
|
||||
elsif /^#{STARTLOG}/ =~ line
|
||||
commit_log = String.new
|
||||
entry_headRev = $1
|
||||
elsif /^symbolic names:/.match?(line)
|
||||
state = "symbolic"
|
||||
elsif /^#{STARTLOG}/.match?(line)
|
||||
commit_log = ""
|
||||
state = "revision"
|
||||
end
|
||||
next
|
||||
|
@ -228,7 +230,7 @@ module Redmine
|
|||
}]
|
||||
})
|
||||
end
|
||||
commit_log = String.new
|
||||
commit_log = ""
|
||||
revision = nil
|
||||
if /^#{ENDLOG}/ =~ line
|
||||
state = "entry_start"
|
||||
|
@ -241,15 +243,14 @@ module Redmine
|
|||
elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
|
||||
revision = $1
|
||||
elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
|
||||
date = Time.parse($1)
|
||||
date = Time.parse($1)
|
||||
line_utf8 = scm_iconv('UTF-8', options[:log_encoding], line)
|
||||
author_utf8 = /author: ([^;]+)/.match(line_utf8)[1]
|
||||
author = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8)
|
||||
file_state = /state: ([^;]+)/.match(line)[1]
|
||||
# TODO:
|
||||
# linechanges only available in CVS....
|
||||
# maybe a feature our SVN implementation.
|
||||
# I'm sure, they are useful for stats or something else
|
||||
# TODO: linechanges only available in CVS....
|
||||
# maybe a feature our SVN implementation.
|
||||
# I'm sure, they are useful for stats or something else
|
||||
# linechanges =/lines: \+(\d+) -(\d+)/.match(line)
|
||||
# unless linechanges.nil?
|
||||
# version.line_plus = linechanges[1]
|
||||
|
@ -259,7 +260,7 @@ module Redmine
|
|||
# version.line_minus = 0
|
||||
# end
|
||||
else
|
||||
commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
|
||||
commit_log += line unless line =~ /^\*\*\* empty log message \*\*\*/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -338,9 +339,8 @@ module Redmine
|
|||
# convert a date/time into the CVS-format
|
||||
def time_to_cvstime(time)
|
||||
return nil if time.nil?
|
||||
time = Time.now if (time.kind_of?(String) && time == 'HEAD')
|
||||
|
||||
unless time.kind_of? Time
|
||||
time = Time.now if (time.is_a?(String) && time == 'HEAD')
|
||||
unless time.is_a?(Time)
|
||||
time = Time.parse(time)
|
||||
end
|
||||
return time_to_cvstime_rlog(time)
|
||||
|
@ -399,10 +399,6 @@ module Redmine
|
|||
parseRevision()
|
||||
end
|
||||
|
||||
def branchPoint
|
||||
return @base
|
||||
end
|
||||
|
||||
def branchVersion
|
||||
if isBranchRevision
|
||||
return @base+"."+@branchid
|
||||
|
@ -428,6 +424,7 @@ module Redmine
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
def buildRevision(rev)
|
||||
if rev == 0
|
||||
if @branchid.nil?
|
||||
|
@ -443,7 +440,7 @@ module Redmine
|
|||
end
|
||||
|
||||
# Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
|
||||
def parseRevision()
|
||||
def parseRevision
|
||||
pieces = @complete_rev.split(".")
|
||||
@revision = pieces.last.to_i
|
||||
baseSize = 1
|
||||
|
|
|
@ -1,239 +0,0 @@
|
|||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/abstract_adapter'
|
||||
require 'rexml/document'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class DarcsAdapter < AbstractAdapter
|
||||
# Darcs executable name
|
||||
DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
|
||||
|
||||
class << self
|
||||
def client_command
|
||||
@@bin ||= DARCS_BIN
|
||||
end
|
||||
|
||||
def sq_bin
|
||||
@@sq_bin ||= shell_quote_command
|
||||
end
|
||||
|
||||
def client_version
|
||||
@@client_version ||= (darcs_binary_version || [])
|
||||
end
|
||||
|
||||
def client_available
|
||||
!client_version.empty?
|
||||
end
|
||||
|
||||
def darcs_binary_version
|
||||
darcsversion = darcs_binary_version_from_command_line.dup.force_encoding('ASCII-8BIT')
|
||||
if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||||
m[2].scan(%r{\d+}).collect(&:to_i)
|
||||
end
|
||||
end
|
||||
|
||||
def darcs_binary_version_from_command_line
|
||||
shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(url, root_url=nil, login=nil, password=nil,
|
||||
path_encoding=nil)
|
||||
@url = url
|
||||
@root_url = url
|
||||
end
|
||||
|
||||
def supports_cat?
|
||||
# cat supported in darcs 2.0.0 and higher
|
||||
self.class.client_version_above?([2, 0, 0])
|
||||
end
|
||||
|
||||
# Get info about the darcs repository
|
||||
def info
|
||||
rev = revisions(nil,nil,nil,{:limit => 1})
|
||||
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
|
||||
end
|
||||
|
||||
# Returns an Entries collection
|
||||
# or nil if the given path doesn't exist in the repository
|
||||
def entries(path=nil, identifier=nil, options={})
|
||||
path_prefix = (path.blank? ? '' : "#{path}/")
|
||||
if path.blank?
|
||||
path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
|
||||
end
|
||||
entries = Entries.new
|
||||
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
|
||||
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
|
||||
cmd << " #{shell_quote path}"
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
doc = REXML::Document.new(io)
|
||||
if doc.root.name == 'directory'
|
||||
doc.elements.each('directory/*') do |element|
|
||||
next unless ['file', 'directory'].include? element.name
|
||||
entries << entry_from_xml(element, path_prefix)
|
||||
end
|
||||
elsif doc.root.name == 'file'
|
||||
entries << entry_from_xml(doc.root, path_prefix)
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
entries.compact!
|
||||
entries.sort_by_name
|
||||
end
|
||||
|
||||
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
||||
path = '.' if path.blank?
|
||||
revisions = Revisions.new
|
||||
cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
|
||||
cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
|
||||
cmd << " --last #{options[:limit].to_i}" if options[:limit]
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
doc = REXML::Document.new(io)
|
||||
doc.elements.each("changelog/patch") do |patch|
|
||||
message = patch.elements['name'].text
|
||||
message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
|
||||
revisions << Revision.new({:identifier => nil,
|
||||
:author => patch.attributes['author'],
|
||||
:scmid => patch.attributes['hash'],
|
||||
:time => Time.parse(patch.attributes['local_date']),
|
||||
:message => message,
|
||||
:paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
|
||||
})
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
revisions
|
||||
end
|
||||
|
||||
def diff(path, identifier_from, identifier_to=nil)
|
||||
path = '*' if path.blank?
|
||||
cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
|
||||
if identifier_to.nil?
|
||||
cmd << " --match #{shell_quote("hash #{identifier_from}")}"
|
||||
else
|
||||
cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
|
||||
cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
|
||||
end
|
||||
cmd << " -u #{shell_quote path}"
|
||||
diff = []
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
diff
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
|
||||
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
|
||||
cmd << " #{shell_quote path}"
|
||||
cat = nil
|
||||
shellout(cmd) do |io|
|
||||
io.binmode
|
||||
cat = io.read
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
cat
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns an Entry from the given XML element
|
||||
# or nil if the entry was deleted
|
||||
def entry_from_xml(element, path_prefix)
|
||||
modified_element = element.elements['modified']
|
||||
if modified_element.elements['modified_how'].text.match(/removed/)
|
||||
return nil
|
||||
end
|
||||
|
||||
Entry.new({:name => element.attributes['name'],
|
||||
:path => path_prefix + element.attributes['name'],
|
||||
:kind => element.name == 'file' ? 'file' : 'dir',
|
||||
:size => nil,
|
||||
:lastrev => Revision.new({
|
||||
:identifier => nil,
|
||||
:scmid => modified_element.elements['patch'].attributes['hash']
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
def get_paths_for_patch(hash)
|
||||
paths = get_paths_for_patch_raw(hash)
|
||||
if self.class.client_version_above?([2, 4])
|
||||
orig_paths = paths
|
||||
paths = []
|
||||
add_paths = []
|
||||
add_paths_name = []
|
||||
mod_paths = []
|
||||
other_paths = []
|
||||
orig_paths.each do |path|
|
||||
if path[:action] == 'A'
|
||||
add_paths << path
|
||||
add_paths_name << path[:path]
|
||||
elsif path[:action] == 'M'
|
||||
mod_paths << path
|
||||
else
|
||||
other_paths << path
|
||||
end
|
||||
end
|
||||
add_paths_name.each do |add_path|
|
||||
mod_paths.delete_if { |m| m[:path] == add_path }
|
||||
end
|
||||
paths.concat add_paths
|
||||
paths.concat mod_paths
|
||||
paths.concat other_paths
|
||||
end
|
||||
paths
|
||||
end
|
||||
|
||||
# Retrieve changed paths for a single patch
|
||||
def get_paths_for_patch_raw(hash)
|
||||
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
|
||||
cmd << " --match #{shell_quote("hash #{hash}")} "
|
||||
paths = []
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
# Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
|
||||
# A root element is added so that REXML doesn't raise an error
|
||||
doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
|
||||
doc.elements.each('fake_root/summary/*') do |modif|
|
||||
paths << {:action => modif.name[0,1].upcase,
|
||||
:path => "/" + modif.text.chomp.gsub(/^\s*/, '')
|
||||
}
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
paths
|
||||
rescue CommandFailed
|
||||
paths
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# FileSystem adapter
|
||||
# File written by Paul Rivier, at Demotera.
|
||||
|
@ -107,7 +109,7 @@ module Redmine
|
|||
# Here we do not shell-out, so we do not want quotes.
|
||||
def target(path=nil)
|
||||
# Prevent the use of ..
|
||||
if path and !path.match(/(^|\/)\.\.(\/|$)/)
|
||||
if path and !/(^|\/)\.\.(\/|$)/.match?(path)
|
||||
return "#{self.url}#{without_leading_slash(path)}"
|
||||
end
|
||||
return self.url
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -47,7 +49,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def scm_command_version
|
||||
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
|
||||
scm_version = scm_version_from_command_line.b
|
||||
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||||
m[2].scan(%r{\d+}).collect(&:to_i)
|
||||
end
|
||||
|
@ -68,11 +70,9 @@ module Redmine
|
|||
end
|
||||
|
||||
def info
|
||||
begin
|
||||
Info.new(:root_url => url, :lastrev => lastrev('',nil))
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
Info.new(:root_url => url, :lastrev => lastrev('',nil))
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def branches
|
||||
|
@ -82,8 +82,8 @@ module Redmine
|
|||
git_cmd(cmd_args) do |io|
|
||||
io.each_line do |line|
|
||||
branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
|
||||
bran = GitBranch.new(branch_rev[2])
|
||||
next unless branch_rev
|
||||
bran = GitBranch.new(scm_iconv('UTF-8', @path_encoding, branch_rev[2]))
|
||||
bran.revision = branch_rev[3]
|
||||
bran.scmid = branch_rev[3]
|
||||
bran.is_default = ( branch_rev[1] == '*' )
|
||||
|
@ -100,7 +100,7 @@ module Redmine
|
|||
@tags = []
|
||||
cmd_args = %w|tag|
|
||||
git_cmd(cmd_args) do |io|
|
||||
@tags = io.readlines.sort!.map{|t| t.strip}
|
||||
@tags = io.readlines.sort!.map{|t| scm_iconv('UTF-8', @path_encoding, t.strip)}
|
||||
end
|
||||
@tags
|
||||
rescue ScmCommandAborted
|
||||
|
@ -109,11 +109,11 @@ module Redmine
|
|||
|
||||
def default_branch
|
||||
bras = self.branches
|
||||
return nil if bras.nil?
|
||||
default_bras = bras.select{|x| x.is_default == true}
|
||||
return default_bras.first.to_s if ! default_bras.empty?
|
||||
master_bras = bras.select{|x| x.to_s == 'master'}
|
||||
master_bras.empty? ? bras.first.to_s : 'master'
|
||||
return unless bras
|
||||
default_bras = bras.detect{|x| x.is_default == true}
|
||||
return default_bras.to_s if default_bras
|
||||
master_bras = bras.detect{|x| x.to_s == 'master'}
|
||||
master_bras ? 'master' : bras.first.to_s
|
||||
end
|
||||
|
||||
def entry(path=nil, identifier=nil)
|
||||
|
@ -136,8 +136,9 @@ module Redmine
|
|||
p = scm_iconv(@path_encoding, 'UTF-8', path)
|
||||
entries = Entries.new
|
||||
cmd_args = %w|ls-tree -l|
|
||||
cmd_args << "HEAD:#{p}" if identifier.nil?
|
||||
cmd_args << "#{identifier}:#{p}" if identifier
|
||||
identifier = 'HEAD' if identifier.nil?
|
||||
git_identifier = scm_iconv(@path_encoding, 'UTF-8', identifier)
|
||||
cmd_args << "#{git_identifier}:#{p}"
|
||||
git_cmd(cmd_args) do |io|
|
||||
io.each_line do |line|
|
||||
e = line.chomp.to_s
|
||||
|
@ -198,13 +199,19 @@ module Redmine
|
|||
cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
|
||||
cmd_args << '--no-renames' if self.class.client_version_above?([2, 9])
|
||||
cmd_args << "--reverse" if options[:reverse]
|
||||
cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
|
||||
cmd_args << "-n" << options[:limit].to_i.to_s if options[:limit]
|
||||
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
|
||||
revisions = []
|
||||
if identifier_from || identifier_to
|
||||
revisions << ""
|
||||
revisions[0] << "#{identifier_from}.." if identifier_from
|
||||
revisions[0] << "#{identifier_to}" if identifier_to
|
||||
revisions << +""
|
||||
if identifier_from
|
||||
git_identifier_from = scm_iconv(@path_encoding, 'UTF-8', identifier_from)
|
||||
revisions[0] << "#{git_identifier_from}.." if identifier_from
|
||||
end
|
||||
if identifier_to
|
||||
git_identifier_to = scm_iconv(@path_encoding, 'UTF-8', identifier_to)
|
||||
revisions[0] << git_identifier_to.to_s if identifier_to
|
||||
end
|
||||
else
|
||||
unless options[:includes].blank?
|
||||
revisions += options[:includes]
|
||||
|
@ -220,14 +227,15 @@ module Redmine
|
|||
io.close_write
|
||||
files=[]
|
||||
changeset = {}
|
||||
parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
|
||||
# 0: not parsing desc or files, 1: parsing desc, 2: parsing files
|
||||
parsing_descr = 0
|
||||
|
||||
io.each_line do |line|
|
||||
if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
|
||||
key = "commit"
|
||||
value = $1
|
||||
parents_str = $2
|
||||
if (parsing_descr == 1 || parsing_descr == 2)
|
||||
if [1, 2].include?(parsing_descr)
|
||||
parsing_descr = 0
|
||||
revision = Revision.new({
|
||||
:identifier => changeset[:commit],
|
||||
|
@ -260,16 +268,16 @@ module Redmine
|
|||
end
|
||||
elsif (parsing_descr == 0) && line.chomp.to_s == ""
|
||||
parsing_descr = 1
|
||||
changeset[:description] = ""
|
||||
elsif (parsing_descr == 1 || parsing_descr == 2) \
|
||||
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
|
||||
changeset[:description] = +""
|
||||
elsif [1, 2].include?(parsing_descr) &&
|
||||
line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
|
||||
parsing_descr = 2
|
||||
fileaction = $1
|
||||
filepath = $2
|
||||
p = scm_iconv('UTF-8', @path_encoding, filepath)
|
||||
files << {:action => fileaction, :path => p}
|
||||
elsif (parsing_descr == 1 || parsing_descr == 2) \
|
||||
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
|
||||
elsif [1, 2].include?(parsing_descr) &&
|
||||
line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
|
||||
parsing_descr = 2
|
||||
fileaction = $1
|
||||
filepath = $3
|
||||
|
@ -333,8 +341,9 @@ module Redmine
|
|||
|
||||
def annotate(path, identifier=nil)
|
||||
identifier = 'HEAD' if identifier.blank?
|
||||
git_identifier = scm_iconv(@path_encoding, 'UTF-8', identifier)
|
||||
cmd_args = %w|blame --encoding=UTF-8|
|
||||
cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
|
||||
cmd_args << "-p" << git_identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
|
||||
blame = Annotate.new
|
||||
content = nil
|
||||
git_cmd(cmd_args) { |io| io.binmode; content = io.read }
|
||||
|
@ -365,11 +374,10 @@ module Redmine
|
|||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
if identifier.nil?
|
||||
identifier = 'HEAD'
|
||||
end
|
||||
identifier = 'HEAD' if identifier.nil?
|
||||
git_identifier = scm_iconv(@path_encoding, 'UTF-8', identifier)
|
||||
cmd_args = %w|show --no-color|
|
||||
cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
|
||||
cmd_args << "#{git_identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
|
||||
cat = nil
|
||||
git_cmd(cmd_args) do |io|
|
||||
io.binmode
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -56,7 +58,7 @@ module Redmine
|
|||
# The hg version is expressed either as a
|
||||
# release number (eg 0.9.5 or 1.0) or as a revision
|
||||
# id composed of 12 hexa characters.
|
||||
theversion = hgversion_from_command_line.dup.force_encoding('ASCII-8BIT')
|
||||
theversion = hgversion_from_command_line.b
|
||||
if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||||
m[2].scan(%r{\d+}).collect(&:to_i)
|
||||
end
|
||||
|
@ -90,7 +92,7 @@ module Redmine
|
|||
:lastrev => Revision.new(:revision => tip['revision'],
|
||||
:scmid => tip['node']))
|
||||
# rescue HgCommandAborted
|
||||
rescue Exception => e
|
||||
rescue => e
|
||||
logger.error "hg: error during getting info: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
@ -133,6 +135,7 @@ module Redmine
|
|||
begin
|
||||
@summary = parse_xml(output)['rhsummary']
|
||||
rescue
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -146,6 +149,7 @@ module Redmine
|
|||
begin
|
||||
parse_xml(output)['rhmanifest']['repository']['manifest']
|
||||
rescue
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
path_prefix = path.blank? ? '' : with_trailling_slash(path)
|
||||
|
@ -191,6 +195,7 @@ module Redmine
|
|||
# Mercurial < 1.5 does not support footer template for '</log>'
|
||||
parse_xml("#{output}</log>")['log']
|
||||
rescue
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
as_ary(log['logentry']).each do |le|
|
||||
|
@ -201,19 +206,25 @@ module Redmine
|
|||
end
|
||||
cpmap = Hash[*cpalist.flatten]
|
||||
paths = as_ary(le['paths']['path']).map do |e|
|
||||
p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
|
||||
p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']))
|
||||
{:action => e['action'],
|
||||
:path => with_leading_slash(p),
|
||||
:from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
|
||||
:from_revision => (cpmap.member?(p) ? le['node'] : nil)}
|
||||
end.sort { |a, b| a[:path] <=> b[:path] }
|
||||
end
|
||||
paths.sort_by!{|e| e[:path]}
|
||||
parents_ary = []
|
||||
as_ary(le['parents']['parent']).map do |par|
|
||||
parents_ary << par['__content__'] if par['__content__'] != "0000000000000000000000000000000000000000"
|
||||
end
|
||||
yield Revision.new(:revision => le['revision'],
|
||||
:scmid => le['node'],
|
||||
:author => (le['author']['__content__'] rescue ''),
|
||||
:author =>
|
||||
(begin
|
||||
le['author']['__content__']
|
||||
rescue
|
||||
''
|
||||
end),
|
||||
:time => Time.parse(le['date']['__content__']),
|
||||
:message => le['msg']['__content__'],
|
||||
:paths => paths,
|
||||
|
@ -243,7 +254,7 @@ module Redmine
|
|||
hg_args << '--' << CGI.escape(hgtarget(p))
|
||||
end
|
||||
diff = []
|
||||
hg *hg_args do |io|
|
||||
hg(*hg_args) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
|
@ -268,8 +279,7 @@ module Redmine
|
|||
blame = Annotate.new
|
||||
hg 'rhannotate', '-ncu', "-r#{CGI.escape(hgrev(identifier))}", '--', hgtarget(p) do |io|
|
||||
io.each_line do |line|
|
||||
line.force_encoding('ASCII-8BIT')
|
||||
next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
|
||||
next unless line.b =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
|
||||
r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
|
||||
:identifier => $3)
|
||||
blame.add_line($4.rstrip, r)
|
||||
|
@ -296,10 +306,10 @@ module Redmine
|
|||
# Runs 'hg' command with the given args
|
||||
def hg(*args, &block)
|
||||
# as of hg 4.4.1, early parsing of bool options is not terminated at '--'
|
||||
if args.any? { |s| s =~ HG_EARLY_BOOL_ARG }
|
||||
if args.any? { |s| HG_EARLY_BOOL_ARG.match?(s) }
|
||||
raise HgCommandArgumentError, "malicious command argument detected"
|
||||
end
|
||||
if args.take_while { |s| s != '--' }.any? { |s| s =~ HG_EARLY_LIST_ARG }
|
||||
if args.take_while { |s| s != '--' }.any? { |s| HG_EARLY_LIST_ARG.match?(s) }
|
||||
raise HgCommandArgumentError, "malicious command argument detected"
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -46,7 +48,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def svn_binary_version
|
||||
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
|
||||
scm_version = scm_version_from_command_line.b
|
||||
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||||
m[2].scan(%r{\d+}).collect(&:to_i)
|
||||
end
|
||||
|
@ -59,7 +61,7 @@ module Redmine
|
|||
|
||||
# Get info about the svn repository
|
||||
def info
|
||||
cmd = "#{self.class.sq_bin} info --xml #{target}"
|
||||
cmd = +"#{self.class.sq_bin} info --xml #{target}"
|
||||
cmd << credentials_string
|
||||
info = nil
|
||||
shellout(cmd) do |io|
|
||||
|
@ -89,7 +91,7 @@ module Redmine
|
|||
path ||= ''
|
||||
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
||||
entries = Entries.new
|
||||
cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
|
||||
cmd = +"#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
|
||||
cmd << credentials_string
|
||||
shellout(cmd) do |io|
|
||||
output = io.read.force_encoding('UTF-8')
|
||||
|
@ -113,7 +115,7 @@ module Redmine
|
|||
})
|
||||
})
|
||||
end
|
||||
rescue Exception => e
|
||||
rescue => e
|
||||
logger.error("Error parsing svn output: #{e.message}")
|
||||
logger.error("Output was:\n #{output}")
|
||||
end
|
||||
|
@ -128,7 +130,7 @@ module Redmine
|
|||
return nil unless self.class.client_version_above?([1, 5, 0])
|
||||
|
||||
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
||||
cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
|
||||
cmd = +"#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
|
||||
cmd << credentials_string
|
||||
properties = {}
|
||||
shellout(cmd) do |io|
|
||||
|
@ -136,7 +138,7 @@ module Redmine
|
|||
begin
|
||||
doc = parse_xml(output)
|
||||
each_xml_element(doc['properties']['target'], 'property') do |property|
|
||||
properties[ property['name'] ] = property['__content__'].to_s
|
||||
properties[property['name']] = property['__content__'].to_s
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
@ -150,7 +152,7 @@ module Redmine
|
|||
identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
|
||||
identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
|
||||
revisions = Revisions.new
|
||||
cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
|
||||
cmd = +"#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
|
||||
cmd << credentials_string
|
||||
cmd << " --verbose " if options[:with_paths]
|
||||
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
|
||||
|
@ -168,7 +170,7 @@ module Redmine
|
|||
:from_revision => path['copyfrom-rev']
|
||||
}
|
||||
end if logentry['paths'] && logentry['paths']['path']
|
||||
paths.sort! { |x,y| x[:path] <=> y[:path] }
|
||||
paths.sort_by! {|e| e[:path]}
|
||||
|
||||
revisions << Revision.new({:identifier => logentry['revision'],
|
||||
:author => (logentry['author'] ? logentry['author']['__content__'] : ""),
|
||||
|
@ -190,7 +192,7 @@ module Redmine
|
|||
|
||||
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
|
||||
|
||||
cmd = "#{self.class.sq_bin} diff -r "
|
||||
cmd = +"#{self.class.sq_bin} diff -r "
|
||||
cmd << "#{identifier_to}:"
|
||||
cmd << "#{identifier_from}"
|
||||
cmd << " #{target(path)}@#{identifier_from}"
|
||||
|
@ -207,7 +209,7 @@ module Redmine
|
|||
|
||||
def cat(path, identifier=nil)
|
||||
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
||||
cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
|
||||
cmd = +"#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
|
||||
cmd << credentials_string
|
||||
cat = nil
|
||||
shellout(cmd) do |io|
|
||||
|
@ -220,7 +222,7 @@ module Redmine
|
|||
|
||||
def annotate(path, identifier=nil)
|
||||
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
||||
cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
|
||||
cmd = +"#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
|
||||
cmd << credentials_string
|
||||
blame = Annotate.new
|
||||
shellout(cmd) do |io|
|
||||
|
@ -242,7 +244,7 @@ module Redmine
|
|||
private
|
||||
|
||||
def credentials_string
|
||||
str = ''
|
||||
str = +''
|
||||
str << " --username #{shell_quote(@login)}" unless @login.blank?
|
||||
str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
|
||||
str << " --no-auth-cache --non-interactive"
|
||||
|
@ -265,7 +267,7 @@ module Redmine
|
|||
end
|
||||
|
||||
def target(path = '')
|
||||
base = path.match(/^\//) ? root_url : url
|
||||
base = /^\//.match?(path) ? root_url : url
|
||||
uri = "#{base}/#{path}"
|
||||
uri = URI.escape(URI.escape(uri), '[]')
|
||||
shell_quote(uri.gsub(/[?<>\*]/, ''))
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
class Base
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -60,7 +62,8 @@ module Redmine
|
|||
# eg. hello "bye bye" => ["hello", "bye bye"]
|
||||
@tokens = @question.scan(%r{((\s|^)"[^"]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
|
||||
# tokens must be at least 2 characters long
|
||||
@tokens = @tokens.uniq.select {|w| w.length > 1 }
|
||||
# but for Chinese characters (Chinese HANZI/Japanese KANJI), tokens can be one character
|
||||
@tokens = @tokens.uniq.select {|w| w.length > 1 || w =~ /\p{Han}/ }
|
||||
# no more than 5 tokens to search for
|
||||
@tokens.slice! 5..-1
|
||||
end
|
||||
|
@ -82,13 +85,11 @@ module Redmine
|
|||
# Returns the results for the given offset and limit
|
||||
def results(offset, limit)
|
||||
result_ids_to_load = result_ids[offset, limit] || []
|
||||
|
||||
results_by_scope = Hash.new {|h,k| h[k] = []}
|
||||
result_ids_to_load.group_by(&:first).each do |scope, scope_and_ids|
|
||||
klass = scope.singularize.camelcase.constantize
|
||||
results_by_scope[scope] += klass.search_results_from_ids(scope_and_ids.map(&:last))
|
||||
end
|
||||
|
||||
result_ids_to_load.map do |scope, id|
|
||||
results_by_scope[scope].detect {|record| record.id == id}
|
||||
end.compact
|
||||
|
@ -110,7 +111,6 @@ module Redmine
|
|||
cache_key = ActiveSupport::Cache.expand_cache_key(
|
||||
[@question, @user.id, @scope.sort, @options, project_ids.sort]
|
||||
)
|
||||
|
||||
Redmine::Search.cache_store.fetch(cache_key, :force => !@cache) do
|
||||
load_result_ids
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -88,17 +90,17 @@ module Redmine
|
|||
|
||||
def normalize!
|
||||
self.reject! {|s| s.first.blank? }
|
||||
self.uniq! {|s| s.first }
|
||||
self.collect! {|s| s = Array(s); [s.first, (s.last == false || s.last.to_s == 'desc') ? 'desc' : 'asc']}
|
||||
self.slice!(3)
|
||||
self
|
||||
self.replace self.first(3)
|
||||
end
|
||||
|
||||
# Appends ASC/DESC to the sort criterion unless it has a fixed order
|
||||
def append_order(criterion, order)
|
||||
if criterion =~ / (asc|desc)$/i
|
||||
if / (asc|desc)$/i.match?(criterion)
|
||||
criterion
|
||||
else
|
||||
"#{criterion} #{order.to_s.upcase}"
|
||||
Arel.sql "#{criterion} #{order.to_s.upcase}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -17,9 +19,9 @@
|
|||
|
||||
module Redmine
|
||||
module SubclassFactory
|
||||
def self.included(base)
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def get_subclass(class_name)
|
||||
|
@ -29,7 +31,7 @@ module Redmine
|
|||
rescue
|
||||
# invalid class name
|
||||
end
|
||||
unless subclasses.include? klass
|
||||
unless descendants.include? klass
|
||||
klass = nil
|
||||
end
|
||||
klass
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'active_support/core_ext/object/to_query'
|
||||
require 'rack/utils'
|
||||
|
||||
|
@ -31,7 +33,7 @@ module Redmine
|
|||
#
|
||||
# taken from https://github.com/brianhempel/hash_to_hidden_fields
|
||||
def hash_to_hidden_fields(hash)
|
||||
cleaned_hash = hash.reject { |k, v| v.nil? }
|
||||
cleaned_hash = hash.to_unsafe_h.reject { |k, v| v.nil? }
|
||||
pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP)
|
||||
tags = pairs.map do |pair|
|
||||
key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -50,44 +52,87 @@ module Redmine
|
|||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
def filename_supported?(filename)
|
||||
if highlighter.respond_to? :filename_supported?
|
||||
highlighter.filename_supported? filename
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module CodeRay
|
||||
require 'coderay'
|
||||
module Rouge
|
||||
require 'rouge'
|
||||
|
||||
def self.retrieve_supported_languages
|
||||
::CodeRay::Scanners.list +
|
||||
# Add CodeRay scanner aliases
|
||||
::CodeRay::Scanners.plugin_hash.keys.map(&:to_sym) -
|
||||
# Remove internal CodeRay scanners
|
||||
%w(debug default raydebug scanner).map(&:to_sym)
|
||||
# Customized formatter based on Rouge::Formatters::HTMLLinewise
|
||||
# Syntax highlighting is completed within each line.
|
||||
class CustomHTMLLinewise < ::Rouge::Formatter
|
||||
def initialize(formatter)
|
||||
@formatter = formatter
|
||||
end
|
||||
|
||||
def stream(tokens, &b)
|
||||
token_lines(tokens) do |line|
|
||||
line.each do |tok, val|
|
||||
yield @formatter.span(tok, val)
|
||||
end
|
||||
yield "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
private_class_method :retrieve_supported_languages
|
||||
|
||||
SUPPORTED_LANGUAGES = retrieve_supported_languages
|
||||
|
||||
class << self
|
||||
# Highlights +text+ as the content of +filename+
|
||||
# Should not return line numbers nor outer pre tag
|
||||
def highlight_by_filename(text, filename)
|
||||
language = ::CodeRay::FileType[filename]
|
||||
language ? ::CodeRay.scan(text, language).html(:break_lines => true) : ERB::Util.h(text)
|
||||
# TODO: Delete the following workaround for #30434 and
|
||||
# test_syntax_highlight_should_normalize_line_endings in
|
||||
# application_helper_test.rb when Rouge is improved to
|
||||
# handle CRLF properly.
|
||||
# See also: https://github.com/jneen/rouge/pull/1078
|
||||
text = text.gsub(/\r\n?/, "\n")
|
||||
|
||||
lexer =::Rouge::Lexer.guess(:source => text, :filename => filename)
|
||||
formatter = ::Rouge::Formatters::HTML.new
|
||||
::Rouge.highlight(text, lexer, CustomHTMLLinewise.new(formatter))
|
||||
end
|
||||
|
||||
# Highlights +text+ using +language+ syntax
|
||||
# Should not return outer pre tag
|
||||
def highlight_by_language(text, language)
|
||||
::CodeRay.scan(text, language).html(:wrap => :span)
|
||||
lexer =
|
||||
find_lexer(language.to_s.downcase) || ::Rouge::Lexers::PlainText
|
||||
::Rouge.highlight(text, lexer, ::Rouge::Formatters::HTML)
|
||||
end
|
||||
|
||||
def language_supported?(language)
|
||||
SUPPORTED_LANGUAGES.include?(language.to_s.downcase.to_sym)
|
||||
rescue
|
||||
false
|
||||
find_lexer(language.to_s.downcase) ? true : false
|
||||
end
|
||||
|
||||
def filename_supported?(filename)
|
||||
!::Rouge::Lexer.guesses(:filename => filename).empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Alias names used by CodeRay and not supported by Rouge
|
||||
LANG_ALIASES = {
|
||||
'delphi' => 'pascal',
|
||||
'cplusplus' => 'cpp',
|
||||
'ecmascript' => 'javascript',
|
||||
'ecma_script' => 'javascript',
|
||||
'java_script' => 'javascript',
|
||||
'xhtml' => 'html'
|
||||
}
|
||||
|
||||
def find_lexer(language)
|
||||
::Rouge::Lexer.find(language) ||
|
||||
::Rouge::Lexer.find(LANG_ALIASES[language])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SyntaxHighlighting.highlighter = 'CodeRay'
|
||||
SyntaxHighlighting.highlighter = 'Rouge'
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -121,7 +123,7 @@ module Redmine
|
|||
end
|
||||
@current_theme
|
||||
end
|
||||
|
||||
|
||||
# Returns the header tags for the current theme
|
||||
def heads_for_theme
|
||||
if current_theme && current_theme.javascripts.include?('theme')
|
||||
|
@ -130,8 +132,6 @@ module Redmine
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.scan_themes
|
||||
dirs = Dir.glob("#{Rails.public_path}/themes/*").select do |f|
|
||||
# A theme should at least override application.css
|
||||
|
@ -139,5 +139,6 @@ module Redmine
|
|||
end
|
||||
dirs.collect {|dir| Theme.new(dir)}.sort
|
||||
end
|
||||
private_class_method :scan_themes
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -23,12 +25,18 @@ module Redmine
|
|||
extend Redmine::Utils::Shell
|
||||
|
||||
CONVERT_BIN = (Redmine::Configuration['imagemagick_convert_command'] || 'convert').freeze
|
||||
ALLOWED_TYPES = %w(image/bmp image/gif image/jpeg image/png)
|
||||
ALLOWED_TYPES = %w(image/bmp image/gif image/jpeg image/png application/pdf)
|
||||
|
||||
# Generates a thumbnail for the source image to target
|
||||
def self.generate(source, target, size)
|
||||
def self.generate(source, target, size, is_pdf = false)
|
||||
return nil unless convert_available?
|
||||
return nil if is_pdf && !gs_available?
|
||||
unless File.exists?(target)
|
||||
mime_type = File.open(source) {|f| MimeMagic.by_magic(f).try(:type) }
|
||||
return nil if mime_type.nil?
|
||||
return nil if !ALLOWED_TYPES.include? mime_type
|
||||
return nil if is_pdf && mime_type != "application/pdf"
|
||||
|
||||
# Make sure we only invoke Imagemagick if the file type is allowed
|
||||
unless File.open(source) {|f| ALLOWED_TYPES.include? MimeMagic.by_magic(f).try(:type) }
|
||||
return nil
|
||||
|
@ -38,7 +46,12 @@ module Redmine
|
|||
FileUtils.mkdir_p directory
|
||||
end
|
||||
size_option = "#{size}x#{size}>"
|
||||
cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -thumbnail #{shell_quote size_option} #{shell_quote target}"
|
||||
|
||||
if is_pdf
|
||||
cmd = "#{shell_quote CONVERT_BIN} #{shell_quote "#{source}[0]"} -thumbnail #{shell_quote size_option} #{shell_quote "png:#{target}"}"
|
||||
else
|
||||
cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -auto-orient -thumbnail #{shell_quote size_option} #{shell_quote target}"
|
||||
end
|
||||
unless system(cmd)
|
||||
logger.error("Creating thumbnail failed (#{$?}):\nCommand: #{cmd}")
|
||||
return nil
|
||||
|
@ -59,6 +72,22 @@ module Redmine
|
|||
@convert_available
|
||||
end
|
||||
|
||||
def self.gs_available?
|
||||
return @gs_available if defined?(@gs_available)
|
||||
|
||||
if Redmine::Platform.mswin?
|
||||
@gs_available = false
|
||||
else
|
||||
begin
|
||||
`gs -version`
|
||||
@gs_available = $?.success?
|
||||
rescue
|
||||
@gs_available = false
|
||||
end
|
||||
end
|
||||
@gs_available
|
||||
end
|
||||
|
||||
def self.logger
|
||||
Rails.logger
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -49,7 +51,7 @@ module Redmine
|
|||
|
||||
# Class that represents a file diff
|
||||
class DiffTable < Array
|
||||
attr_reader :file_name
|
||||
attr_reader :file_name, :previous_file_name
|
||||
|
||||
# Initialize with a Diff file and the type of Diff View
|
||||
# The type view must be inline or sbs (side_by_side)
|
||||
|
@ -60,6 +62,7 @@ module Redmine
|
|||
@type = type
|
||||
@style = style
|
||||
@file_name = nil
|
||||
@previous_file_name = nil
|
||||
@git_diff = false
|
||||
end
|
||||
|
||||
|
@ -75,7 +78,7 @@ module Redmine
|
|||
@parsing = true
|
||||
end
|
||||
else
|
||||
if line =~ %r{^[^\+\-\s@\\]}
|
||||
if %r{^[^\+\-\s@\\]}.match?(line)
|
||||
@parsing = false
|
||||
return false
|
||||
elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
|
||||
|
@ -111,17 +114,21 @@ module Redmine
|
|||
def file_name=(arg)
|
||||
both_git_diff = false
|
||||
if file_name.nil?
|
||||
@git_diff = true if arg =~ %r{^(a/|/dev/null)}
|
||||
@git_diff = true if %r{^(a/|/dev/null)}.match?(arg)
|
||||
else
|
||||
both_git_diff = (@git_diff && arg =~ %r{^(b/|/dev/null)})
|
||||
both_git_diff = (@git_diff && %r{^(b/|/dev/null)}.match?(arg))
|
||||
end
|
||||
if both_git_diff
|
||||
if file_name && arg == "/dev/null"
|
||||
# keep the original file name
|
||||
@file_name = file_name.sub(%r{^a/}, '')
|
||||
else
|
||||
# remove leading a/
|
||||
@previous_file_name = file_name.sub(%r{^a/}, '') unless file_name == "/dev/null"
|
||||
# remove leading b/
|
||||
@file_name = arg.sub(%r{^b/}, '')
|
||||
|
||||
@previous_file_name = nil if @previous_file_name == @file_name
|
||||
end
|
||||
elsif @style == "Subversion"
|
||||
# removing trailing "(revision nn)"
|
||||
|
@ -161,7 +168,7 @@ module Redmine
|
|||
true
|
||||
else
|
||||
write_offsets
|
||||
if line[0, 1] =~ /\s/
|
||||
if /\s/.match?(line[0, 1])
|
||||
diff = Diff.new
|
||||
diff.line_right = line[1..-1]
|
||||
diff.nb_line_right = @line_num_r
|
||||
|
@ -196,11 +203,13 @@ module Redmine
|
|||
if line_left.present? && line_right.present? && line_left != line_right
|
||||
max = [line_left.size, line_right.size].min
|
||||
starting = 0
|
||||
while starting < max && line_left[starting] == line_right[starting]
|
||||
while starting < max &&
|
||||
line_left[starting] == line_right[starting]
|
||||
starting += 1
|
||||
end
|
||||
ending = -1
|
||||
while ending >= -(max - starting) && (line_left[ending] == line_right[ending])
|
||||
while ending >= -(max - starting) &&
|
||||
(line_left[ending] == line_right[ending])
|
||||
ending -= 1
|
||||
end
|
||||
unless starting == 0 && ending == -1
|
||||
|
@ -220,7 +229,7 @@ module Redmine
|
|||
attr_accessor :type_diff_left
|
||||
attr_accessor :offsets
|
||||
|
||||
def initialize()
|
||||
def initialize
|
||||
self.nb_line_left = ''
|
||||
self.nb_line_right = ''
|
||||
self.line_left = ''
|
||||
|
@ -267,7 +276,7 @@ module Redmine
|
|||
|
||||
def line_to_html_raw(line, offsets)
|
||||
if offsets
|
||||
s = ''
|
||||
s = +''
|
||||
unless offsets.first == 0
|
||||
s << CGI.escapeHTML(line[0..offsets.first-1])
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -22,9 +24,11 @@ module Redmine
|
|||
class << self
|
||||
# Returns the relative root url of the application
|
||||
def relative_url_root
|
||||
ActionController::Base.respond_to?('relative_url_root') ?
|
||||
ActionController::Base.relative_url_root.to_s :
|
||||
if ActionController::Base.respond_to?('relative_url_root')
|
||||
ActionController::Base.relative_url_root.to_s
|
||||
else
|
||||
ActionController::Base.config.relative_url_root.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the relative root url of the application
|
||||
|
@ -128,9 +132,7 @@ module Redmine
|
|||
def next_working_date(date)
|
||||
cwday = date.cwday
|
||||
days = 0
|
||||
while non_working_week_days.include?(((cwday + days - 1) % 7) + 1)
|
||||
days += 1
|
||||
end
|
||||
days += 1 while non_working_week_days.include?(((cwday + days - 1) % 7) + 1)
|
||||
date + days
|
||||
end
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
require 'rexml/document'
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'redmine/scm/adapters/subversion_adapter'
|
||||
|
||||
module Redmine
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 3
|
||||
MINOR = 4
|
||||
TINY = 13
|
||||
# @private
|
||||
module VERSION
|
||||
MAJOR = 4
|
||||
MINOR = 1
|
||||
TINY = 1
|
||||
|
||||
# Branch values:
|
||||
# * official release: nil
|
||||
|
@ -17,7 +20,7 @@ module Redmine
|
|||
if File.directory?(File.join(Rails.root, '.svn'))
|
||||
begin
|
||||
path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s)
|
||||
if `svn info --xml #{path}` =~ /revision="(\d+)"/
|
||||
if `#{Redmine::Scm::Adapters::SubversionAdapter.client_command} info --xml #{path}` =~ /commit\s+revision="(\d+)"/
|
||||
return $1.to_i
|
||||
end
|
||||
rescue
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -21,16 +23,20 @@ require 'redmine/views/builders/xml'
|
|||
module Redmine
|
||||
module Views
|
||||
module Builders
|
||||
def self.for(format, request, response, &block)
|
||||
builder = case format
|
||||
when 'xml', :xml; Builders::Xml.new(request, response)
|
||||
when 'json', :json; Builders::Json.new(request, response)
|
||||
else; raise "No builder for format #{format}"
|
||||
end
|
||||
if block
|
||||
block.call(builder)
|
||||
else
|
||||
builder
|
||||
class << self
|
||||
def for(format, request, response, &block)
|
||||
builder =
|
||||
case format
|
||||
when 'xml', :xml then Builders::Xml.new(request, response)
|
||||
when 'json', :json then Builders::Json.new(request, response)
|
||||
else
|
||||
raise "No builder for format #{format}"
|
||||
end
|
||||
if block_given?
|
||||
yield(builder)
|
||||
else
|
||||
builder
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -35,7 +37,7 @@ module Redmine
|
|||
json = @struct.first.to_json
|
||||
if jsonp.present?
|
||||
json = "#{jsonp}(#{json})"
|
||||
response.content_type = 'application/javascript'
|
||||
@response.content_type = 'application/javascript'
|
||||
end
|
||||
json
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -21,17 +23,16 @@ module Redmine
|
|||
module Views
|
||||
module Builders
|
||||
class Structure < BlankSlate
|
||||
attr_accessor :request, :response
|
||||
|
||||
def initialize(request, response)
|
||||
@struct = [{}]
|
||||
self.request = request
|
||||
self.response = response
|
||||
@request = request
|
||||
@response = response
|
||||
end
|
||||
|
||||
def array(tag, options={}, &block)
|
||||
@struct << []
|
||||
block.call(self)
|
||||
yield(self)
|
||||
ret = @struct.pop
|
||||
@struct.last[tag] = ret
|
||||
@struct.last.merge!(options) if options
|
||||
|
@ -68,10 +69,9 @@ module Redmine
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
if block
|
||||
if block_given?
|
||||
@struct << (args.first.is_a?(Hash) ? args.first : {})
|
||||
block.call(self)
|
||||
yield(self)
|
||||
ret = @struct.pop
|
||||
if @struct.last.is_a?(Array)
|
||||
@struct.last << ret
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -55,10 +57,10 @@ class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder
|
|||
def label_for_field(field, options = {})
|
||||
return ''.html_safe if options.delete(:no_label)
|
||||
text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label]
|
||||
text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym)
|
||||
text ||= @object.class.human_attribute_name(field)
|
||||
text += @template.content_tag("span", " *", :class => "required") if options.delete(:required)
|
||||
@template.content_tag("label", text.html_safe,
|
||||
:class => (@object && @object.errors[field].present? ? "error" : nil),
|
||||
:for => (@object_name.to_s + "_" + field.to_s))
|
||||
:class => (@object && @object.errors[field].present? ? "error" : nil),
|
||||
:for => (@object_name.to_s + "_" + field.to_s))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -19,7 +21,7 @@ require 'digest/md5'
|
|||
|
||||
module Redmine
|
||||
module WikiFormatting
|
||||
class StaleSectionError < Exception; end
|
||||
class StaleSectionError < StandardError; end
|
||||
|
||||
@@formatters = {}
|
||||
|
||||
|
@ -32,13 +34,13 @@ module Redmine
|
|||
options = args.last.is_a?(Hash) ? args.pop : {}
|
||||
name = name.to_s
|
||||
raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
|
||||
|
||||
formatter, helper, parser = args.any? ?
|
||||
args :
|
||||
%w(Formatter Helper HtmlParser).map {|m| "Redmine::WikiFormatting::#{name.classify}::#{m}".constantize rescue nil}
|
||||
|
||||
formatter, helper, parser =
|
||||
if args.any?
|
||||
args
|
||||
else
|
||||
%w(Formatter Helper HtmlParser).map {|m| "Redmine::WikiFormatting::#{name.classify}::#{m}".constantize rescue nil}
|
||||
end
|
||||
raise "A formatter class is required" if formatter.nil?
|
||||
|
||||
@@formatters[name] = {
|
||||
:formatter => formatter,
|
||||
:helper => helper,
|
||||
|
@ -79,15 +81,17 @@ module Redmine
|
|||
end
|
||||
|
||||
def to_html(format, text, options = {})
|
||||
text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute])
|
||||
# Text retrieved from the cache store may be frozen
|
||||
# We need to dup it so we can do in-place substitutions with gsub!
|
||||
cache_store.fetch cache_key do
|
||||
text =
|
||||
if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store &&
|
||||
cache_key = cache_key_for(format, text, options[:object], options[:attribute])
|
||||
# Text retrieved from the cache store may be frozen
|
||||
# We need to dup it so we can do in-place substitutions with gsub!
|
||||
cache_store.fetch cache_key do
|
||||
formatter_for(format).new(text).to_html
|
||||
end.dup
|
||||
else
|
||||
formatter_for(format).new(text).to_html
|
||||
end.dup
|
||||
else
|
||||
formatter_for(format).new(text).to_html
|
||||
end
|
||||
end
|
||||
text
|
||||
end
|
||||
|
||||
|
@ -125,7 +129,7 @@ module Redmine
|
|||
([^<]\S*?) # url
|
||||
(\/)? # slash
|
||||
)
|
||||
((?:>)?|[^[:alnum:]_\=\/;\(\)]*?) # post
|
||||
((?:>)?|[^[:alnum:]_\=\/;\(\)\-]*?) # post
|
||||
(?=<|\s|$)
|
||||
}x unless const_defined?(:AUTO_LINK_RE)
|
||||
|
||||
|
@ -133,16 +137,16 @@ module Redmine
|
|||
def auto_link!(text)
|
||||
text.gsub!(AUTO_LINK_RE) do
|
||||
all, leading, proto, url, post = $&, $1, $2, $3, $6
|
||||
if leading =~ /<a\s/i || leading =~ /![<>=]?/
|
||||
if /<a\s/i.match?(leading) || /![<>=]?/.match?(leading)
|
||||
# don't replace URLs that are already linked
|
||||
# and URLs prefixed with ! !> !< != (textile images)
|
||||
all
|
||||
else
|
||||
# Idea below : an URL with unbalanced parenthesis and
|
||||
# ending by ')' is put into external parenthesis
|
||||
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
|
||||
url=url[0..-2] # discard closing parenthesis from url
|
||||
post = ")"+post # add closing parenthesis to post
|
||||
if url[-1] == ")" and ((url.count("(") - url.count(")")) < 0)
|
||||
url = url[0..-2] # discard closing parenthesis from url
|
||||
post = ")" + post # add closing parenthesis to post
|
||||
end
|
||||
content = proto + url
|
||||
href = "#{proto=="www."?"http://www.":proto}#{url}"
|
||||
|
@ -153,15 +157,43 @@ module Redmine
|
|||
|
||||
# Destructively replaces email addresses into clickable links
|
||||
def auto_mailto!(text)
|
||||
text.gsub!(/((?<!@)\b[\w\.!#\$%\-+.\/]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
|
||||
text.gsub!(/([\w\.!#\$%\-+.\/]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
|
||||
mail = $1
|
||||
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
|
||||
if /<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/.match?(text)
|
||||
mail
|
||||
else
|
||||
%(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def restore_redmine_links(html)
|
||||
# restore wiki links eg. [[Foo]]
|
||||
html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
|
||||
"[[#{$2}]]"
|
||||
end
|
||||
# restore Redmine links with double-quotes, eg. version:"1.0"
|
||||
html.gsub!(/(\w):"(.+?)"/) do
|
||||
"#{$1}:\"#{$2}\""
|
||||
end
|
||||
# restore user links with @ in login name eg. [@jsmith@somenet.foo]
|
||||
html.gsub!(%r{[@\A]<a(\sclass="email")? href="mailto:(.*?)">(.*?)</a>}) do
|
||||
"@#{$2}"
|
||||
end
|
||||
# restore user links with @ in login name eg. [user:jsmith@somenet.foo]
|
||||
html.gsub!(%r{\buser:<a(\sclass="email")? href="mailto:(.*?)">(.*?)<\/a>}) do
|
||||
"user:#{$2}"
|
||||
end
|
||||
# restore attachments links with @ in file name eg. [attachment:image@2x.png]
|
||||
html.gsub!(%r{\battachment:<a(\sclass="email")? href="mailto:(.*?)">(.*?)</a>}) do
|
||||
"attachment:#{$2}"
|
||||
end
|
||||
# restore hires images which are misrecognized as email address eg. [printscreen@2x.png]
|
||||
html.gsub!(%r{<a(\sclass="email")? href="mailto:[^"]+@\dx\.(bmp|gif|jpg|jpe|jpeg|png)">(.*?)</a>}) do
|
||||
"#{$3}"
|
||||
end
|
||||
html
|
||||
end
|
||||
end
|
||||
|
||||
# Default formatter module
|
||||
|
@ -180,12 +212,13 @@ module Redmine
|
|||
t = CGI::escapeHTML(@text)
|
||||
auto_link!(t)
|
||||
auto_mailto!(t)
|
||||
restore_redmine_links(t)
|
||||
simple_format(t, {}, :sanitize => false)
|
||||
end
|
||||
end
|
||||
|
||||
module Helper
|
||||
def wikitoolbar_for(field_id)
|
||||
def wikitoolbar_for(field_id, preview_url = preview_text_path)
|
||||
end
|
||||
|
||||
def heads_for_wiki_formatter
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -28,13 +30,13 @@ module Redmine
|
|||
}
|
||||
|
||||
def self.to_text(html)
|
||||
html = html.gsub(/[\n\r]/, '').squeeze(' ')
|
||||
|
||||
html = html.gsub(/[\n\r]/, ' ')
|
||||
|
||||
doc = Loofah.document(html)
|
||||
doc.scrub!(WikiTags.new(tags))
|
||||
doc.scrub!(:newline_block_elements)
|
||||
|
||||
Loofah::Helpers.remove_extraneous_whitespace(doc.text).strip
|
||||
|
||||
Loofah.remove_extraneous_whitespace(doc.text(:encode_special_chars => false)).strip.squeeze(' ').gsub(/^ +/, '')
|
||||
end
|
||||
|
||||
class WikiTags < ::Loofah::Scrubber
|
||||
|
@ -42,7 +44,7 @@ module Redmine
|
|||
@direction = :bottom_up
|
||||
@tags_to_text = tags_to_text || {}
|
||||
end
|
||||
|
||||
|
||||
def scrub(node)
|
||||
formatting = @tags_to_text[node.name]
|
||||
case formatting
|
||||
|
@ -52,6 +54,9 @@ module Redmine
|
|||
when String
|
||||
node.add_next_sibling Nokogiri::XML::Text.new(formatting, node.document)
|
||||
node.remove
|
||||
when Proc
|
||||
node.add_next_sibling formatting.call(node)
|
||||
node.remove
|
||||
else
|
||||
CONTINUE
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -24,10 +26,16 @@ module Redmine
|
|||
Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
|
||||
end
|
||||
|
||||
def exec_macro(name, obj, args, text)
|
||||
def exec_macro(name, obj, args, text, options={})
|
||||
macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
|
||||
return unless macro_options
|
||||
|
||||
if options[:inline_attachments] == false
|
||||
Redmine::WikiFormatting::Macros.inline_attachments = false
|
||||
else
|
||||
Redmine::WikiFormatting::Macros.inline_attachments = true
|
||||
end
|
||||
|
||||
method_name = "macro_#{name}"
|
||||
unless macro_options[:parse_args] == false
|
||||
args = args.split(',').map(&:strip)
|
||||
|
@ -57,7 +65,9 @@ module Redmine
|
|||
end
|
||||
|
||||
@@available_macros = {}
|
||||
@@inline_attachments = true
|
||||
mattr_accessor :available_macros
|
||||
mattr_accessor :inline_attachments
|
||||
|
||||
class << self
|
||||
# Plugins can use this method to define new macros:
|
||||
|
@ -67,7 +77,7 @@ module Redmine
|
|||
# macro :my_macro do |obj, args|
|
||||
# "My macro output"
|
||||
# end
|
||||
#
|
||||
#
|
||||
# desc "This is my macro that accepts a block of text"
|
||||
# macro :my_macro do |obj, args, text|
|
||||
# "My macro output"
|
||||
|
@ -81,7 +91,7 @@ module Redmine
|
|||
#
|
||||
# Options:
|
||||
# * :desc - A description of the macro
|
||||
# * :parse_args => false - Disables arguments parsing (the whole arguments
|
||||
# * :parse_args => false - Disables arguments parsing (the whole arguments
|
||||
# string is passed to the macro)
|
||||
#
|
||||
# Macro blocks accept 2 or 3 arguments:
|
||||
|
@ -89,7 +99,7 @@ module Redmine
|
|||
# * args: macro arguments
|
||||
# * text: the block of text given to the macro (should be present only if the
|
||||
# macro accepts a block of text). text is a String or nil if the macro is
|
||||
# invoked without a block of text.
|
||||
# invoked without a block of text.
|
||||
#
|
||||
# Examples:
|
||||
# By default, when the macro is invoked, the comma separated list of arguments
|
||||
|
@ -141,7 +151,7 @@ module Redmine
|
|||
# If a block of text is given, the closing tag }} must be at the start of a new line.
|
||||
def macro(name, options={}, &block)
|
||||
options.assert_valid_keys(:desc, :parse_args)
|
||||
unless name.to_s.match(/\A\w+\z/)
|
||||
unless /\A\w+\z/.match?(name.to_s)
|
||||
raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
|
||||
end
|
||||
unless block_given?
|
||||
|
@ -162,7 +172,7 @@ module Redmine
|
|||
# Builtin macros
|
||||
desc "Sample macro."
|
||||
macro :hello_world do |obj, args, text|
|
||||
h("Hello world! Object: #{obj.class.name}, " +
|
||||
h("Hello world! Object: #{obj.class.name}, " +
|
||||
(args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
|
||||
" and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
|
||||
)
|
||||
|
@ -209,7 +219,7 @@ module Redmine
|
|||
@included_wiki_pages ||= []
|
||||
raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.id)
|
||||
@included_wiki_pages << page.id
|
||||
out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
|
||||
out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false, :inline_attachments => @@inline_attachments)
|
||||
@included_wiki_pages.pop
|
||||
out
|
||||
end
|
||||
|
@ -223,13 +233,14 @@ module Redmine
|
|||
hide_label = args[1] || args[0] || l(:button_hide)
|
||||
js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
|
||||
out = ''.html_safe
|
||||
out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
|
||||
out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
|
||||
out << content_tag('div', textilizable(text, :object => obj, :headings => false), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
|
||||
out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible')
|
||||
out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'icon icon-expended collapsible', :style => 'display:none;')
|
||||
out << content_tag('div', textilizable(text, :object => obj, :headings => false, :inline_attachments => @@inline_attachments), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
|
||||
out
|
||||
end
|
||||
|
||||
desc "Displays a clickable thumbnail of an attached image. Examples:\n\n" +
|
||||
desc "Displays a clickable thumbnail of an attached image.\n" +
|
||||
"Default size is 200 pixels. Examples:\n\n" +
|
||||
"{{thumbnail(image.png)}}\n" +
|
||||
"{{thumbnail(image.png, size=300, title=Thumbnail)}} -- with custom title and size"
|
||||
macro :thumbnail do |obj, args|
|
||||
|
@ -237,9 +248,9 @@ module Redmine
|
|||
filename = args.first
|
||||
raise 'Filename required' unless filename.present?
|
||||
size = options[:size]
|
||||
raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
|
||||
raise 'Invalid size parameter' unless size.nil? || /^\d+$/.match?(size)
|
||||
size = size.to_i
|
||||
size = nil unless size > 0
|
||||
size = 200 unless size > 0
|
||||
if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
|
||||
title = options[:title] || attachment.title
|
||||
thumbnail_url = url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size, :only_path => @only_path)
|
||||
|
@ -251,6 +262,33 @@ module Redmine
|
|||
raise "Attachment #{filename} not found"
|
||||
end
|
||||
end
|
||||
|
||||
desc "Displays an issue link including additional information. Examples:\n\n" +
|
||||
"{{issue(123)}} -- Issue #123: Enhance macro capabilities\n" +
|
||||
"{{issue(123, project=true)}} -- Andromeda - Issue #123: Enhance macro capabilities\n" +
|
||||
"{{issue(123, tracker=false)}} -- #123: Enhance macro capabilities\n" +
|
||||
"{{issue(123, subject=false, project=true)}} -- Andromeda - Issue #123\n"
|
||||
macro :issue do |obj, args|
|
||||
args, options = extract_macro_options(args, :project, :subject, :tracker)
|
||||
id = args.first
|
||||
issue = Issue.visible.find_by(id: id)
|
||||
|
||||
if issue
|
||||
# remove invalid options
|
||||
options.delete_if { |k,v| v != 'true' && v != 'false' }
|
||||
|
||||
# turn string values into boolean
|
||||
options.each do |k, v|
|
||||
options[k] = v == 'true'
|
||||
end
|
||||
|
||||
link_to_issue(issue, options)
|
||||
else
|
||||
# Fall back to regular issue link format to indicate, that there
|
||||
# should have been something.
|
||||
"##{id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -52,24 +54,16 @@ module Redmine
|
|||
end
|
||||
|
||||
class Formatter
|
||||
include Redmine::WikiFormatting::LinksHelper
|
||||
alias :inline_restore_redmine_links :restore_redmine_links
|
||||
|
||||
def initialize(text)
|
||||
@text = text
|
||||
end
|
||||
|
||||
def to_html(*args)
|
||||
html = formatter.render(@text)
|
||||
# restore wiki links eg. [[Foo]]
|
||||
html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
|
||||
"[[#{$2}]]"
|
||||
end
|
||||
# restore Redmine links with double-quotes, eg. version:"1.0"
|
||||
html.gsub!(/(\w):"(.+?)"/) do
|
||||
"#{$1}:\"#{$2}\""
|
||||
end
|
||||
# restore user links with @ in login name eg. [@jsmith@somenet.foo]
|
||||
html.gsub!(%r{[@\A]<a href="mailto:(.*?)">(.*?)</a>}) do
|
||||
"@#{$2}"
|
||||
end
|
||||
html = inline_restore_redmine_links(html)
|
||||
html
|
||||
end
|
||||
|
||||
|
@ -89,14 +83,14 @@ module Redmine
|
|||
end
|
||||
|
||||
def extract_sections(index)
|
||||
sections = ['', '', '']
|
||||
sections = [+'', +'', +'']
|
||||
offset = 0
|
||||
i = 0
|
||||
l = 1
|
||||
inside_pre = false
|
||||
@text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|(?:~~~|```).*)\s*$)/).each do |part|
|
||||
level = nil
|
||||
if part =~ /\A(~{3,}|`{3,})(\S+)?\s*$/
|
||||
if part =~ /\A(~{3,}|`{3,})(\s*\S+)?\s*$/
|
||||
if !inside_pre
|
||||
inside_pre = true
|
||||
elsif !$2
|
||||
|
@ -141,7 +135,8 @@ module Redmine
|
|||
:superscript => true,
|
||||
:no_intra_emphasis => true,
|
||||
:footnotes => true,
|
||||
:lax_spacing => true
|
||||
:lax_spacing => true,
|
||||
:underline => true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -19,10 +21,10 @@ module Redmine
|
|||
module WikiFormatting
|
||||
module Markdown
|
||||
module Helper
|
||||
def wikitoolbar_for(field_id)
|
||||
def wikitoolbar_for(field_id, preview_url = preview_text_path)
|
||||
heads_for_wiki_formatter
|
||||
url = "#{Redmine::Utils.relative_url_root}/help/#{current_language.to_s.downcase}/wiki_syntax_markdown.html"
|
||||
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.draw();")
|
||||
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.setPreviewUrl('#{escape_javascript preview_url}'); wikiToolbar.draw();")
|
||||
end
|
||||
|
||||
def initial_page_content(page)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -23,15 +25,27 @@ module Redmine
|
|||
self.tags = tags.merge(
|
||||
'b' => {:pre => '**', :post => '**'},
|
||||
'strong' => {:pre => '**', :post => '**'},
|
||||
'i' => {:pre => '_', :post => '_'},
|
||||
'em' => {:pre => '_', :post => '_'},
|
||||
'i' => {:pre => '*', :post => '*'},
|
||||
'em' => {:pre => '*', :post => '*'},
|
||||
'u' => {:pre => '_', :post => '_'},
|
||||
'strike' => {:pre => '~~', :post => '~~'},
|
||||
'h1' => {:pre => "\n\n# ", :post => "\n\n"},
|
||||
'h2' => {:pre => "\n\n## ", :post => "\n\n"},
|
||||
'h3' => {:pre => "\n\n### ", :post => "\n\n"},
|
||||
'h4' => {:pre => "\n\n#### ", :post => "\n\n"},
|
||||
'h5' => {:pre => "\n\n##### ", :post => "\n\n"},
|
||||
'h6' => {:pre => "\n\n###### ", :post => "\n\n"}
|
||||
'h6' => {:pre => "\n\n###### ", :post => "\n\n"},
|
||||
'th' => {:pre => '*', :post => "*\n"},
|
||||
'td' => {:pre => '', :post => "\n"},
|
||||
'a' => lambda do |node|
|
||||
if node.content.present? && node.attributes.key?('href')
|
||||
%| [#{node.content}](#{node.attributes['href'].value}) |
|
||||
elsif node.attributes.key?('href')
|
||||
%| #{node.attributes['href'].value} |
|
||||
else
|
||||
node.content
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -27,9 +29,10 @@ module Redmine
|
|||
|
||||
alias :inline_auto_link :auto_link!
|
||||
alias :inline_auto_mailto :auto_mailto!
|
||||
alias :inline_restore_redmine_links :restore_redmine_links
|
||||
|
||||
# auto_link rule after textile rules so that it doesn't break !image_url! tags
|
||||
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
|
||||
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_restore_redmine_links]
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
|
@ -62,9 +65,9 @@ module Redmine
|
|||
@pre_list = []
|
||||
text = self.dup
|
||||
rip_offtags text, false, false
|
||||
before = ''
|
||||
s = ''
|
||||
after = ''
|
||||
before = +''
|
||||
s = +''
|
||||
after = +''
|
||||
i = 0
|
||||
l = 1
|
||||
started = false
|
||||
|
@ -105,7 +108,7 @@ module Redmine
|
|||
sections
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
|
||||
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
|
||||
|
@ -125,6 +128,7 @@ module Redmine
|
|||
language = $1 || $2
|
||||
text = $3
|
||||
if Redmine::SyntaxHighlighting.language_supported?(language)
|
||||
text.gsub!(/x%x%/, '&')
|
||||
content = "<code class=\"#{language} syntaxhl\">" +
|
||||
Redmine::SyntaxHighlighting.highlight_by_language(text, language)
|
||||
else
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -19,11 +21,11 @@ module Redmine
|
|||
module WikiFormatting
|
||||
module Textile
|
||||
module Helper
|
||||
def wikitoolbar_for(field_id)
|
||||
def wikitoolbar_for(field_id, preview_url = preview_text_path)
|
||||
heads_for_wiki_formatter
|
||||
# Is there a simple way to link to a public resource?
|
||||
url = "#{Redmine::Utils.relative_url_root}/help/#{current_language.to_s.downcase}/wiki_syntax_textile.html"
|
||||
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.draw();")
|
||||
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.setPreviewUrl('#{escape_javascript preview_url}'); wikiToolbar.draw();")
|
||||
end
|
||||
|
||||
def initial_page_content(page)
|
||||
|
@ -33,7 +35,8 @@ module Redmine
|
|||
def heads_for_wiki_formatter
|
||||
unless @heads_for_wiki_formatter_included
|
||||
content_for :header_tags do
|
||||
javascript_include_tag('jstoolbar/jstoolbar-textile.min') +
|
||||
javascript_include_tag('jstoolbar/jstoolbar') +
|
||||
javascript_include_tag('jstoolbar/textile') +
|
||||
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
|
||||
javascript_tag("var wikiImageMimeTypes = #{Redmine::MimeType.by_type('image').to_json};") +
|
||||
stylesheet_link_tag('jstoolbar')
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -32,7 +34,18 @@ module Redmine
|
|||
'h3' => {:pre => "\n\nh3. ", :post => "\n\n"},
|
||||
'h4' => {:pre => "\n\nh4. ", :post => "\n\n"},
|
||||
'h5' => {:pre => "\n\nh5. ", :post => "\n\n"},
|
||||
'h6' => {:pre => "\n\nh6. ", :post => "\n\n"}
|
||||
'h6' => {:pre => "\n\nh6. ", :post => "\n\n"},
|
||||
'th' => {:pre => '*', :post => "*\n"},
|
||||
'td' => {:pre => '', :post => "\n"},
|
||||
'a' => lambda do |node|
|
||||
if node.content.present? && node.attributes.key?('href')
|
||||
%| "#{node.content}":#{node.attributes['href'].value} |
|
||||
elsif node.attributes.key?('href')
|
||||
%| #{node.attributes['href'].value} |
|
||||
else
|
||||
node.content
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# vim:ts=4:sw=4:
|
||||
# = RedCloth - Textile and Markdown Hybrid for Ruby
|
||||
#
|
||||
|
@ -71,33 +73,33 @@
|
|||
#
|
||||
# == Links
|
||||
#
|
||||
# To make a hypertext link, put the link text in "quotation
|
||||
# To make a hypertext link, put the link text in "quotation
|
||||
# marks" followed immediately by a colon and the URL of the link.
|
||||
#
|
||||
# Optional: text in (parentheses) following the link text,
|
||||
# but before the closing quotation mark, will become a Title
|
||||
#
|
||||
# Optional: text in (parentheses) following the link text,
|
||||
# but before the closing quotation mark, will become a Title
|
||||
# attribute for the link, visible as a tool tip when a cursor is above it.
|
||||
#
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# "This is a link (This is a title) ":http://www.textism.com
|
||||
#
|
||||
#
|
||||
# Will become:
|
||||
#
|
||||
#
|
||||
# <a href="http://www.textism.com" title="This is a title">This is a link</a>
|
||||
#
|
||||
# == Images
|
||||
#
|
||||
# To insert an image, put the URL for the image inside exclamation marks.
|
||||
#
|
||||
# Optional: text that immediately follows the URL in (parentheses) will
|
||||
# be used as the Alt text for the image. Images on the web should always
|
||||
# have descriptive Alt text for the benefit of readers using non-graphical
|
||||
# Optional: text that immediately follows the URL in (parentheses) will
|
||||
# be used as the Alt text for the image. Images on the web should always
|
||||
# have descriptive Alt text for the benefit of readers using non-graphical
|
||||
# browsers.
|
||||
#
|
||||
# Optional: place a colon followed by a URL immediately after the
|
||||
# Optional: place a colon followed by a URL immediately after the
|
||||
# closing ! to make the image into a link.
|
||||
#
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# !http://www.textism.com/common/textist.gif(Textist)!
|
||||
|
@ -116,13 +118,13 @@
|
|||
#
|
||||
# == Defining Acronyms
|
||||
#
|
||||
# HTML allows authors to define acronyms via the tag. The definition appears as a
|
||||
# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
|
||||
# HTML allows authors to define acronyms via the tag. The definition appears as a
|
||||
# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
|
||||
# this should be used at least once for each acronym in documents where they appear.
|
||||
#
|
||||
# To quickly define an acronym in Textile, place the full text in (parentheses)
|
||||
# To quickly define an acronym in Textile, place the full text in (parentheses)
|
||||
# immediately following the acronym.
|
||||
#
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# ACLU(American Civil Liberties Union)
|
||||
|
@ -145,7 +147,7 @@
|
|||
# (background:#ddd;color:red). |{}| | | |
|
||||
#
|
||||
# == Using RedCloth
|
||||
#
|
||||
#
|
||||
# RedCloth is simply an extension of the String class, which can handle
|
||||
# Textile formatting. Use it like a String and output HTML with its
|
||||
# RedCloth#to_html method.
|
||||
|
@ -253,7 +255,7 @@ class RedCloth3 < String
|
|||
# #=>"<h1>A <b>bold</b> man</h1>"
|
||||
#
|
||||
def initialize( string, restrictions = [] )
|
||||
restrictions.each { |r| method( "#{ r }=" ).call( true ) }
|
||||
restrictions.each { |r| method( "#{r}=" ).call( true ) }
|
||||
super( string )
|
||||
end
|
||||
|
||||
|
@ -268,14 +270,14 @@ class RedCloth3 < String
|
|||
rules = DEFAULT_RULES if rules.empty?
|
||||
# make our working copy
|
||||
text = self.dup
|
||||
|
||||
|
||||
@urlrefs = {}
|
||||
@shelf = []
|
||||
textile_rules = [:block_textile_table, :block_textile_lists,
|
||||
:block_textile_prefix, :inline_textile_image, :inline_textile_link,
|
||||
:inline_textile_code, :inline_textile_span, :glyphs_textile]
|
||||
:block_textile_prefix, :inline_textile_image, :inline_textile_code,
|
||||
:inline_textile_span, :inline_textile_link, :glyphs_textile]
|
||||
markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
|
||||
:block_markdown_bq, :block_markdown_lists,
|
||||
:block_markdown_bq, :block_markdown_lists,
|
||||
:inline_markdown_reflink, :inline_markdown_link]
|
||||
@rules = rules.collect do |rule|
|
||||
case rule
|
||||
|
@ -289,8 +291,8 @@ class RedCloth3 < String
|
|||
end.flatten
|
||||
|
||||
# standard clean up
|
||||
incoming_entities text
|
||||
clean_white_space text
|
||||
incoming_entities text
|
||||
clean_white_space text
|
||||
|
||||
# start processor
|
||||
@pre_list = []
|
||||
|
@ -299,7 +301,7 @@ class RedCloth3 < String
|
|||
escape_html_tags text
|
||||
# need to do this before #hard_break and #blocks
|
||||
block_textile_quotes text unless @lite_mode
|
||||
hard_break text
|
||||
hard_break text
|
||||
unless @lite_mode
|
||||
refs text
|
||||
blocks text
|
||||
|
@ -314,28 +316,23 @@ class RedCloth3 < String
|
|||
clean_html text if filter_html
|
||||
text.strip!
|
||||
text
|
||||
|
||||
end
|
||||
|
||||
#######
|
||||
private
|
||||
#######
|
||||
private
|
||||
|
||||
#
|
||||
# Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
|
||||
# (from PyTextile)
|
||||
#
|
||||
TEXTILE_TAGS =
|
||||
|
||||
[[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
|
||||
[134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
|
||||
[140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
|
||||
[147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
|
||||
[153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
|
||||
|
||||
collect! do |a, b|
|
||||
[a.chr, ( b.zero? and "" or "&#{ b };" )]
|
||||
end
|
||||
|
||||
[[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
|
||||
[134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
|
||||
[140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
|
||||
[147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
|
||||
[153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
|
||||
collect! do |a, b|
|
||||
[a.chr, ( b.zero? and "" or "&#{b};" )]
|
||||
end
|
||||
#
|
||||
# Regular expressions to convert to HTML.
|
||||
#
|
||||
|
@ -357,7 +354,7 @@ class RedCloth3 < String
|
|||
|
||||
# Text markup tags, don't conflict with block tags
|
||||
SIMPLE_HTML_TAGS = [
|
||||
'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
|
||||
'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
|
||||
'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
|
||||
'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
|
||||
]
|
||||
|
@ -373,14 +370,14 @@ class RedCloth3 < String
|
|||
['+', 'ins', :limit],
|
||||
['^', 'sup', :limit],
|
||||
['~', 'sub', :limit]
|
||||
]
|
||||
]
|
||||
QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
|
||||
|
||||
|
||||
QTAGS.collect! do |rc, ht, rtype|
|
||||
rcq = Regexp::quote rc
|
||||
re =
|
||||
case rtype
|
||||
when :limit
|
||||
case rtype
|
||||
when :limit
|
||||
/(^|[>\s\(]) # sta
|
||||
(?!\-\-)
|
||||
(#{QTAGS_JOIN}|) # oqs
|
||||
|
@ -390,38 +387,38 @@ class RedCloth3 < String
|
|||
#{rcq}
|
||||
(#{QTAGS_JOIN}|) # oqa
|
||||
(?=[[:punct:]]|<|\s|\)|$)/x
|
||||
else
|
||||
else
|
||||
/(#{rcq})
|
||||
(#{C})
|
||||
(?::(\S+))?
|
||||
([[:word:]]|[^\s\-].*?[^\s\-])
|
||||
#{rcq}/xm
|
||||
end
|
||||
#{rcq}/xm
|
||||
end
|
||||
[rc, ht, re, rtype]
|
||||
end
|
||||
|
||||
# Elements to handle
|
||||
GLYPHS = [
|
||||
# [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing
|
||||
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing
|
||||
# [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing
|
||||
# [ /\'/, '‘' ], # single opening
|
||||
# [ /</, '<' ], # less-than
|
||||
# [ />/, '>' ], # greater-than
|
||||
# [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing
|
||||
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing
|
||||
# [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing
|
||||
# [ /"/, '“' ], # double opening
|
||||
# [ /\b( )?\.{3}/, '\1…' ], # ellipsis
|
||||
# [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
|
||||
# [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
|
||||
# [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash
|
||||
# [ /\s->\s/, ' → ' ], # right arrow
|
||||
# [ /\s-\s/, ' – ' ], # en dash
|
||||
# [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign
|
||||
# [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark
|
||||
# [ /\b ?[(\[]R[\])]/i, '®' ], # registered
|
||||
# [ /\b ?[(\[]C[\])]/i, '©' ] # copyright
|
||||
# [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing
|
||||
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing
|
||||
# [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing
|
||||
# [ /\'/, '‘' ], # single opening
|
||||
# [ /</, '<' ], # less-than
|
||||
# [ />/, '>' ], # greater-than
|
||||
# [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing
|
||||
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing
|
||||
# [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing
|
||||
# [ /"/, '“' ], # double opening
|
||||
# [ /\b( )?\.{3}/, '\1…' ], # ellipsis
|
||||
# [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
|
||||
# [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
|
||||
# [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash
|
||||
# [ /\s->\s/, ' → ' ], # right arrow
|
||||
# [ /\s-\s/, ' – ' ], # en dash
|
||||
# [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign
|
||||
# [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark
|
||||
# [ /\b ?[(\[]R[\])]/i, '®' ], # registered
|
||||
# [ /\b ?[(\[]C[\])]/i, '©' ] # copyright
|
||||
]
|
||||
|
||||
H_ALGN_VALS = {
|
||||
|
@ -453,10 +450,10 @@ class RedCloth3 < String
|
|||
|
||||
# Search and replace for Textile glyphs (quotes, dashes, other symbols)
|
||||
def pgl( text )
|
||||
#GLYPHS.each do |re, resub, tog|
|
||||
# GLYPHS.each do |re, resub, tog|
|
||||
# next if tog and method( tog ).call
|
||||
# text.gsub! re, resub
|
||||
#end
|
||||
# end
|
||||
text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
|
||||
"<abbr title=\"#{htmlesc $2}\">#{$1}</abbr>"
|
||||
end
|
||||
|
@ -464,20 +461,19 @@ class RedCloth3 < String
|
|||
|
||||
# Parses Textile attribute lists and builds an HTML attribute string
|
||||
def pba( text_in, element = "" )
|
||||
|
||||
return '' unless text_in
|
||||
return +'' unless text_in
|
||||
|
||||
style = []
|
||||
text = text_in.dup
|
||||
if element == 'td'
|
||||
colspan = $1 if text =~ /\\(\d+)/
|
||||
rowspan = $1 if text =~ /\/(\d+)/
|
||||
style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
|
||||
style << "vertical-align:#{v_align($&)};" if text =~ A_VLGN
|
||||
end
|
||||
|
||||
if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
|
||||
sanitized = sanitize_styles($1)
|
||||
style << "#{ sanitized };" unless sanitized.blank?
|
||||
style << "#{sanitized};" unless sanitized.blank?
|
||||
end
|
||||
|
||||
lang = $1 if
|
||||
|
@ -485,13 +481,13 @@ class RedCloth3 < String
|
|||
|
||||
cls = $1 if
|
||||
text.sub!( /\(([^()]+?)\)/, '' )
|
||||
|
||||
style << "padding-left:#{ $1.length }em;" if
|
||||
|
||||
style << "padding-left:#{$1.length}em;" if
|
||||
text.sub!( /([(]+)/, '' )
|
||||
|
||||
style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
|
||||
style << "padding-right:#{$1.length}em;" if text.sub!( /([)]+)/, '' )
|
||||
|
||||
style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
|
||||
style << "text-align:#{h_align($&)};" if text =~ A_HLGN
|
||||
|
||||
cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
|
||||
|
||||
|
@ -503,18 +499,18 @@ class RedCloth3 < String
|
|||
|
||||
id = id.starts_with?('wiki-id-') ? id : "wiki-id-#{id}" if id
|
||||
|
||||
atts = ''
|
||||
atts << " style=\"#{ style.join }\"" unless style.empty?
|
||||
atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
|
||||
atts << " lang=\"#{ lang }\"" if lang
|
||||
atts << " id=\"#{ id }\"" if id
|
||||
atts << " colspan=\"#{ colspan }\"" if colspan
|
||||
atts << " rowspan=\"#{ rowspan }\"" if rowspan
|
||||
|
||||
atts = +''
|
||||
atts << " style=\"#{style.join}\"" unless style.empty?
|
||||
atts << " class=\"#{cls}\"" unless cls.to_s.empty?
|
||||
atts << " lang=\"#{lang}\"" if lang
|
||||
atts << " id=\"#{id}\"" if id
|
||||
atts << " colspan=\"#{colspan}\"" if colspan
|
||||
atts << " rowspan=\"#{rowspan}\"" if rowspan
|
||||
|
||||
atts
|
||||
end
|
||||
|
||||
STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
|
||||
STYLES_RE = /^(color|(min-|max-)?+(width|height)|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
|
||||
|
||||
def sanitize_styles(str)
|
||||
styles = str.split(";").map(&:strip)
|
||||
|
@ -525,11 +521,10 @@ class RedCloth3 < String
|
|||
end
|
||||
|
||||
TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
|
||||
|
||||
# Parses a Textile table block, building HTML from the result.
|
||||
def block_textile_table( text )
|
||||
text.gsub!( TABLE_RE ) do |matches|
|
||||
|
||||
# Parses a Textile table block, building HTML from the result.
|
||||
def block_textile_table( text )
|
||||
text.gsub!( TABLE_RE ) do |matches|
|
||||
tatts, fullrow = $~[1..2]
|
||||
tatts = pba( tatts, 'table' )
|
||||
tatts = shelve( tatts ) if tatts
|
||||
|
@ -538,7 +533,7 @@ class RedCloth3 < String
|
|||
fullrow.each_line do |row|
|
||||
ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
|
||||
cells = []
|
||||
# the regexp prevents wiki links with a | from being cut as cells
|
||||
# the regexp prevents wiki links with a | from being cut as cells
|
||||
row.scan(/\|(_?#{S}#{A}#{C}\. ?)?((\[\[[^|\]]*\|[^|\]]*\]\]|[^|])*?)(?=\|)/) do |modifiers, cell|
|
||||
ctyp = 'd'
|
||||
ctyp = 'h' if modifiers && modifiers =~ /^_/
|
||||
|
@ -547,12 +542,12 @@ class RedCloth3 < String
|
|||
catts = pba( modifiers, 'td' ) if modifiers
|
||||
|
||||
catts = shelve( catts ) if catts
|
||||
cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
|
||||
cells << "\t\t\t<t#{ctyp}#{catts}>#{cell}</t#{ctyp}>"
|
||||
end
|
||||
ratts = shelve( ratts ) if ratts
|
||||
rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
|
||||
rows << "\t\t<tr#{ratts}>\n#{cells.join("\n")}\n\t\t</tr>"
|
||||
end
|
||||
"\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
|
||||
"\t<table#{tatts}>\n#{rows.join("\n")}\n\t</table>\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -560,19 +555,19 @@ class RedCloth3 < String
|
|||
LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
|
||||
|
||||
# Parses Textile lists and generates HTML
|
||||
def block_textile_lists( text )
|
||||
def block_textile_lists( text )
|
||||
text.gsub!( LISTS_RE ) do |match|
|
||||
lines = match.split( /\n/ )
|
||||
last_line = -1
|
||||
depth = []
|
||||
lines.each_with_index do |line, line_id|
|
||||
if line =~ LISTS_CONTENT_RE
|
||||
if line =~ LISTS_CONTENT_RE
|
||||
tl,atts,content = $~[1..3]
|
||||
if depth.last
|
||||
if depth.last.length > tl.length
|
||||
(depth.length - 1).downto(0) do |i|
|
||||
break if depth[i].length == tl.length
|
||||
lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
|
||||
lines[line_id - 1] << "</li>\n\t</#{lT(depth[i])}l>\n\t"
|
||||
depth.pop
|
||||
end
|
||||
end
|
||||
|
@ -580,39 +575,38 @@ class RedCloth3 < String
|
|||
lines[line_id - 1] << '</li>'
|
||||
end
|
||||
end
|
||||
unless depth.last == tl
|
||||
if depth.last != tl
|
||||
depth << tl
|
||||
atts = pba( atts )
|
||||
atts = shelve( atts ) if atts
|
||||
lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
|
||||
lines[line_id] = +"\t<#{lT(tl)}l#{atts}>\n\t<li>#{content}"
|
||||
else
|
||||
lines[line_id] = "\t\t<li>#{ content }"
|
||||
lines[line_id] = +"\t\t<li>#{content}"
|
||||
end
|
||||
last_line = line_id
|
||||
|
||||
else
|
||||
last_line = line_id
|
||||
end
|
||||
if line_id - last_line > 1 or line_id == lines.length - 1
|
||||
while v = depth.pop
|
||||
lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
|
||||
lines[last_line] << "</li>\n\t</#{lT(v)}l>"
|
||||
end
|
||||
end
|
||||
end
|
||||
lines.join( "\n" )
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
|
||||
QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
|
||||
|
||||
|
||||
def block_textile_quotes( text )
|
||||
text.gsub!( QUOTES_RE ) do |match|
|
||||
lines = match.split( /\n/ )
|
||||
quotes = ''
|
||||
quotes = +''
|
||||
indent = 0
|
||||
lines.each do |line|
|
||||
line =~ QUOTES_CONTENT_RE
|
||||
line =~ QUOTES_CONTENT_RE
|
||||
bq,content = $1, $2
|
||||
l = bq.count('>')
|
||||
if l != indent
|
||||
|
@ -633,15 +627,15 @@ class RedCloth3 < String
|
|||
@
|
||||
(?=\W)/x
|
||||
|
||||
def inline_textile_code( text )
|
||||
def inline_textile_code( text )
|
||||
text.gsub!( CODE_RE ) do |m|
|
||||
before,lang,code,after = $~[1..4]
|
||||
lang = " lang=\"#{ lang }\"" if lang
|
||||
rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
|
||||
lang = " lang=\"#{lang}\"" if lang
|
||||
rip_offtags( +"#{before}<code#{lang}>#{code}</code>#{after}", false )
|
||||
end
|
||||
end
|
||||
|
||||
def lT( text )
|
||||
def lT( text )
|
||||
text =~ /\#$/ ? 'o' : 'u'
|
||||
end
|
||||
|
||||
|
@ -676,35 +670,34 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
|
||||
block_applied = 0
|
||||
block_applied = 0
|
||||
@rules.each do |rule_name|
|
||||
block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
|
||||
block_applied += 1 if rule_name.to_s.match /^block_/ and method(rule_name).call(blk)
|
||||
end
|
||||
if block_applied.zero?
|
||||
if deep_code
|
||||
blk = "\t<pre><code>#{ blk }</code></pre>"
|
||||
blk = "\t<pre><code>#{blk}</code></pre>"
|
||||
else
|
||||
blk = "\t<p>#{ blk }</p>"
|
||||
blk = "\t<p>#{blk}</p>"
|
||||
end
|
||||
end
|
||||
# hard_break blk
|
||||
blk + "\n#{ code_blk }"
|
||||
blk + "\n#{code_blk}"
|
||||
end
|
||||
end
|
||||
|
||||
end.join( "\n\n" ) )
|
||||
end
|
||||
|
||||
def textile_bq( tag, atts, cite, content )
|
||||
cite, cite_title = check_refs( cite )
|
||||
cite = " cite=\"#{ cite }\"" if cite
|
||||
cite = " cite=\"#{cite}\"" if cite
|
||||
atts = shelve( atts ) if atts
|
||||
"\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
|
||||
"\t<blockquote#{cite}>\n\t\t<p#{atts}>#{content}</p>\n\t</blockquote>"
|
||||
end
|
||||
|
||||
def textile_p( tag, atts, cite, content )
|
||||
atts = shelve( atts ) if atts
|
||||
"\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
|
||||
"\t<#{tag}#{atts}>#{content}</#{tag}>"
|
||||
end
|
||||
|
||||
alias textile_h1 textile_p
|
||||
|
@ -715,35 +708,35 @@ class RedCloth3 < String
|
|||
alias textile_h6 textile_p
|
||||
|
||||
def textile_fn_( tag, num, atts, cite, content )
|
||||
atts << " id=\"fn#{ num }\" class=\"footnote\""
|
||||
content = "<sup>#{ num }</sup> #{ content }"
|
||||
atts << " id=\"fn#{num}\" class=\"footnote\""
|
||||
content = "<sup>#{num}</sup> #{content}"
|
||||
atts = shelve( atts ) if atts
|
||||
"\t<p#{ atts }>#{ content }</p>"
|
||||
"\t<p#{atts}>#{content}</p>"
|
||||
end
|
||||
|
||||
BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
|
||||
|
||||
def block_textile_prefix( text )
|
||||
def block_textile_prefix( text )
|
||||
if text =~ BLOCK_RE
|
||||
tag,tagpre,num,atts,cite,content = $~[1..6]
|
||||
atts = pba( atts )
|
||||
|
||||
# pass to prefix handler
|
||||
replacement = nil
|
||||
if respond_to? "textile_#{ tag }", true
|
||||
replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
|
||||
elsif respond_to? "textile_#{ tagpre }_", true
|
||||
replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
|
||||
if respond_to? "textile_#{tag}", true
|
||||
replacement = method( "textile_#{tag}" ).call( tag, atts, cite, content )
|
||||
elsif respond_to? "textile_#{tagpre}_", true
|
||||
replacement = method( "textile_#{tagpre}_" ).call( tagpre, num, atts, cite, content )
|
||||
end
|
||||
text.gsub!( $& ) { replacement } if replacement
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
|
||||
def block_markdown_setext( text )
|
||||
if text =~ SETEXT_RE
|
||||
tag = if $2 == "="; "h1"; else; "h2"; end
|
||||
blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
|
||||
tag = ($2 == "=" ? "h1" : "h2")
|
||||
blk, cont = "<#{tag}>#{$1}</#{tag}>", $'
|
||||
blocks cont
|
||||
text.replace( blk + cont )
|
||||
end
|
||||
|
@ -757,8 +750,8 @@ class RedCloth3 < String
|
|||
$/x
|
||||
def block_markdown_atx( text )
|
||||
if text =~ ATX_RE
|
||||
tag = "h#{ $1.length }"
|
||||
blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
|
||||
tag = "h#{$1.length}"
|
||||
blk, cont = "<#{tag}>#{$2}</#{tag}>\n\n", $'
|
||||
blocks cont
|
||||
text.replace( blk + cont )
|
||||
end
|
||||
|
@ -772,7 +765,7 @@ class RedCloth3 < String
|
|||
flush_left blk
|
||||
blocks blk
|
||||
blk.gsub!( /^(\S)/, "\t\\1" )
|
||||
"<blockquote>\n#{ blk }\n</blockquote>\n\n"
|
||||
"<blockquote>\n#{blk}\n</blockquote>\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -790,10 +783,9 @@ class RedCloth3 < String
|
|||
def block_markdown_lists( text )
|
||||
end
|
||||
|
||||
def inline_textile_span( text )
|
||||
def inline_textile_span( text )
|
||||
QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
|
||||
text.gsub!( qtag_re ) do |m|
|
||||
|
||||
case rtype
|
||||
when :limit
|
||||
sta,oqs,qtag,content,oqa = $~[1..6]
|
||||
|
@ -808,8 +800,7 @@ class RedCloth3 < String
|
|||
atts = pba( atts )
|
||||
atts = shelve( atts ) if atts
|
||||
|
||||
"#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
|
||||
|
||||
"#{sta}#{oqs}<#{ht}#{atts}>#{content}</#{ht}>#{oqa}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -826,36 +817,37 @@ class RedCloth3 < String
|
|||
( # $url
|
||||
(\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
|
||||
[[:alnum:]_\/]\S+?
|
||||
)
|
||||
)
|
||||
(\/)? # $slash
|
||||
([^[:alnum:]_\=\/;\(\)]*?) # $post
|
||||
([^[:alnum:]_\=\/;\(\)\-]*?) # $post
|
||||
)
|
||||
(?=<|\s|$)
|
||||
/x
|
||||
#"
|
||||
def inline_textile_link( text )
|
||||
/x
|
||||
|
||||
def inline_textile_link( text )
|
||||
text.gsub!( LINK_RE ) do |m|
|
||||
all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
|
||||
if text.include?('<br />')
|
||||
all
|
||||
else
|
||||
url, url_title = check_refs( url )
|
||||
url, url_title = check_refs(url)
|
||||
title ||= url_title
|
||||
|
||||
# Idea below : an URL with unbalanced parethesis and
|
||||
# ending by ')' is put into external parenthesis
|
||||
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
|
||||
url=url[0..-2] # discard closing parenth from url
|
||||
post = ")"+post # add closing parenth to post
|
||||
if url[-1] == ")" and ((url.count("(") - url.count(")")) < 0)
|
||||
url = url[0..-2] # discard closing parenth from url
|
||||
post = ")" + post # add closing parenth to post
|
||||
end
|
||||
atts = pba( atts )
|
||||
atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
|
||||
atts << " title=\"#{ htmlesc title }\"" if title
|
||||
atts = shelve( atts ) if atts
|
||||
|
||||
|
||||
url = htmlesc(url.dup)
|
||||
next all if url.downcase.start_with?('javascript:')
|
||||
|
||||
atts = pba(atts)
|
||||
atts = +" href=\"#{url}#{slash}\"#{atts}"
|
||||
atts << " title=\"#{htmlesc title}\"" if title
|
||||
atts = shelve(atts) if atts
|
||||
external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
|
||||
|
||||
"#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
|
||||
"#{pre}<a#{atts}#{external}>#{text}</a>#{post}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -865,9 +857,9 @@ class RedCloth3 < String
|
|||
[ ]? # opt. space
|
||||
(?:\n[ ]*)? # one optional newline followed by spaces
|
||||
\[(.*?)\] # $id
|
||||
/x
|
||||
/x
|
||||
|
||||
def inline_markdown_reflink( text )
|
||||
def inline_markdown_reflink( text )
|
||||
text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
|
||||
text, id = $~[1..2]
|
||||
|
||||
|
@ -876,12 +868,12 @@ class RedCloth3 < String
|
|||
else
|
||||
url, title = check_refs( id )
|
||||
end
|
||||
|
||||
atts = " href=\"#{ url }\""
|
||||
atts << " title=\"#{ title }\"" if title
|
||||
|
||||
atts = " href=\"#{url}\""
|
||||
atts << " title=\"#{title}\"" if title
|
||||
atts = shelve( atts )
|
||||
|
||||
"<a#{ atts }>#{ text }</a>"
|
||||
|
||||
"<a#{atts}>#{text}</a>"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -897,17 +889,17 @@ class RedCloth3 < String
|
|||
\3 # matching quote
|
||||
)? # title is optional
|
||||
\)
|
||||
/x
|
||||
/x
|
||||
|
||||
def inline_markdown_link( text )
|
||||
def inline_markdown_link( text )
|
||||
text.gsub!( MARKDOWN_LINK_RE ) do |m|
|
||||
text, url, quote, title = $~[1..4]
|
||||
|
||||
atts = " href=\"#{ url }\""
|
||||
atts << " title=\"#{ title }\"" if title
|
||||
atts = " href=\"#{url}\""
|
||||
atts << " title=\"#{title}\"" if title
|
||||
atts = shelve( atts )
|
||||
|
||||
"<a#{ atts }>#{ text }</a>"
|
||||
|
||||
"<a#{atts}>#{text}</a>"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -920,14 +912,14 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
|
||||
def refs_textile( text )
|
||||
def refs_textile( text )
|
||||
text.gsub!( TEXTILE_REFS_RE ) do |m|
|
||||
flag, url = $~[2..3]
|
||||
@urlrefs[flag.downcase] = [url, nil]
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def refs_markdown( text )
|
||||
text.gsub!( MARKDOWN_REFS_RE ) do |m|
|
||||
flag, url = $~[2..3]
|
||||
|
@ -937,7 +929,7 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
|
||||
def check_refs( text )
|
||||
def check_refs( text )
|
||||
ret = @urlrefs[text.downcase] if text
|
||||
ret || [text, nil]
|
||||
end
|
||||
|
@ -952,17 +944,17 @@ class RedCloth3 < String
|
|||
\s? # optional space
|
||||
(?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
|
||||
\! # closing
|
||||
(?::#{ HYPERLINK })? # optional href
|
||||
/x
|
||||
(?::#{HYPERLINK})? # optional href
|
||||
/x
|
||||
|
||||
def inline_textile_image( text )
|
||||
def inline_textile_image( text )
|
||||
text.gsub!( IMAGE_RE ) do |m|
|
||||
stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
|
||||
htmlesc title
|
||||
atts = pba( atts )
|
||||
atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
|
||||
atts << " title=\"#{ title }\"" if title
|
||||
atts << " alt=\"#{ title }\""
|
||||
atts = +" src=\"#{htmlesc url.dup}\"#{atts}"
|
||||
atts << " title=\"#{title}\"" if title
|
||||
atts << " alt=\"#{title}\""
|
||||
# size = @getimagesize($url);
|
||||
# if($size) $atts.= " $size[3]";
|
||||
|
||||
|
@ -970,18 +962,22 @@ class RedCloth3 < String
|
|||
url, url_title = check_refs( url )
|
||||
|
||||
next m unless uri_with_safe_scheme?(url)
|
||||
if href
|
||||
href = htmlesc(href.dup)
|
||||
next m if href.downcase.start_with?('javascript:')
|
||||
end
|
||||
|
||||
out = ''
|
||||
out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
|
||||
out << "<img#{ shelve( atts ) } />"
|
||||
out << "</a>#{ href_a1 }#{ href_a2 }" if href
|
||||
|
||||
if algn
|
||||
out = +''
|
||||
out << "<a#{shelve(" href=\"#{href}\"")}>" if href
|
||||
out << "<img#{shelve(atts)} />"
|
||||
out << "</a>#{href_a1}#{href_a2}" if href
|
||||
|
||||
if algn
|
||||
algn = h_align( algn )
|
||||
if stln == "<p>"
|
||||
out = "<p style=\"float:#{ algn }\">#{ out }"
|
||||
out = "<p style=\"float:#{algn}\">#{out}"
|
||||
else
|
||||
out = "#{ stln }<span style=\"float:#{ algn }\">#{ out }</span>"
|
||||
out = "#{stln}<span style=\"float:#{algn}\">#{out}</span>"
|
||||
end
|
||||
else
|
||||
out = stln + out
|
||||
|
@ -991,18 +987,18 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
|
||||
def shelve( val )
|
||||
def shelve( val )
|
||||
@shelf << val
|
||||
" :redsh##{ @shelf.length }:"
|
||||
" :redsh##{@shelf.length}:"
|
||||
end
|
||||
|
||||
def retrieve( text )
|
||||
|
||||
def retrieve( text )
|
||||
text.gsub!(/ :redsh#(\d+):/) do
|
||||
@shelf[$1.to_i - 1] || $&
|
||||
end
|
||||
end
|
||||
|
||||
def incoming_entities( text )
|
||||
def incoming_entities( text )
|
||||
## turn any incoming ampersands into a dummy character for now.
|
||||
## This uses a negative lookahead for alphanumerics followed by a semicolon,
|
||||
## implying an incoming html entity, to be skipped
|
||||
|
@ -1010,14 +1006,14 @@ class RedCloth3 < String
|
|||
text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
|
||||
end
|
||||
|
||||
def no_textile( text )
|
||||
text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
|
||||
'\1<notextile>\2</notextile>\3' )
|
||||
text.gsub!( /^ *==([^=]+.*?)==/m,
|
||||
'\1<notextile>\2</notextile>\3' )
|
||||
def no_textile( text )
|
||||
text.gsub!(/(^|\s)==([^=]+.*?)==(\s|$)?/,
|
||||
'\1<notextile>\2</notextile>\3')
|
||||
text.gsub!(/^ *==([^=]+.*?)==/m,
|
||||
'\1<notextile>\2</notextile>\3')
|
||||
end
|
||||
|
||||
def clean_white_space( text )
|
||||
def clean_white_space( text )
|
||||
# normalize line breaks
|
||||
text.gsub!( /\r\n/, "\n" )
|
||||
text.gsub!( /\r/, "\n" )
|
||||
|
@ -1032,26 +1028,27 @@ class RedCloth3 < String
|
|||
end
|
||||
|
||||
def flush_left( text )
|
||||
indt = 0
|
||||
if text =~ /^ /
|
||||
while text !~ /^ {#{indt}}[^ ]/
|
||||
indt += 1
|
||||
end unless text.empty?
|
||||
if /(?![\r\n\t ])[[:cntrl:]]/.match?(text)
|
||||
text.gsub!(/(?![\r\n\t ])[[:cntrl:]]/, '')
|
||||
end
|
||||
if /^ +\S/.match?(text)
|
||||
indt = 0
|
||||
indt += 1 until /^ {#{indt}}\S/.match?(text)
|
||||
if indt.nonzero?
|
||||
text.gsub!( /^ {#{indt}}/, '' )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def footnote_ref( text )
|
||||
text.gsub!( /\b\[([0-9]+?)\](\s)?/,
|
||||
'<sup><a href="#fn\1">\1</a></sup>\2' )
|
||||
def footnote_ref( text )
|
||||
text.gsub!(/\b\[([0-9]+?)\](\s)?/,
|
||||
'<sup><a href="#fn\1">\1</a></sup>\2')
|
||||
end
|
||||
|
||||
|
||||
OFFTAGS = /(code|pre|kbd|notextile)/
|
||||
OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }\b>)|(<#{ OFFTAGS }\b[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\b\W|\Z)/mi
|
||||
OFFTAG_OPEN = /<#{ OFFTAGS }/
|
||||
OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
|
||||
OFFTAG_MATCH = /(?:(<\/#{OFFTAGS}\b>)|(<#{OFFTAGS}\b[^>]*>))(.*?)(?=<\/?#{OFFTAGS}\b\W|\Z)/mi
|
||||
OFFTAG_OPEN = /<#{OFFTAGS}/
|
||||
OFFTAG_CLOSE = /<\/?#{OFFTAGS}/
|
||||
HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
|
||||
ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
|
||||
|
||||
|
@ -1069,7 +1066,7 @@ class RedCloth3 < String
|
|||
elsif line =~ OFFTAG_CLOSE
|
||||
codepre -= 1
|
||||
codepre = 0 if codepre < 0
|
||||
end
|
||||
end
|
||||
elsif codepre.zero?
|
||||
glyphs_textile( line, level + 1 )
|
||||
else
|
||||
|
@ -1094,29 +1091,29 @@ class RedCloth3 < String
|
|||
if codepre - used_offtags.length > 0
|
||||
htmlesc( line, :NoQuotes ) if escape_line
|
||||
@pre_list.last << line
|
||||
line = ""
|
||||
line = +""
|
||||
else
|
||||
### htmlesc is disabled between CODE tags which will be parsed with highlighter
|
||||
### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
|
||||
### NB: some changes were made not to use $N variables, because we use "match"
|
||||
### and it breaks following lines
|
||||
htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
|
||||
line = "<redpre##{ @pre_list.length }>"
|
||||
first.match(/<#{ OFFTAGS }([^>]*)>/)
|
||||
line = +"<redpre##{@pre_list.length}>"
|
||||
first.match(/<#{OFFTAGS}([^>]*)>/)
|
||||
tag = $1
|
||||
$2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
|
||||
tag << " #{$1}" if $1 && tag == 'code'
|
||||
@pre_list << "<#{ tag }>#{ aftertag }"
|
||||
@pre_list << +"<#{tag}>#{aftertag}"
|
||||
end
|
||||
elsif $1 and codepre > 0
|
||||
if codepre - used_offtags.length > 0
|
||||
htmlesc( line, :NoQuotes ) if escape_line
|
||||
@pre_list.last << line
|
||||
line = ""
|
||||
line = +""
|
||||
end
|
||||
codepre -= 1 unless codepre.zero?
|
||||
used_offtags = {} if codepre.zero?
|
||||
end
|
||||
end
|
||||
line
|
||||
end
|
||||
end
|
||||
|
@ -1130,7 +1127,7 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
|
||||
def inline( text )
|
||||
def inline( text )
|
||||
[/^inline_/, /^glyphs_/].each do |meth_re|
|
||||
@rules.each do |rule_name|
|
||||
method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
|
||||
|
@ -1138,11 +1135,11 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
|
||||
def h_align( text )
|
||||
def h_align( text )
|
||||
H_ALGN_VALS[text]
|
||||
end
|
||||
|
||||
def v_align( text )
|
||||
def v_align( text )
|
||||
V_ALGN_VALS[text]
|
||||
end
|
||||
|
||||
|
@ -1156,7 +1153,7 @@ class RedCloth3 < String
|
|||
'img' => ['src', 'alt', 'title'],
|
||||
'br' => [],
|
||||
'i' => nil,
|
||||
'u' => nil,
|
||||
'u' => nil,
|
||||
'b' => nil,
|
||||
'pre' => nil,
|
||||
'kbd' => nil,
|
||||
|
@ -1181,7 +1178,7 @@ class RedCloth3 < String
|
|||
'h3' => nil,
|
||||
'h4' => nil,
|
||||
'h5' => nil,
|
||||
'h6' => nil,
|
||||
'h6' => nil,
|
||||
'blockquote' => ['cite']
|
||||
}
|
||||
|
||||
|
@ -1209,8 +1206,7 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
ALLOWED_TAGS = %w(redpre pre code kbd notextile)
|
||||
def escape_html_tags(text)
|
||||
text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) do |m|
|
||||
|
@ -1222,4 +1218,3 @@ class RedCloth3 < String
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue