Redmine 4.1.7

This commit is contained in:
Manuel Cillero 2023-07-07 08:08:27 +02:00
parent 55458d3479
commit 3ca3c37487
103 changed files with 2426 additions and 431 deletions

14
Gemfile
View file

@ -3,17 +3,18 @@ source 'https://rubygems.org'
ruby '>= 2.3.0', '< 2.7.0' if Bundler::VERSION >= '1.12.0' ruby '>= 2.3.0', '< 2.7.0' if Bundler::VERSION >= '1.12.0'
gem "bundler", ">= 1.5.0" gem "bundler", ">= 1.5.0"
gem 'rails', '5.2.4.2' gem 'rails', '5.2.6.3'
gem 'sprockets', '~> 3.7.2' if RUBY_VERSION < '2.5' gem 'sprockets', '~> 3.7.2' if RUBY_VERSION < '2.5'
gem 'globalid', '~> 0.4.2' if Gem.ruby_version < Gem::Version.new('2.6.0')
gem "rouge", "~> 3.12.0" gem "rouge", "~> 3.12.0"
gem "request_store", "~> 1.4.1" gem "request_store", "~> 1.4.1"
gem "mini_mime", "~> 1.0.1" gem "mini_mime", "~> 1.0.1"
gem "actionpack-xml_parser" gem "actionpack-xml_parser"
gem "roadie-rails", (RUBY_VERSION < "2.5" ? "~> 1.3.0" : "~> 2.1.0") gem "roadie-rails", (RUBY_VERSION < "2.5" ? "~> 1.3.0" : "~> 2.1.0")
gem "mimemagic" gem 'marcel'
gem "mail", "~> 2.7.1" gem "mail", "~> 2.7.1"
gem "csv", "~> 3.1.1" gem 'csv', (RUBY_VERSION < '2.5' ? ['>= 3.1.1', '<= 3.1.5'] : '~> 3.1.1')
gem "nokogiri", "~> 1.10.0" gem 'nokogiri', (RUBY_VERSION < '2.5' ? '~> 1.10.0' : '~> 1.11.1')
gem "i18n", "~> 1.6.0" gem "i18n", "~> 1.6.0"
gem "rbpdf", "~> 1.20.0" gem "rbpdf", "~> 1.20.0"
@ -38,7 +39,7 @@ end
# Optional Markdown support, not for JRuby # Optional Markdown support, not for JRuby
group :markdown do group :markdown do
gem "redcarpet", "~> 3.5.0" gem 'redcarpet', '~> 3.5.1'
end end
# Include database gems for the adapters found in the database # Include database gems for the adapters found in the database
@ -47,7 +48,8 @@ require 'erb'
require 'yaml' require 'yaml'
database_file = File.join(File.dirname(__FILE__), "config/database.yml") database_file = File.join(File.dirname(__FILE__), "config/database.yml")
if File.exist?(database_file) if File.exist?(database_file)
database_config = YAML::load(ERB.new(IO.read(database_file)).result) yaml_config = ERB.new(IO.read(database_file)).result
database_config = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(yaml_config) : YAML.load(yaml_config)
adapters = database_config.values.map {|c| c['adapter']}.compact.uniq adapters = database_config.values.map {|c| c['adapter']}.compact.uniq
if adapters.any? if adapters.any?
adapters.each do |adapter| adapters.each do |adapter|

0
Rakefile Normal file → Executable file
View file

View file

@ -297,6 +297,7 @@ class AccountController < ApplicationController
:value => token, :value => token,
:expires => 1.year.from_now, :expires => 1.year.from_now,
:path => (Redmine::Configuration['autologin_cookie_path'] || RedmineApp::Application.config.relative_url_root || '/'), :path => (Redmine::Configuration['autologin_cookie_path'] || RedmineApp::Application.config.relative_url_root || '/'),
:same_site => :lax,
:secure => secure, :secure => secure,
:httponly => true :httponly => true
} }

View file

@ -33,7 +33,7 @@ class ActivitiesController < ApplicationController
@date_from = @date_to - @days @date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
if params[:user_id].present? if params[:user_id].present?
@author = User.active.find(params[:user_id]) @author = User.visible.active.find(params[:user_id])
end end
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
@ -55,7 +55,12 @@ class ActivitiesController < ApplicationController
end end
end end
events = @activity.events(@date_from, @date_to) events =
if params[:format] == 'atom'
@activity.events(nil, nil, :limit => Setting.feeds_limit.to_i)
else
@activity.events(@date_from, @date_to)
end
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language]) if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
respond_to do |format| respond_to do |format|

View file

@ -101,7 +101,7 @@ class AttachmentsController < ApplicationController
return return
end end
@attachment = Attachment.new(:file => request.raw_post) @attachment = Attachment.new(:file => raw_request_body)
@attachment.author = User.current @attachment.author = User.current
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16) @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
@attachment.content_type = params[:content_type].presence @attachment.content_type = params[:content_type].presence
@ -265,4 +265,14 @@ class AttachmentsController < ApplicationController
def update_all_params def update_all_params
params.permit(:attachments => [:filename, :description]).require(:attachments) params.permit(:attachments => [:filename, :description]).require(:attachments)
end end
# Get an IO-like object for the request body which is usable to create a new
# attachment. We try to avoid having to read the whole body into memory.
def raw_request_body
if request.body.respond_to?(:size)
request.body
else
request.raw_post
end
end
end end

View file

@ -18,6 +18,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MailHandlerController < ActionController::Base class MailHandlerController < ActionController::Base
include ActiveSupport::SecurityUtils
before_action :check_credential before_action :check_credential
# Displays the email submission form # Displays the email submission form
@ -39,7 +41,7 @@ class MailHandlerController < ActionController::Base
def check_credential def check_credential
User.current = nil User.current = nil
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key unless Setting.mail_handler_api_enabled? && secure_compare(params[:key].to_s, Setting.mail_handler_api_key.to_s)
render :plain => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403 render :plain => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
end end
end end

View file

@ -307,7 +307,7 @@ class RepositoriesController < ApplicationController
render_404 render_404
end end
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i REV_PARAM_RE = %r{\A[a-f0-9]*\z}i
def find_project_repository def find_project_repository
@project = Project.find(params[:id]) @project = Project.find(params[:id])
@ -318,14 +318,12 @@ class RepositoriesController < ApplicationController
end end
(render_404; return false) unless @repository (render_404; return false) unless @repository
@path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
@rev_to = params[:rev_to]
unless REV_PARAM_RE.match?(@rev.to_s) && REV_PARAM_RE.match?(@rev_to.to_s) @rev = params[:rev].to_s.strip.presence || @repository.default_branch
if @repository.branches.blank? raise InvalidRevisionParam unless valid_name?(@rev)
raise InvalidRevisionParam
end @rev_to = params[:rev_to].to_s.strip.presence
end raise InvalidRevisionParam unless valid_name?(@rev_to)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
rescue InvalidRevisionParam rescue InvalidRevisionParam
@ -410,4 +408,11 @@ class RepositoriesController < ApplicationController
'attachment' 'attachment'
end end
end end
def valid_name?(rev)
return true if rev.nil?
return true if REV_PARAM_RE.match?(rev)
@repository ? @repository.valid_name?(rev) : true
end
end end

View file

@ -63,7 +63,7 @@ class SearchController < ApplicationController
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)} @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
end end
@scope = @object_types.select {|t| params[t]} @scope = @object_types.select {|t| params[t].present?}
@scope = @object_types if @scope.empty? @scope = @object_types if @scope.empty?
fetcher = Redmine::Search::Fetcher.new( fetcher = Redmine::Search::Fetcher.new(

View file

@ -18,6 +18,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SysController < ActionController::Base class SysController < ActionController::Base
include ActiveSupport::SecurityUtils
before_action :check_enabled before_action :check_enabled
def projects def projects
@ -76,7 +78,7 @@ class SysController < ActionController::Base
def check_enabled def check_enabled
User.current = nil User.current = nil
unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key unless Setting.sys_api_enabled? && secure_compare(params[:key].to_s, Setting.sys_api_key.to_s)
render :plain => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403 render :plain => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
return false return false
end end

View file

@ -134,7 +134,9 @@ class WatchersController < ApplicationController
def find_objets_from_params def find_objets_from_params
klass = Object.const_get(params[:object_type].camelcase) rescue nil klass = Object.const_get(params[:object_type].camelcase) rescue nil
return unless klass && klass.respond_to?('watched_by') return unless klass && Class === klass # rubocop:disable Style/CaseEquality
return unless klass < ActiveRecord::Base
return unless klass < Redmine::Acts::Watchable::InstanceMethods
scope = klass.where(:id => Array.wrap(params[:object_id])) scope = klass.where(:id => Array.wrap(params[:object_id]))
if klass.reflect_on_association(:project) if klass.reflect_on_association(:project)

View file

@ -44,8 +44,6 @@ class WikiController < ApplicationController
helper :watchers helper :watchers
include Redmine::Export::PDF include Redmine::Export::PDF
include ActionView::Helpers::SanitizeHelper
# List of pages, sorted alphabetically and by parent (hierarchy) # List of pages, sorted alphabetically and by parent (hierarchy)
def index def index
load_pages_for_index load_pages_for_index
@ -91,7 +89,7 @@ class WikiController < ApplicationController
end end
@content = @page.content_for_version(params[:version]) @content = @page.content_for_version(params[:version])
if @content.nil? if @content.nil?
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request? if params[:version].blank? && User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
edit edit
render :action => 'edit' render :action => 'edit'
else else
@ -111,7 +109,7 @@ class WikiController < ApplicationController
send_data(export, :type => 'text/html', :filename => filename_for_content_disposition("#{@page.title}.html")) send_data(export, :type => 'text/html', :filename => filename_for_content_disposition("#{@page.title}.html"))
return return
elsif params[:format] == 'txt' elsif params[:format] == 'txt'
send_data(strip_tags(@content.text), :type => 'text/plain', :filename => filename_for_content_disposition("#{@page.title}.txt")) send_data(@content.text, :type => 'text/plain', :filename => filename_for_content_disposition("#{@page.title}.txt"))
return return
end end
end end

View file

@ -1190,7 +1190,7 @@ module ApplicationHelper
)| )|
( (
(?<sep4>@) (?<sep4>@)
(?<identifier3>[A-Za-z0-9_\-@\.]*) (?<identifier3>[A-Za-z0-9_\-@\.]*?)
) )
) )
(?= (?=
@ -1596,7 +1596,7 @@ module ApplicationHelper
# Returns the javascript tags that are included in the html layout head # Returns the javascript tags that are included in the html layout head
def javascript_heads def javascript_heads
tags = javascript_include_tag('jquery-2.2.4-ui-1.11.0-ujs-5.2.3', 'tribute-3.7.3.min', 'application', 'responsive') tags = javascript_include_tag('jquery-2.2.4-ui-1.11.0-ujs-5.2.4.5', 'tribute-3.7.3.min', 'application', 'responsive')
unless User.current.pref.warn_on_leaving_unsaved == '0' unless User.current.pref.warn_on_leaving_unsaved == '0'
tags << "\n".html_safe + javascript_tag("$(window).on('load', function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") tags << "\n".html_safe + javascript_tag("$(window).on('load', function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
end end

View file

@ -217,6 +217,11 @@ module IssuesHelper
if issue.total_spent_hours == issue.spent_hours if issue.total_spent_hours == issue.spent_hours
link_to(l_hours_short(issue.spent_hours), path) link_to(l_hours_short(issue.spent_hours), path)
else else
# link to global time entries if cross-project subtasks are allowed
# in order to show time entries from not descendents projects
if %w(system tree hierarchy).include?(Setting.cross_project_subtasks)
path = time_entries_path(:issue_id => "~#{issue.id}")
end
s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : "" s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
s += " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})" s += " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
s.html_safe s.html_safe
@ -551,7 +556,7 @@ module IssuesHelper
unless no_html unless no_html
diff_link = diff_link =
link_to( link_to(
'diff', l(:label_diff),
diff_journal_url(detail.journal_id, :detail_id => detail.id, diff_journal_url(detail.journal_id, :detail_id => detail.id,
:only_path => options[:only_path]), :only_path => options[:only_path]),
:title => l(:label_view_diff)) :title => l(:label_view_diff))
@ -577,6 +582,7 @@ module IssuesHelper
end end
# Find the name of an associated record stored in the field attribute # Find the name of an associated record stored in the field attribute
# For project, return the associated record only if is visible for the current User
def find_name_by_reflection(field, id) def find_name_by_reflection(field, id)
return nil if id.blank? return nil if id.blank?
@detail_value_name_by_reflection ||= Hash.new do |hash, key| @detail_value_name_by_reflection ||= Hash.new do |hash, key|
@ -584,7 +590,7 @@ module IssuesHelper
name = nil name = nil
if association if association
record = association.klass.find_by_id(key.last) record = association.klass.find_by_id(key.last)
if record if (record && !record.is_a?(Project)) || (record.is_a?(Project) && record.visible?)
name = record.name.force_encoding('UTF-8') name = record.name.force_encoding('UTF-8')
end end
end end

View file

@ -22,7 +22,7 @@ module JournalsHelper
# Returns the attachments of a journal that are displayed as thumbnails # Returns the attachments of a journal that are displayed as thumbnails
def journal_thumbnail_attachments(journal) def journal_thumbnail_attachments(journal)
ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key) ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key)
ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : [] ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?).sort_by{|a| ids.index(a.id.to_s)} : []
end end
# Returns the action links for an issue journal # Returns the action links for an issue journal

View file

@ -167,7 +167,7 @@ module QueriesHelper
def total_tag(column, value) def total_tag(column, value)
label = content_tag('span', "#{column.caption}:") label = content_tag('span', "#{column.caption}:")
value = value =
if [:hours, :spent_hours, :total_spent_hours, :estimated_hours].include? column.name if [:hours, :spent_hours, :total_spent_hours, :estimated_hours, :total_estimated_hours].include? column.name
format_hours(value) format_hours(value)
else else
format_object(value) format_object(value)
@ -238,7 +238,7 @@ module QueriesHelper
'span', 'span',
value.to_s(item) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe, value.to_s(item) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
:class => value.css_classes_for(item)) :class => value.css_classes_for(item))
when :hours, :estimated_hours when :hours, :estimated_hours, :total_estimated_hours
format_hours(value) format_hours(value)
when :spent_hours when :spent_hours
link_to_if(value > 0, format_hours(value), project_time_entries_path(item.project, :issue_id => "#{item.id}")) link_to_if(value > 0, format_hours(value), project_time_entries_path(item.project, :issue_id => "#{item.id}"))

View file

@ -18,14 +18,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module SearchHelper module SearchHelper
include ActionView::Helpers::SanitizeHelper
def highlight_tokens(text, tokens) def highlight_tokens(text, tokens)
return text unless text && tokens && !tokens.empty? return text unless text && tokens && !tokens.empty?
re_tokens = tokens.collect {|t| Regexp.escape(t)} re_tokens = tokens.collect {|t| Regexp.escape(t)}
regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
result = +'' result = +''
text = strip_tags(text)
text.split(regexp).each_with_index do |words, i| text.split(regexp).each_with_index do |words, i|
if result.length > 1200 if result.length > 1200
# maximum length of the preview reached # maximum length of the preview reached

View file

@ -44,7 +44,7 @@ module TimelogHelper
def user_collection_for_select_options(time_entry) def user_collection_for_select_options(time_entry)
collection = time_entry.assignable_users collection = time_entry.assignable_users
collection << time_entry.user unless time_entry.user.nil? && !collection.include?(time_entry.user) collection << time_entry.user if time_entry.user && !collection.include?(time_entry.user)
principals_options_for_select(collection, time_entry.user_id.to_s) principals_options_for_select(collection, time_entry.user_id.to_s)
end end

View file

@ -29,7 +29,8 @@ class Attachment < ActiveRecord::Base
validates_length_of :filename, :maximum => 255 validates_length_of :filename, :maximum => 255
validates_length_of :disk_filename, :maximum => 255 validates_length_of :disk_filename, :maximum => 255
validates_length_of :description, :maximum => 255 validates_length_of :description, :maximum => 255
validate :validate_max_file_size, :validate_file_extension validate :validate_max_file_size
validate :validate_file_extension, :if => :filename_changed?
acts_as_event :title => :filename, acts_as_event :title => :filename,
:url => Proc.new {|o| {:controller => 'attachments', :action => 'show', :id => o.id, :filename => o.filename}} :url => Proc.new {|o| {:controller => 'attachments', :action => 'show', :id => o.id, :filename => o.filename}}
@ -76,13 +77,11 @@ class Attachment < ActiveRecord::Base
end end
def validate_file_extension def validate_file_extension
if @temp_file
extension = File.extname(filename) extension = File.extname(filename)
unless self.class.valid_extension?(extension) unless self.class.valid_extension?(extension)
errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension)) errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
end end
end end
end
def file=(incoming_file) def file=(incoming_file)
unless incoming_file.nil? unless incoming_file.nil?
@ -436,15 +435,15 @@ class Attachment < ActiveRecord::Base
private private
def reuse_existing_file_if_possible def reuse_existing_file_if_possible
original_diskfile = nil original_diskfile = diskfile
original_filename = disk_filename
reused = with_lock do reused = with_lock do
if existing = Attachment if existing = Attachment
.where(digest: self.digest, filesize: self.filesize) .where(digest: self.digest, filesize: self.filesize)
.where('id <> ? and disk_filename <> ?', .where.not(disk_filename: original_filename)
self.id, self.disk_filename) .order(:id)
.first .last
existing.with_lock do existing.with_lock do
original_diskfile = self.diskfile
existing_diskfile = existing.diskfile existing_diskfile = existing.diskfile
if File.readable?(original_diskfile) && if File.readable?(original_diskfile) &&
File.readable?(existing_diskfile) && File.readable?(existing_diskfile) &&
@ -455,7 +454,7 @@ class Attachment < ActiveRecord::Base
end end
end end
end end
if reused if reused && Attachment.where(disk_filename: original_filename).none?
File.delete(original_diskfile) File.delete(original_diskfile)
end end
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound

View file

@ -201,6 +201,11 @@ class Issue < ActiveRecord::Base
user_tracker_permission?(user, :delete_issues) user_tracker_permission?(user, :delete_issues)
end end
# Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_deletable?
def attachments_deletable?(user=User.current)
attributes_editable?(user)
end
def initialize(attributes=nil, *args) def initialize(attributes=nil, *args)
super super
if new_record? if new_record?
@ -471,7 +476,6 @@ class Issue < ActiveRecord::Base
'custom_field_values', 'custom_field_values',
'custom_fields', 'custom_fields',
'lock_version', 'lock_version',
'notes',
:if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user)}) :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user)})
safe_attributes( safe_attributes(
'notes', 'notes',
@ -722,7 +726,7 @@ class Issue < ActiveRecord::Base
errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start) errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
end end
if fixed_version if project && fixed_version
if !assignable_versions.include?(fixed_version) if !assignable_versions.include?(fixed_version)
errors.add :fixed_version_id, :inclusion errors.add :fixed_version_id, :inclusion
elsif reopening? && fixed_version.closed? elsif reopening? && fixed_version.closed?
@ -737,7 +741,7 @@ class Issue < ActiveRecord::Base
end end
end end
if assigned_to_id_changed? && assigned_to_id.present? if project && assigned_to_id_changed? && assigned_to_id.present?
unless assignable_users.include?(assigned_to) unless assignable_users.include?(assigned_to)
errors.add :assigned_to_id, :invalid errors.add :assigned_to_id, :invalid
end end
@ -937,6 +941,8 @@ class Issue < ActiveRecord::Base
# Users the issue can be assigned to # Users the issue can be assigned to
def assignable_users def assignable_users
return [] if project.nil?
users = project.assignable_users(tracker).to_a users = project.assignable_users(tracker).to_a
users << author if author && author.active? users << author if author && author.active?
if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was) if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was)
@ -948,6 +954,7 @@ class Issue < ActiveRecord::Base
# Versions that the issue can be assigned to # Versions that the issue can be assigned to
def assignable_versions def assignable_versions
return @assignable_versions if @assignable_versions return @assignable_versions if @assignable_versions
return [] if project.nil?
versions = project.shared_versions.open.to_a versions = project.shared_versions.open.to_a
if fixed_version if fixed_version
@ -1704,12 +1711,12 @@ class Issue < ActiveRecord::Base
if children.any? if children.any?
child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0} child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
if child_with_total_estimated_hours.any? if child_with_total_estimated_hours.any?
average = child_with_total_estimated_hours.map(&:total_estimated_hours).sum.to_f / child_with_total_estimated_hours.count average = child_with_total_estimated_hours.map(&:total_estimated_hours).sum.to_d / child_with_total_estimated_hours.count
else else
average = 1.0 average = 1.0.to_d
end end
done = children.map {|c| done = children.map {|c|
estimated = c.total_estimated_hours.to_f estimated = (c.total_estimated_hours || 0.0).to_d
estimated = average unless estimated > 0.0 estimated = average unless estimated > 0.0
ratio = c.closed? ? 100 : (c.done_ratio || 0) ratio = c.closed? ? 100 : (c.done_ratio || 0)
estimated * ratio estimated * ratio
@ -1734,8 +1741,8 @@ class Issue < ActiveRecord::Base
# a different project and that is not systemwide shared # a different project and that is not systemwide shared
Issue.joins(:project, :fixed_version). Issue.joins(:project, :fixed_version).
where("#{Issue.table_name}.fixed_version_id IS NOT NULL" + where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + " AND #{Issue.table_name}.project_id <> #{::Version.table_name}.project_id" +
" AND #{Version.table_name}.sharing <> 'system'"). " AND #{::Version.table_name}.sharing <> 'system'").
where(conditions).each do |issue| where(conditions).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil? next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version) unless issue.project.shared_versions.include?(issue.fixed_version)
@ -1756,7 +1763,7 @@ class Issue < ActiveRecord::Base
# Callback on file attachment # Callback on file attachment
def attachment_added(attachment) def attachment_added(attachment)
if current_journal && !attachment.new_record? if current_journal && !attachment.new_record? && !copy?
current_journal.journalize_attachment(attachment, :added) current_journal.journalize_attachment(attachment, :added)
end end
end end

View file

@ -513,7 +513,8 @@ class IssueQuery < Query
def sql_for_fixed_version_status_field(field, operator, value) def sql_for_fixed_version_status_field(field, operator, value)
where = sql_for_field(field, operator, value, Version.table_name, "status") where = sql_for_field(field, operator, value, Version.table_name, "status")
version_ids = versions(:conditions => [where]).map(&:id) version_id_scope = project ? project.shared_versions : Version.visible
version_ids = version_id_scope.where(where).pluck(:id)
nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : '' nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
"(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})" "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
@ -521,7 +522,8 @@ class IssueQuery < Query
def sql_for_fixed_version_due_date_field(field, operator, value) def sql_for_fixed_version_due_date_field(field, operator, value)
where = sql_for_field(field, operator, value, Version.table_name, "effective_date") where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
version_ids = versions(:conditions => [where]).map(&:id) version_id_scope = project ? project.shared_versions : Version.visible
version_ids = version_id_scope.where(where).pluck(:id)
nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : '' nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
"(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})" "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"

7
app/models/mail_handler.rb Executable file → Normal file
View file

@ -225,9 +225,8 @@ class MailHandler < ActionMailer::Base
# check permission # check permission
unless handler_options[:no_permission_check] unless handler_options[:no_permission_check]
unless user.allowed_to?(:add_issue_notes, issue.project) || unless issue.notes_addable?
user.allowed_to?(:edit_issues, issue.project) raise UnauthorizedAction, "not allowed to add notes on issues to project [#{issue.project.name}]"
raise UnauthorizedAction, "not allowed to add notes on issues to project [#{project.name}]"
end end
end end
@ -276,7 +275,7 @@ class MailHandler < ActionMailer::Base
end end
unless handler_options[:no_permission_check] unless handler_options[:no_permission_check]
raise UnauthorizedAction, "not allowed to add messages to project [#{project.name}]" unless user.allowed_to?(:add_messages, message.project) raise UnauthorizedAction, "not allowed to add messages to project [#{message.project.name}]" unless user.allowed_to?(:add_messages, message.project)
end end
if !message.locked? if !message.locked?

View file

@ -883,6 +883,17 @@ class Project < ActiveRecord::Base
end end
end end
# Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
# so that custom values that are not editable are not validated (eg. a custom field that
# is marked as required should not trigger a validation error if the user is not allowed
# to edit this field).
def validate_custom_field_values
user = User.current
if new_record? || custom_field_values_changed?
editable_custom_field_values(user).each(&:validate_value)
end
end
# Returns the custom_field_values that can be edited by the given user # Returns the custom_field_values that can be edited by the given user
def editable_custom_field_values(user=nil) def editable_custom_field_values(user=nil)
visible_custom_field_values(user) visible_custom_field_values(user)

View file

@ -70,7 +70,7 @@ class ProjectQuery < Query
def available_columns def available_columns
return @available_columns if @available_columns return @available_columns if @available_columns
@available_columns = self.class.available_columns.dup @available_columns = self.class.available_columns.dup
@available_columns += ProjectCustomField.visible. @available_columns += project_custom_fields.visible.
map {|cf| QueryCustomFieldColumn.new(cf) } map {|cf| QueryCustomFieldColumn.new(cf) }
@available_columns @available_columns
end end

View file

@ -51,7 +51,7 @@ class QueryColumn
# Returns true if the column is sortable, otherwise false # Returns true if the column is sortable, otherwise false
def sortable? def sortable?
!@sortable.nil? @sortable.present?
end end
def sortable def sortable
@ -501,7 +501,7 @@ class Query < ActiveRecord::Base
if has_filter?(field) || !filter.remote if has_filter?(field) || !filter.remote
options[:values] = filter.values options[:values] = filter.values
if options[:values] && values_for(field) if options[:values] && values_for(field)
missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last) missing = Array(values_for(field)).select(&:present?) - options[:values].map{|v| v[1]}
if missing.any? && respond_to?(method = "find_#{field}_filter_values") if missing.any? && respond_to?(method = "find_#{field}_filter_values")
options[:values] += send(method, missing) options[:values] += send(method, missing)
end end
@ -609,13 +609,18 @@ class Query < ActiveRecord::Base
if project if project
project.rolled_up_custom_fields project.rolled_up_custom_fields
else else
IssueCustomField.all IssueCustomField.sorted
end end
end end
# Returns a scope of project custom fields that are available as columns or filters # Returns a scope of project custom fields that are available as columns or filters
def project_custom_fields def project_custom_fields
ProjectCustomField.all ProjectCustomField.sorted
end
# Returns a scope of time entry custom fields that are available as columns or filters
def time_entry_custom_fields
TimeEntryCustomField.sorted
end end
# Returns a scope of project statuses that are available as columns or filters # Returns a scope of project statuses that are available as columns or filters
@ -866,10 +871,10 @@ class Query < ActiveRecord::Base
def project_statement def project_statement
project_clauses = [] project_clauses = []
active_subprojects_ids = [] subprojects_ids = []
active_subprojects_ids = project.descendants.active.map(&:id) if project subprojects_ids = project.descendants.where.not(status: Project::STATUS_ARCHIVED).ids if project
if active_subprojects_ids.any? if subprojects_ids.any?
if has_filter?("subproject_id") if has_filter?("subproject_id")
case operator_for("subproject_id") case operator_for("subproject_id")
when '=' when '='
@ -878,7 +883,7 @@ class Query < ActiveRecord::Base
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
when '!' when '!'
# exclude the selected subprojects # exclude the selected subprojects
ids = [project.id] + active_subprojects_ids - values_for("subproject_id").map(&:to_i) ids = [project.id] + subprojects_ids - values_for("subproject_id").map(&:to_i)
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
when '!*' when '!*'
# main project only # main project only

View file

@ -461,6 +461,10 @@ class Repository < ActiveRecord::Base
scope scope
end end
def valid_name?(name)
scm.valid_name?(name)
end
protected protected
# Validates repository url based against an optional regular expression # Validates repository url based against an optional regular expression

View file

@ -100,7 +100,8 @@ class Setting < ActiveRecord::Base
v = read_attribute(:value) v = read_attribute(:value)
# Unserialize serialized settings # Unserialize serialized settings
if available_settings[name]['serialized'] && v.is_a?(String) if available_settings[name]['serialized'] && v.is_a?(String)
v = YAML::load(v) # YAML.load works as YAML.safe_load if Psych >= 4.0 is installed
v = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(v) : YAML.load(v)
v = force_utf8_strings(v) v = force_utf8_strings(v)
end end
v = v.to_sym if available_settings[name]['format'] == 'symbol' && !v.blank? v = v.to_sym if available_settings[name]['format'] == 'symbol' && !v.blank?

View file

@ -113,6 +113,13 @@ class TimeEntry < ActiveRecord::Base
self.project_id = issue.project_id self.project_id = issue.project_id
end end
@invalid_issue_id = nil @invalid_issue_id = nil
elsif user.allowed_to?(:log_time, issue.project) && issue.assigned_to_id_changed? && issue.previous_assignee == User.current
current_assignee = issue.assigned_to
issue.assigned_to = issue.previous_assignee
unless issue.visible?(user)
@invalid_issue_id = issue_id
end
issue.assigned_to = current_assignee
else else
@invalid_issue_id = issue_id @invalid_issue_id = issue_id
end end
@ -122,7 +129,14 @@ class TimeEntry < ActiveRecord::Base
else else
@invalid_user_id = nil @invalid_user_id = nil
end end
# Delete assigned custom fields not visible by the user
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
self.custom_field_values.delete_if do |c|
!editable_custom_field_ids.include?(c.custom_field.id.to_s)
end end
end
attrs attrs
end end
@ -193,7 +207,7 @@ class TimeEntry < ActiveRecord::Base
# Returns the custom_field_values that can be edited by the given user # Returns the custom_field_values that can be edited by the given user
def editable_custom_field_values(user=nil) def editable_custom_field_values(user=nil)
visible_custom_field_values visible_custom_field_values(user)
end end
# Returns the custom fields that can be edited by the given user # Returns the custom fields that can be edited by the given user

View file

@ -89,7 +89,7 @@ class TimeEntryQuery < Query
activities = (project ? project.activities : TimeEntryActivity.shared) activities = (project ? project.activities : TimeEntryActivity.shared)
add_available_filter( add_available_filter(
"activity_id", "activity_id",
:type => :list, :values => activities.map {|a| [a.name, a.id.to_s]} :type => :list, :values => activities.map {|a| [a.name, (a.parent_id || a.id).to_s]}
) )
add_available_filter( add_available_filter(
"project.status", "project.status",
@ -101,7 +101,7 @@ class TimeEntryQuery < Query
add_available_filter "comments", :type => :text add_available_filter "comments", :type => :text
add_available_filter "hours", :type => :float add_available_filter "hours", :type => :float
add_custom_fields_filters(TimeEntryCustomField) add_custom_fields_filters(time_entry_custom_fields)
add_associations_custom_fields_filters :project add_associations_custom_fields_filters :project
add_custom_fields_filters(issue_custom_fields, :issue) add_custom_fields_filters(issue_custom_fields, :issue)
add_associations_custom_fields_filters :user add_associations_custom_fields_filters :user
@ -110,11 +110,11 @@ class TimeEntryQuery < Query
def available_columns def available_columns
return @available_columns if @available_columns return @available_columns if @available_columns
@available_columns = self.class.available_columns.dup @available_columns = self.class.available_columns.dup
@available_columns += TimeEntryCustomField.visible. @available_columns += time_entry_custom_fields.visible.
map {|cf| QueryCustomFieldColumn.new(cf) } map {|cf| QueryCustomFieldColumn.new(cf) }
@available_columns += issue_custom_fields.visible. @available_columns += issue_custom_fields.visible.
map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) } map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
@available_columns += ProjectCustomField.visible. @available_columns += project_custom_fields.visible.
map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) } map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) }
@available_columns @available_columns
end end

View file

@ -115,12 +115,14 @@ class Token < ActiveRecord::Base
return nil unless action.present? && /\A[a-z0-9]+\z/i.match?(key) return nil unless action.present? && /\A[a-z0-9]+\z/i.match?(key)
token = Token.find_by(:action => action, :value => key) token = Token.find_by(:action => action, :value => key)
if token && (token.action == action) && (token.value == key) && token.user return unless token
if validity_days.nil? || (token.created_on > validity_days.days.ago) return unless token.action == action
return unless ActiveSupport::SecurityUtils.secure_compare(token.value.to_s, key)
return unless token.user
return unless validity_days.nil? || (token.created_on > validity_days.days.ago)
token token
end end
end
end
def self.generate_token_value def self.generate_token_value
Redmine::Utils.random_hex(20) Redmine::Utils.random_hex(20)

View file

@ -155,11 +155,7 @@ class WikiPage < ActiveRecord::Base
end end
def content_for_version(version=nil) def content_for_version(version=nil)
if content (content && version) ? content.versions.find_by_version(version.to_i) : content
result = content.versions.find_by_version(version.to_i) if version
result ||= content
result
end
end end
def diff(version_to=nil, version_from=nil) def diff(version_to=nil, version_from=nil)

View file

@ -1,7 +1,6 @@
<%= call_hook :view_account_login_top %> <%= call_hook :view_account_login_top %>
<div id="login-form"> <div id="login-form">
<h2><%= l(:label_login) %></h2>
<%= form_tag(signin_path, onsubmit: 'return keepAnchorOnSignIn(this);') do %> <%= form_tag(signin_path, onsubmit: 'return keepAnchorOnSignIn(this);') do %>
<%= back_url_hidden_field_tag %> <%= back_url_hidden_field_tag %>

View file

@ -1,5 +1,5 @@
<p> <p>
<%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), :rows => 15 %> <%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), :rows => 15, :required => true %>
<em class="info"><%= l(:text_custom_field_possible_values_info) %></em> <em class="info"><%= l(:text_custom_field_possible_values_info) %></em>
</p> </p>
<p><%= f.text_field(:default_value) %></p> <p><%= f.text_field(:default_value) %></p>

View file

@ -13,12 +13,10 @@
<% @document.custom_field_values.each do |value| %> <% @document.custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :document, value %></p> <p><%= custom_field_tag_with_label :document, value %></p>
<% end %> <% end %>
<% if @document.new_record? %>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form', :locals => {:container => @document} %></p>
<% end %>
</div> </div>
<%= wikitoolbar_for 'document_description' %> <%= wikitoolbar_for 'document_description' %>
<% if @document.new_record? %>
<div class="box tabular">
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form', :locals => {:container => @document} %></p>
</div>
<% end %>

View file

@ -177,14 +177,17 @@
<% end %> <% end %>
<% end %> <% end %>
</td> </td>
<% @query.columns.each do |column| %> <%
<% next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name) %> @query.columns.each do |column|
<td class="gantt_<%= column.name %>_column gantt_selected_column <%= 'last_gantt_selected_column' if @query.columns.last == column %>" id="<%= column.name %>"> next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name)
column_name = column.name.to_s.tr('.', '_')
%>
<td class="gantt_<%= column_name %>_column gantt_selected_column <%= 'last_gantt_selected_column' if @query.columns.last == column %>" id="<%= column_name %>">
<% <%
style = "position: relative;" style = "position: relative;"
style += "height: #{t_height + 24}px;" style += "height: #{t_height + 24}px;"
%> %>
<%= content_tag(:div, :style => style, :class => "gantt_#{column.name}_container gantt_selected_column_container") do %> <%= content_tag(:div, :style => style, :class => "gantt_#{column_name}_container gantt_selected_column_container") do %>
<% <%
style = "height: #{t_height}px;" style = "height: #{t_height}px;"
style += 'overflow: hidden;' style += 'overflow: hidden;'
@ -195,7 +198,7 @@
style += 'background: #eee;' style += 'background: #eee;'
%> %>
<%= content_tag(:div, content_tag(:p, column.caption, :class => 'gantt_hdr_selected_column_name'), :style => style, :class => "gantt_hdr") %> <%= content_tag(:div, content_tag(:p, column.caption, :class => 'gantt_hdr_selected_column_name'), :style => style, :class => "gantt_hdr") %>
<%= content_tag(:div, :class => "gantt_#{column.name} gantt_selected_column_content") do %> <%= content_tag(:div, :class => "gantt_#{column_name} gantt_selected_column_content") do %>
<%= @gantt.selected_column_content({:column => column, :top => headers_height + 8, :zoom => zoom, :g_width => g_width}).html_safe %> <%= @gantt.selected_column_content({:column => column, :top => headers_height + 8, :zoom => zoom, :g_width => g_width}).html_safe %>
<% end %> <% end %>
<% end %> <% end %>

View file

@ -21,7 +21,7 @@
</div> </div>
</div> </div>
<p><%= time_entry.text_field :comments, :size => 60 %></p> <p><%= time_entry.text_field :comments, :size => 60 %></p>
<% @time_entry.custom_field_values.each do |value| %> <% @time_entry.editable_custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :time_entry, value %></p> <p><%= custom_field_tag_with_label :time_entry, value %></p>
<% end %> <% end %>
<% end %> <% end %>

View file

@ -13,7 +13,7 @@
<%= link_to_if_authorized l(:label_settings), <%= link_to_if_authorized l(:label_settings),
{:controller => 'projects', :action => 'settings', :id => @project, :tab => 'issues'}, {:controller => 'projects', :action => 'settings', :id => @project, :tab => 'issues'},
:class => 'icon icon-settings' if User.current.allowed_to?(:manage_categories, @project) %> :class => 'icon icon-settings' if User.current.allowed_to?(:edit_project, @project) %>
<% end %> <% end %>
</div> </div>

View file

@ -16,7 +16,9 @@
:rev => changeset.identifier) %>) :rev => changeset.identifier) %>)
<% end %></p> <% end %></p>
<div class="wiki changeset-comments"><%= format_changeset_comments changeset %></div> <div class="wiki changeset-comments">
<%= format_changeset_comments changeset %>
</div>
</div> </div>
<%= call_hook(:view_issues_history_changeset_bottom, { :changeset => changeset }) %> <%= call_hook(:view_issues_history_changeset_bottom, { :changeset => changeset }) %>
<% end %> <% end %>

View file

@ -4,25 +4,20 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title><%= html_title %></title> <title><%= html_title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="<%= Redmine::Info.app_name %>" /> <meta name="description" content="<%= Redmine::Info.app_name %>" />
<meta name="keywords" content="issue,bug,tracker" /> <meta name="keywords" content="issue,bug,tracker" />
<%= csrf_meta_tag %> <%= csrf_meta_tag %>
<%= favicon %> <%= favicon %>
<%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'cookieconsent.min', 'tribute-3.7.3', 'application', 'responsive', :media => 'all' %> <%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'tribute-3.7.3', 'application', 'responsive', :media => 'all' %>
<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %> <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
<% is_welcome = !User.current.logged? && current_page?(:controller => 'welcome', :action => 'index') %>
<%= stylesheet_link_tag 'frontpage', :media => 'all' if is_welcome %>
<%= javascript_heads %> <%= javascript_heads %>
<script src="/themes/circlepro/javascripts/cookieconsent.min.js"></script>
<%= heads_for_theme %> <%= heads_for_theme %>
<%= call_hook :view_layouts_base_html_head %> <%= call_hook :view_layouts_base_html_head %>
<!-- page specific tags --> <!-- page specific tags -->
<%= yield :header_tags -%> <%= yield :header_tags -%>
</head> </head>
<body class="<%= body_css_classes %><%= ' is-preload' if is_welcome %>"> <body class="<%= body_css_classes %>">
<%= call_hook :view_layouts_base_body_top %> <%= call_hook :view_layouts_base_body_top %>
<div id="wrapper"> <div id="wrapper">
@ -65,31 +60,20 @@
<div id="wrapper2"> <div id="wrapper2">
<div id="wrapper3"> <div id="wrapper3">
<div id="top-menu"> <div id="top-menu">
<div id="wrapper-top-menu">
<ul class="social-menu">
<li class="social-link-blog"><a href="https://manuel.cillero.es" title="<%= l(:link_my_blog) %>" class="icon-blog"><span><%= l(:link_my_blog) %></span></a></li>
<li class="social-link-mastodon"><a href="https://noc.social/@manuelcillero" title="Mastodon" target="_blank" class="icon-mastodon"><span>Mastodon</span></a></li>
<li class="social-link-linkedin"><a href="https://es.linkedin.com/in/manuelcillero" title="Linkedin" target="_blank" class="icon-linkedin"><span>Linkedin</span></a></li>
<li class="social-link-github"><a href="https://github.com/manuelcillero" title="Github" target="_blank" class="icon-github"><span>Github</span></a></li>
<li class="social-link-mail"><a href="https://manuel.cillero.es/contacto/#suitepro" title="Mail" class="icon-mail"><span>Mail</span></a></li>
</ul>
<div id="account"> <div id="account">
<%= render_menu :account_menu -%> <%= render_menu :account_menu -%>
</div> </div>
<%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %> <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
<%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%> <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
</div> </div>
</div>
<div id="header"> <div id="header">
<a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a> <a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a>
<div id="wrapper-header">
<% if User.current.logged? || !Setting.login_required? %> <% if User.current.logged? || !Setting.login_required? %>
<div id="quick-search" class="hide-when-print"> <div id="quick-search">
<%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %> <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
<%= hidden_field_tag 'scope', default_search_project_scope, :id => nil %> <%= hidden_field_tag 'scope', default_search_project_scope, :id => nil %>
<%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %> <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
@ -107,18 +91,15 @@
<% end %> <% end %>
<h1><%= page_header_title %></h1> <h1><%= page_header_title %></h1>
</div>
<% if display_main_menu?(@project) %> <% if display_main_menu?(@project) %>
<div id="main-menu" class="tabs"> <div id="main-menu" class="tabs">
<div id="wrapper-main-menu">
<%= render_main_menu(@project) %> <%= render_main_menu(@project) %>
<div class="tabs-buttons" style="display:none;"> <div class="tabs-buttons" style="display:none;">
<button class="tab-left" onclick="moveTabLeft(this); return false;"></button> <button class="tab-left" onclick="moveTabLeft(this); return false;"></button>
<button class="tab-right" onclick="moveTabRight(this); return false;"></button> <button class="tab-right" onclick="moveTabRight(this); return false;"></button>
</div> </div>
</div> </div>
</div>
<% end %> <% end %>
</div> </div>
@ -135,54 +116,16 @@
<div style="clear:both;"></div> <div style="clear:both;"></div>
</div> </div>
</div> </div>
<div id="footer">
</div> <!-- #wrapper3 --> Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2022 Jean-Philippe Lang
</div>
<a href="#" id="scrollup" class="hide-when-print" style="display: none;"><%=l(:label_sort_higher)%></a><%= javascript_tag "$('#scrollup').click(function(){$('html,body').animate({scrollTop:0},600);return false;});" %> </div>
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div> <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
<div id="ajax-modal" style="display:none;"></div> <div id="ajax-modal" style="display:none;"></div>
<div id="footer">
<div id="wrapper-footer">
<ul class="social-menu">
<li class="social-link-blog"><a href="https://manuel.cillero.es" title="<%= l(:link_my_blog) %>" class="icon-blog"><span><%= l(:link_my_blog) %></span></a></li>
<li class="social-link-mastodon"><a href="https://noc.social/@manuelcillero" title="Mastodon" target="_blank" class="icon-mastodon"><span>Mastodon</span></a></li>
<li class="social-link-linkedin"><a href="https://es.linkedin.com/in/manuelcillero" title="Linkedin" target="_blank" class="icon-linkedin"><span>Linkedin</span></a></li>
<li class="social-link-github"><a href="https://github.com/manuelcillero" title="Github" target="_blank" class="icon-github"><span>Github</span></a></li>
<li class="social-link-mail"><a href="https://manuel.cillero.es/contacto/#suitepro" title="Mail" class="icon-mail"><span>Mail</span></a></li>
</ul>
<div class="bgl"><div class="bgr">
<%= Time.current.year %> &copy; SuitePro (powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %>)
<div id="legal">
<span class="legal-legal"><a href="/projects/suitepro/wiki/Legal"><%= l(:label_legal) %></a></span>
<span class="legal-terms"> &nbsp;|&nbsp; <a href="/projects/suitepro/wiki/Condiciones_de_uso"><%= l(:label_legal_terms) %></a></span>
<span class="legal-privacy"> &nbsp;|&nbsp; <a href="/projects/suitepro/wiki/Política_de_privacidad"><%= l(:label_legal_privacy) %></a></span>
<span class="legal-cookies"> &nbsp;|&nbsp; <a href="/projects/suitepro/wiki/Política_de_cookies"><%= l(:label_legal_cookies) %></a></span>
</div>
</div></div>
</div> </div>
</div> </div>
</div> <!-- #wrapper2 -->
</div> <!-- #wrapper -->
<%= call_hook :view_layouts_base_body_bottom %> <%= call_hook :view_layouts_base_body_bottom %>
<script>
//<![CDATA[
window.addEventListener("load", function(){
window.cookieconsent.initialise({
"palette": { "popup": { "background": "rgba(20,20,20,0.8)" }, "button": { "background": "#fff" } },
"theme": "classic",
"position": "bottom-left",
"content": { "message": "<a href='https://suitepro.cillero.es'>SuitePro</a> requiere el uso de cookies para ofrecer la mejor experiencia de acceso a sus contenidos. Puedes aceptar su uso o abandonar la página si lo deseas.", "dismiss": "ACEPTO SU USO", "link": "Más información", "href": "/projects/suitepro/wiki/Pol%C3%ADtica_de_cookies", "target": "_self" }
})});
//]]>
</script>
</body> </body>
</html> </html>

View file

@ -31,7 +31,7 @@
<%= textilizable @project.description %> <%= textilizable @project.description %>
</div> </div>
<% end %> <% end %>
<% if @project.homepage.present? || @project.visible_custom_field_values.any?(&:present?) %> <% if @project.homepage.present? || @project.visible_custom_field_values.any? { |o| o.value.present? } %>
<ul> <ul>
<% unless @project.homepage.blank? %> <% unless @project.homepage.blank? %>
<li><span class="label"><%=l(:field_homepage)%>:</span> <%= link_to_if uri_with_safe_scheme?(@project.homepage), @project.homepage, @project.homepage %></li> <li><span class="label"><%=l(:field_homepage)%>:</span> <%= link_to_if uri_with_safe_scheme?(@project.homepage), @project.homepage, @project.homepage %></li>

View file

@ -36,7 +36,7 @@
<%= javascript_tag do %> <%= javascript_tag do %>
$(document).ready(function(){ $(document).ready(function(){
$('.query-columns').closest('form').submit(function(){ $('.query-columns').closest('form').submit(function(){
$('#<%= selected_tag_id %> option').prop('selected', true); $('#<%= selected_tag_id %> option:not(:disabled)').prop('selected', true);
}); });
}); });
<% end %> <% end %>

View file

@ -25,10 +25,10 @@
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<div class="issue-report-graph"> <div class="issue-report-graph hide-when-print">
<canvas id="issues_by_<%= params[:detail] %>"></canvas> <canvas id="issues_by_<%= params[:detail] %>"></canvas>
</div> </div>
<div class="issue-report-graph"> <div class="issue-report-graph hide-when-print">
<canvas id="issues_by_status"></canvas> <canvas id="issues_by_status"></canvas>
</div> </div>
<%= javascript_tag do %> <%= javascript_tag do %>

View file

@ -33,7 +33,9 @@
</div> </div>
<div class="wiki changeset-comments"><%= format_changeset_comments @changeset %></div> <div class="wiki changeset-comments">
<%= format_changeset_comments @changeset %>
</div>
<% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %> <% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %>
<%= render :partial => 'related_issues' %> <%= render :partial => 'related_issues' %>

View file

@ -32,13 +32,13 @@
<% end %> <% end %>
<div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>"> <div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>">
<p> <p>
<%= f.password_field :password, :required => true, :size => 25 %> <%= f.password_field :password, :required => @user.new_record?, :size => 25 %>
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em> <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em>
<% if Setting.password_required_char_classes.any? %> <% if Setting.password_required_char_classes.any? %>
<em class="info"><%= l(:text_characters_must_contain, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em> <em class="info"><%= l(:text_characters_must_contain, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em>
<% end %> <% end %>
</p> </p>
<p><%= f.password_field :password_confirmation, :required => true, :size => 25 %></p> <p><%= f.password_field :password_confirmation, :required => @user.new_record?, :size => 25 %></p>
<p><%= f.check_box :generate_password %></p> <p><%= f.check_box :generate_password %></p>
<p><%= f.check_box :must_change_passwd %></p> <p><%= f.check_box :must_change_passwd %></p>
</div> </div>

View file

@ -7,7 +7,7 @@
<%= <%=
page_title = title [l(:label_user_plural), users_path], @user.login page_title = title [l(:label_user_plural), users_path], @user.login
page_title.insert(page_title.rindex(' ') + 1, avatar(@user)) page_title.insert(page_title.rindex(' ') + 1, avatar(@user).to_s)
%> %>
<%= render_tabs user_settings_tabs %> <%= render_tabs user_settings_tabs %>

View file

@ -10,3 +10,6 @@ Disallow: <%= url_for(issues_gantt_path) %>
Disallow: <%= url_for(issues_calendar_path) %> Disallow: <%= url_for(issues_calendar_path) %>
Disallow: <%= url_for(activity_path) %> Disallow: <%= url_for(activity_path) %>
Disallow: <%= url_for(search_path) %> Disallow: <%= url_for(search_path) %>
Disallow: <%= url_for(issues_path(:sort => '')) %>
Disallow: <%= url_for(issues_path(:query_id => '')) %>
Disallow: <%= url_for(issues_path) %>?*set_filter=

View file

@ -5,7 +5,7 @@ api.wiki_page do
end end
api.text @content.text api.text @content.text
api.version @content.version api.version @content.version
api.author(:id => @content.author_id, :name => @content.author.name) api.author(:id => @content.author_id, :name => @content.author.name) unless @content.author_id.nil?
api.comments @content.comments api.comments @content.comments
api.created_on @page.created_on api.created_on @page.created_on
api.updated_on @content.updated_on api.updated_on @content.updated_on

View file

@ -61,8 +61,7 @@
<%= render(:partial => "wiki/content", :locals => {:content => @content}) %> <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
<% if @page.attachments.length > 0 || (@editable && authorize_for('wiki', 'add_attachment')) %> <fieldset class="collapsible collapsed hide-when-print">
<fieldset class="collapsible collapsed<% if @page.attachments.length == 0 %> hide-when-print<% end %>">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_attachment_plural) %> (<%= @page.attachments.length %>)</legend> <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_attachment_plural) %> (<%= @page.attachments.length %>)</legend>
<div style="display: none;"> <div style="display: none;">
@ -82,7 +81,6 @@
<% end %> <% end %>
</div> </div>
</fieldset> </fieldset>
<% end %>
<p class="wiki-update-info"> <p class="wiki-update-info">
<% if User.current.allowed_to?(:view_wiki_edits, @project) %> <% if User.current.allowed_to?(:view_wiki_edits, @project) %>

View file

@ -4,7 +4,7 @@
<th> <th>
<%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled)')", <%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled)')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:class => 'icon-only icon-checked') %> :class => 'no-tooltip icon-only icon-checked') %>
<%=l(:label_current_status)%> <%=l(:label_current_status)%>
</th> </th>
<th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th> <th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
@ -15,7 +15,7 @@
<td style="width:<%= 75 / @statuses.size %>%;"> <td style="width:<%= 75 / @statuses.size %>%;">
<%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled).new-status-#{new_status.id}')", <%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled).new-status-#{new_status.id}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:class => 'icon-only icon-checked') %> :class => 'no-tooltip icon-only icon-checked') %>
<%= new_status.name %> <%= new_status.name %>
</td> </td>
<% end %> <% end %>
@ -29,7 +29,7 @@
<td class="name"> <td class="name">
<%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled).old-status-#{old_status.try(:id) || 0}')", <%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled).old-status-#{old_status.try(:id) || 0}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:class => 'icon-only icon-checked') %> :class => 'no-tooltip icon-only icon-checked') %>
<% if old_status %> <% if old_status %>
<% old_status_name = old_status.name %> <% old_status_name = old_status.name %>
<%= old_status_name %> <%= old_status_name %>
@ -40,7 +40,7 @@
</td> </td>
<% for new_status in @statuses -%> <% for new_status in @statuses -%>
<% checked = (old_status == new_status) || (transition_counts[[old_status, new_status]] > 0) %> <% checked = (old_status == new_status) || (transition_counts[[old_status, new_status]] > 0) %>
<td class="<%= checked ? 'enabled' : '' %>" title="<%= old_status_name %> &#187; <%= new_status.name %>"> <td class="no-tooltip <%= checked ? 'enabled' : '' %>" title="<%= old_status_name %> &#187; <%= new_status.name %>">
<%= transition_tag transition_counts[[old_status, new_status]], old_status, new_status, name %> <%= transition_tag transition_counts[[old_status, new_status]], old_status, new_status, name %>
</td> </td>
<% end -%> <% end -%>

View file

@ -67,7 +67,7 @@
<% for status in @statuses -%> <% for status in @statuses -%>
<td class="<%= @permissions[status.id][field].try(:join, ' ') %>" title="<%= name %> (<%= status.name %>)"> <td class="<%= @permissions[status.id][field].try(:join, ' ') %>" title="<%= name %> (<%= status.name %>)">
<%= field_permission_tag(@permissions, status, field, @roles) %> <%= field_permission_tag(@permissions, status, field, @roles) %>
<% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %> <% unless status == @statuses.last %><a href="#" class="repeat-value" title="<%= l(:button_copy) %>">&#187;</a><% end %>
</td> </td>
<% end -%> <% end -%>
</tr> </tr>

View file

@ -79,7 +79,8 @@ module RedmineApp
config.session_store :cookie_store, config.session_store :cookie_store,
:key => '_redmine_session', :key => '_redmine_session',
:path => config.relative_url_root || '/' :path => config.relative_url_root || '/',
:same_site => :lax
if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb')) if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb')) instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))

View file

@ -213,3 +213,17 @@ module ActionView
end end
end end
end end
# https://github.com/rack/rack/pull/1703
# TODO: remove this when Rack is updated to 3.0.0
require 'rack'
module Rack
class RewindableInput
unless method_defined?(:size)
def size
make_rewindable unless @rewindable_io
@rewindable_io.size
end
end
end
end

View file

@ -416,7 +416,7 @@ en:
setting_timespan_format: Time span format setting_timespan_format: Time span format
setting_cross_project_issue_relations: Allow cross-project issue relations setting_cross_project_issue_relations: Allow cross-project issue relations
setting_cross_project_subtasks: Allow cross-project subtasks setting_cross_project_subtasks: Allow cross-project subtasks
setting_issue_list_default_columns: Isuses list defaults setting_issue_list_default_columns: Issues list defaults
setting_repositories_encodings: Attachments and repositories encodings setting_repositories_encodings: Attachments and repositories encodings
setting_emails_header: Email header setting_emails_header: Email header
setting_emails_footer: Email footer setting_emails_footer: Email footer
@ -1284,11 +1284,3 @@ en:
text_project_is_public_non_member: Public projects and their contents are available to all logged-in users. text_project_is_public_non_member: Public projects and their contents are available to all logged-in users.
text_project_is_public_anonymous: Public projects and their contents are openly available on the network. text_project_is_public_anonymous: Public projects and their contents are openly available on the network.
label_import_time_entries: Import time entries label_import_time_entries: Import time entries
link_my_blog: My Blog
label_legal: Legal notice
label_legal_terms: Terms of use
label_legal_privacy: Privacy policy
label_legal_cookies: Cookies policy

View file

@ -507,7 +507,7 @@ es:
label_loading: Cargando... label_loading: Cargando...
label_logged_as: Conectado como label_logged_as: Conectado como
label_login: Iniciar sesión label_login: Iniciar sesión
label_logout: Cerrar sesión label_logout: Terminar sesión
label_max_size: Tamaño máximo label_max_size: Tamaño máximo
label_me: yo mismo label_me: yo mismo
label_member: Miembro label_member: Miembro
@ -546,7 +546,7 @@ es:
label_optional_description: Descripción opcional label_optional_description: Descripción opcional
label_options: Opciones label_options: Opciones
label_overall_activity: Actividad global label_overall_activity: Actividad global
label_overview: Resumen label_overview: Vistazo
label_password_lost: ¿Olvidaste la contraseña? label_password_lost: ¿Olvidaste la contraseña?
label_permissions: Permisos label_permissions: Permisos
label_permissions_report: Informe de permisos label_permissions_report: Informe de permisos
@ -1216,7 +1216,7 @@ es:
mail_body_security_notification_notify_disabled: Se han desactivado las notificaciones para el correo electrónico %{value} mail_body_security_notification_notify_disabled: Se han desactivado las notificaciones para el correo electrónico %{value}
mail_body_settings_updated: ! 'Las siguientes opciones han sido actualizadas:' mail_body_settings_updated: ! 'Las siguientes opciones han sido actualizadas:'
field_remote_ip: Dirección IP field_remote_ip: Dirección IP
label_wiki_page_new: Nueva página label_wiki_page_new: Nueva pagina wiki
label_relations: Relaciones label_relations: Relaciones
button_filter: Filtro button_filter: Filtro
mail_body_password_updated: Su contraseña se ha cambiado. mail_body_password_updated: Su contraseña se ha cambiado.
@ -1231,121 +1231,114 @@ es:
label_font_default: Fuente por defecto label_font_default: Fuente por defecto
label_font_monospace: Fuente Monospaced label_font_monospace: Fuente Monospaced
label_font_proportional: Fuente Proportional label_font_proportional: Fuente Proportional
setting_timespan_format: Time span format setting_timespan_format: Formato de timespan
label_table_of_contents: Tabla de contenidos label_table_of_contents: Índice de contenidos
setting_commit_logs_formatting: Apply text formatting to commit messages setting_commit_logs_formatting: Aplicar formato de texto a los mensajes de commits
setting_mail_handler_enable_regex: Enable regular expressions setting_mail_handler_enable_regex: Habilitar expresiones regulares
error_move_of_child_not_possible: 'Subtask %{child} could not be moved to the new error_move_of_child_not_possible: 'Subtarea %{child} no ha podido ser movida al nuevo
project: %{errors}' proyecto: %{errors}'
error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted: Spent time cannot error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted: El tiempo dedicado no puede
be reassigned to an issue that is about to be deleted ser reasignado a una petición que va a ser borrada
setting_timelog_required_fields: Required fields for time logs setting_timelog_required_fields: Campos requeridos para imputación de tiempo
label_attribute_of_object: '%{object_name}''s %{name}' label_attribute_of_object: '%{object_name}''s %{name}'
label_user_mail_option_only_assigned: Only for things I watch or I am assigned to label_user_mail_option_only_assigned: Sólo para asuntos que sigo o en los que estoy asignado
label_user_mail_option_only_owner: Only for things I watch or I am the owner of label_user_mail_option_only_owner: Sólo para asuntos que sigo o de los que soy propietario
warning_fields_cleared_on_bulk_edit: Changes will result in the automatic deletion warning_fields_cleared_on_bulk_edit: Los cambios conllevarán la eliminación automática
of values from one or more fields on the selected objects de valores de uno o más campos de los objetos seleccionados
field_updated_by: Updated by field_updated_by: Actualizado por
field_last_updated_by: Last updated by field_last_updated_by: Última actualización de
field_full_width_layout: Full width layout field_full_width_layout: Diseño de ancho completo
label_last_notes: Last notes label_last_notes: Últimas notas
field_digest: Checksum field_digest: Checksum
field_default_assigned_to: Default assignee field_default_assigned_to: Asignado por defecto
setting_show_custom_fields_on_registration: Show custom fields on registration setting_show_custom_fields_on_registration: Mostrar campos personalizados en el registro
permission_view_news: View news
label_no_preview_alternative_html: No hay vista previa disponible. %{link} el archivo. permission_view_news: Ver noticias
label_no_preview_alternative_html: No hay vista previa disponible. %{link} el archivo en su lugar.
label_no_preview_download: Descargar label_no_preview_download: Descargar
setting_close_duplicate_issues: Close duplicate issues automatically setting_close_duplicate_issues: Cerrar peticiones duplicadas automáticamente
error_exceeds_maximum_hours_per_day: Cannot log more than %{max_hours} hours on the error_exceeds_maximum_hours_per_day: No se pueden registrar más de %{max_hours} horas en el
same day (%{logged_hours} hours have already been logged) mismo día (se han registrado ya %{logged_hours} horas)
setting_time_entry_list_defaults: Timelog list defaults setting_time_entry_list_defaults: Listas por defecto del Timelog
setting_timelog_accept_0_hours: Accept time logs with 0 hours setting_timelog_accept_0_hours: Aceptar registros de tiempo de 0 horas
setting_timelog_max_hours_per_day: Maximum hours that can be logged per day and user setting_timelog_max_hours_per_day: Número de horas máximo que se puede imputar por día y usuario
label_x_revisions: "Revisiones: %{count}" label_x_revisions: "%{count} revisiones"
error_can_not_delete_auth_source: This authentication mode is in use and cannot be error_can_not_delete_auth_source: Este modo de autenticación está en uso y no puede ser
deleted. eliminado.
button_actions: Acciones button_actions: Acciones
mail_body_lost_password_validity: Please be aware that you may change the password mail_body_lost_password_validity: Por favor, tenga en cuenta que sólo puede cambiar la contraseña
only once using this link. una vez usando este enlace.
text_login_required_html: When not requiring authentication, public projects and their text_login_required_html: Cuando no se requiera autenticación, los proyectos públicos y sus
contents are openly available on the network. You can <a href="%{anonymous_role_path}">edit contenidos están abiertos en la red. Puede <a href="%{anonymous_role_path}">editar
the applicable permissions</a>. los permisos aplicables</a>.
label_login_required_yes: 'Yes' label_login_required_yes: ''
label_login_required_no: No, allow anonymous access to public projects label_login_required_no: No, permitir acceso anónimo a los proyectos públicos
text_project_is_public_non_member: Public projects and their contents are available text_project_is_public_non_member: Los proyectos públicos y sus contenidos están disponibles
to all logged-in users. a todos los usuarios identificados.
text_project_is_public_anonymous: Public projects and their contents are openly available text_project_is_public_anonymous: Los proyectos públicos y sus contenidos están disponibles libremente
on the network. en la red.
label_version_and_files: Versions (%{count}) and Files label_version_and_files: Versionees (%{count}) y Ficheros
label_ldap: LDAP label_ldap: LDAP
label_ldaps_verify_none: LDAPS (without certificate check) label_ldaps_verify_none: LDAPS (sin chequeo de certificado)
label_ldaps_verify_peer: LDAPS label_ldaps_verify_peer: LDAPS
label_ldaps_warning: It is recommended to use an encrypted LDAPS connection with certificate label_ldaps_warning: Se recomienda usar una conexión LDAPS encriptada con chequeo de
check to prevent any manipulation during the authentication process. certificado para prevenir cualquier manipulación durante el proceso de autenticación.
label_nothing_to_preview: Nothing to preview label_nothing_to_preview: Nada que previsualizar
error_token_expired: This password recovery link has expired, please try again. error_token_expired: Este enlace de recuperación de contraseña ha expirado, por favor, inténtelo de nuevo.
error_spent_on_future_date: Cannot log time on a future date error_spent_on_future_date: No se puede registrar tiempo en una fecha futura
setting_timelog_accept_future_dates: Accept time logs on future dates setting_timelog_accept_future_dates: Aceptar registros de tiempo en fechas futuras
label_delete_link_to_subtask: Eliminar relación label_delete_link_to_subtask: Eliminar relación
error_not_allowed_to_log_time_for_other_users: You are not allowed to log time error_not_allowed_to_log_time_for_other_users: No está autorizado a registrar tiempo para
for other users otros usuarios
permission_log_time_for_other_users: Log spent time for other users permission_log_time_for_other_users: Registrar tiempo para otros usuarios
label_tomorrow: tomorrow label_tomorrow: mañana
label_next_week: next week label_next_week: próxima semana
label_next_month: next month label_next_month: próximo mes
text_role_no_workflow: No workflow defined for this role text_role_no_workflow: No se ha definido un flujo de trabajo para este perfil
text_status_no_workflow: No tracker uses this status in the workflows text_status_no_workflow: Ningún tipo de petición utiliza este estado en los flujos de trabajo
setting_mail_handler_preferred_body_part: Preferred part of multipart (HTML) emails setting_mail_handler_preferred_body_part: Parte preferida de los correos electrónicos multiparte (HTML)
setting_show_status_changes_in_mail_subject: Show status changes in issue mail notifications setting_show_status_changes_in_mail_subject: Mostrar los cambios de estado en el asunto de las notificaciones de correo
subject electrónico de las peticiones
label_inherited_from_parent_project: Inherited from parent project label_inherited_from_parent_project: Heredado del proyecto padre
label_inherited_from_group: Inherited from group %{name} label_inherited_from_group: Heredado del grupo %{name}
label_trackers_description: Trackers description label_trackers_description: Descripción del tipo de petición
label_open_trackers_description: View all trackers description label_open_trackers_description: Ver todas las descripciones de los tipos de petición
label_preferred_body_part_text: Text label_preferred_body_part_text: Texto
label_preferred_body_part_html: HTML (experimental) label_preferred_body_part_html: HTML (experimental)
field_parent_issue_subject: Parent task subject field_parent_issue_subject: Asunto de la tarea padre
permission_edit_own_issues: Edit own issues permission_edit_own_issues: Editar sus propias peticiones
text_select_apply_tracker: Select tracker text_select_apply_tracker: Seleccionar tipo de petición
label_updated_issues: Updated issues label_updated_issues: Peticiones actualizadas
text_avatar_server_config_html: The current avatar server is <a href="%{url}">%{url}</a>. text_avatar_server_config_html: El servidor actual de avatares es <a href="%{url}">%{url}</a>.
You can configure it in config/configuration.yml. Puede configurarlo en config/configuration.yml.
setting_gantt_months_limit: Maximum number of months displayed on the gantt chart setting_gantt_months_limit: Máximo número de meses mostrados en el diagrama de Gantt
permission_import_time_entries: Import time entries permission_import_time_entries: Importar registros de tiempo
label_import_notifications: Send email notifications during the import label_import_notifications: Enviar notificaciones de correo electrónico durante la importación
text_gs_available: ImageMagick PDF support available (optional) text_gs_available: Disponible soporte ImageMagick PDF (opcional)
field_recently_used_projects: Number of recently used projects in jump box field_recently_used_projects: Número de proyectos recientemente usados en el selector
label_optgroup_bookmarks: Marcadores label_optgroup_bookmarks: Marcadores
label_optgroup_others: Otros proyectos label_optgroup_others: Otros proyectos
label_optgroup_recents: Accesos recientes label_optgroup_recents: Utilizados recientemente
button_project_bookmark: Añadir marcador button_project_bookmark: Añadir marcador
button_project_bookmark_delete: Quitar marcador button_project_bookmark_delete: Quitar marcador
field_history_default_tab: Issue's history default tab field_history_default_tab: Pstaña por defecto del historial de la petición
label_issue_history_properties: Cambios de propiedad label_issue_history_properties: Cambios de propiedades
label_issue_history_notes: Notas label_issue_history_notes: Notas
label_last_tab_visited: Last visited tab label_last_tab_visited: Última pestaña visitada
field_unique_id: Unique ID field_unique_id: ID único
text_no_subject: no subject text_no_subject: sin asunto
setting_password_required_char_classes: Required character classes for passwords setting_password_required_char_classes: Clases de caracteres requeridos para las contraseñas
label_password_char_class_uppercase: uppercase letters label_password_char_class_uppercase: mayúsculas
label_password_char_class_lowercase: lowercase letters label_password_char_class_lowercase: minúsculas
label_password_char_class_digits: digits label_password_char_class_digits: dígitos
label_password_char_class_special_chars: special characters label_password_char_class_special_chars: caracteres especiales
text_characters_must_contain: Must contain %{character_classes}. text_characters_must_contain: Debe contener %{character_classes}.
label_starts_with: starts with label_starts_with: empieza con
label_ends_with: ends with label_ends_with: termina con
label_issue_fixed_version_updated: Target version updated label_issue_fixed_version_updated: Versión objetivo actualizada
setting_project_list_defaults: Projects list defaults setting_project_list_defaults: Por defecto para la lista de proyectos
label_display_type: Mostrar resultados como label_display_type: Mostrar resultados como
label_display_type_list: Lista label_display_type_list: Lista
label_display_type_board: Panel label_display_type_board: Tablón
label_my_bookmarks: Mis marcadores label_my_bookmarks: Mis marcadores
label_import_time_entries: Import time entries label_import_time_entries: Importar registros de tiempo
link_my_blog: Mi blog personal
label_legal: Aviso legal
label_legal_terms: Condiciones de uso
label_legal_privacy: Política de privacidad
label_legal_cookies: Uso de cookies

View file

@ -1261,60 +1261,60 @@ pt-BR:
label_ldaps_warning: É recomendado utilizar uma conexão LDAPS criptografada com verificação de certificado para evitar qualquer manipulação durante o processo de autenticação. label_ldaps_warning: É recomendado utilizar uma conexão LDAPS criptografada com verificação de certificado para evitar qualquer manipulação durante o processo de autenticação.
label_nothing_to_preview: Nada para visualizar label_nothing_to_preview: Nada para visualizar
error_token_expired: Este link de recuperação de senha expirou. Por favor, tente novamente. error_token_expired: Este link de recuperação de senha expirou. Por favor, tente novamente.
error_spent_on_future_date: Cannot log time on a future date error_spent_on_future_date: Não é possível registrar o tempo em uma data futura
setting_timelog_accept_future_dates: Accept time logs on future dates setting_timelog_accept_future_dates: Permitir registros de tempo em datas futuras
label_delete_link_to_subtask: Excluir relação label_delete_link_to_subtask: Excluir relação
error_not_allowed_to_log_time_for_other_users: You are not allowed to log time error_not_allowed_to_log_time_for_other_users: Você não possuí permissão para registrar o tempo
for other users para outros usuários
permission_log_time_for_other_users: Log spent time for other users permission_log_time_for_other_users: Tempo de registro gasto para outros usuários
label_tomorrow: tomorrow label_tomorrow: amanhã
label_next_week: next week label_next_week: próxima semana
label_next_month: next month label_next_month: próximo mês
text_role_no_workflow: No workflow defined for this role text_role_no_workflow: Não foi definido fluxo de trabalho para este papel
text_status_no_workflow: No tracker uses this status in the workflows text_status_no_workflow: Nenhum tipo de tarefa utiliza esta situação nos fluxos de trabalho
setting_mail_handler_preferred_body_part: Preferred part of multipart (HTML) emails setting_mail_handler_preferred_body_part: Parte preferida de e-mails multipartes (HTML)
setting_show_status_changes_in_mail_subject: Show status changes in issue mail notifications setting_show_status_changes_in_mail_subject: Exibir mudança de situação no título do email de notificação
subject data tarefa
label_inherited_from_parent_project: Inherited from parent project label_inherited_from_parent_project: Herdado do projeto pai
label_inherited_from_group: Inherited from group %{name} label_inherited_from_group: Herdado do grupo %{name}
label_trackers_description: Trackers description label_trackers_description: Descrição dos tipos de tarefa
label_open_trackers_description: View all trackers description label_open_trackers_description: Ver todas as descrições dos tipos de tarefas
label_preferred_body_part_text: Text label_preferred_body_part_text: Texto
label_preferred_body_part_html: HTML (experimental) label_preferred_body_part_html: HTML (experimental)
field_parent_issue_subject: Parent task subject field_parent_issue_subject: Assunto da tarefa pai
permission_edit_own_issues: Edit own issues permission_edit_own_issues: Editar as próprias tarefas
text_select_apply_tracker: Select tracker text_select_apply_tracker: Selecione o tipo de tarefa
label_updated_issues: Updated issues label_updated_issues: Tarefas atualizadas
text_avatar_server_config_html: The current avatar server is <a href="%{url}">%{url}</a>. text_avatar_server_config_html: O servidor de avatar atual é <a href="%{url}">% {url} </a>.
You can configure it in config/configuration.yml. Você pode configurá-lo em config / configuration.yml.
setting_gantt_months_limit: Maximum number of months displayed on the gantt chart setting_gantt_months_limit: Número máximo de meses exibidos no gráfico de Gantt
permission_import_time_entries: Import time entries permission_import_time_entries: Importar tempo gasto
label_import_notifications: Send email notifications during the import label_import_notifications: Enviar notificações por e-mail durante a importação
text_gs_available: ImageMagick PDF support available (optional) text_gs_available: Suporte ao ImageMagick PDF disponível (opcional)
field_recently_used_projects: Number of recently used projects in jump box field_recently_used_projects: Número de projetos usados recentemente no acesso rápido
label_optgroup_bookmarks: Bookmarks label_optgroup_bookmarks: Marcadores
label_optgroup_others: Other projects label_optgroup_others: Outros projetos
label_optgroup_recents: Recently used label_optgroup_recents: Recentes
button_project_bookmark: Add bookmark button_project_bookmark: Adicionar marcador
button_project_bookmark_delete: Remove bookmark button_project_bookmark_delete: Remover marcador
field_history_default_tab: Issue's history default tab field_history_default_tab: Aba padrão no histórico das tarefas
label_issue_history_properties: Property changes label_issue_history_properties: Campos alterados
label_issue_history_notes: Notes label_issue_history_notes: Notas
label_last_tab_visited: Last visited tab label_last_tab_visited: Última aba visitada
field_unique_id: Unique ID field_unique_id: ID único
text_no_subject: no subject text_no_subject: sem assunto
setting_password_required_char_classes: Required character classes for passwords setting_password_required_char_classes: Classes de caracteres obrigatórias para as senhas
label_password_char_class_uppercase: uppercase letters label_password_char_class_uppercase: Letras maiúsculas
label_password_char_class_lowercase: lowercase letters label_password_char_class_lowercase: Letras minúsculas
label_password_char_class_digits: digits label_password_char_class_digits: digitos
label_password_char_class_special_chars: special characters label_password_char_class_special_chars: caracteres especiais
text_characters_must_contain: Must contain %{character_classes}. text_characters_must_contain: Precisa conter %{character_classes}.
label_starts_with: starts with label_starts_with: começando com
label_ends_with: ends with label_ends_with: terminando com
label_issue_fixed_version_updated: Target version updated label_issue_fixed_version_updated: Versão atualizada
setting_project_list_defaults: Projects list defaults setting_project_list_defaults: Padrão da lista de projetos
label_display_type: Display results as label_display_type: Exibir resultados como
label_display_type_list: List label_display_type_list: Lista
label_display_type_board: Board label_display_type_board: Quadro
label_my_bookmarks: My bookmarks label_my_bookmarks: Meus marcaroes
label_import_time_entries: Import time entries label_import_time_entries: Importar tempo gasto

View file

@ -2,7 +2,7 @@ class PopulateMemberRoles < ActiveRecord::Migration[4.2]
def self.up def self.up
MemberRole.delete_all MemberRole.delete_all
Member.all.each do |member| Member.all.each do |member|
MemberRole.create!(:member_id => member.id, :role_id => member.role_id) MemberRole.insert!({:member_id => member.id, :role_id => member.role_id})
end end
end end

View file

@ -1,8 +1,279 @@
== Redmine changelog == Redmine changelog
Redmine - project management software Redmine - project management software
Copyright (C) 2006-2019 Jean-Philippe Lang Copyright (C) 2006-2021 Jean-Philippe Lang
http://www.redmine.org/ https://www.redmine.org/
== 2022-03-28 v4.1.7
=== [Attachments]
* Defect #36013: Paste image mixed with other DataTransferItem
=== [Database]
* Defect #36766: Database migration from Redmine 0.8.7 or earlier fails
=== [Documents]
* Defect #36686: Allow pasting screenshots from clipboard in documents
=== [Issues filter]
* Defect #30924: Filter on Target version's Status in subproject doesn't work on version from top project
=== [Projects]
* Defect #36593: User without permissions to view required project custom fields cannot create new projects
=== [Rails support]
* Patch #36757: Update Rails to 5.2.6.3
== 2022-02-20 v4.1.6
=== [Gantt]
* Defect #35027: Gantt PNG export ignores imagemagick_convert_command
=== [Gems support]
* Defect #35435: Psych 4: aliases in database.yml cause Psych::BadAlias exception
* Defect #36226: Psych 4: Psych::DisallowedClass exception when unserializing a setting value
=== [Issues]
* Defect #36455: Text custom field values are not aligned with their labels when text formatting is enabled
=== [Rails support]
* Patch #36633: Update Rails to 5.2.6.2
=== [UI]
* Defect #35090: Permission check of the setting button on the issues page mismatches button semantics
* Defect #36363: Cannot select text in a table with a context menu available
* Patch #36378: Update copyright year in the footer to 2022
=== [Wiki]
* Defect #36494: WikiContentVersion API returns 500 if author is nil
* Defect #36561: Wiki revision page does not return 404 if revision does not exist
== 2021-10-10 v4.1.5
=== [Administration]
* Defect #35731: Password and Confirmation fields are marked as required when editing a user
=== [Attachments]
* Defect #35715: File upload fails when run with uWSGI
=== [Issues]
* Defect #35642: Long text custom field values are not aligned with their labels
=== [Issues planning]
* Defect #35669: Prints of Issues Report details are messed-up due to the size of the graphs
=== [Permissions and roles]
* Defect #35634: Attachments deletable even though issue edit not permitted
=== [Security]
* Defect #35789: Redmine is leaking usernames on activities index view
* Patch #35463: Enforce stricter class filtering in WatchersController
=== [UI]
* Defect #34834: Line breaks in the description of a custom field are ignored in a tooltip
== 2021-08-01 v4.1.4
=== [Accounts / authentication]
* Defect #35226: Add SameSite=Lax to cookies to fix warnings in web browsers
=== [Attachments]
* Defect #33752: Uploading a big file fails with NoMemoryError
=== [Gantt]
* Defect #34694: Progress bar for a shared version on gantt disappears when the tree is collapsed and then expanded
=== [Gems support]
* Defect #35621: Bundler fails to install globalid when using Ruby < 2.6.0
=== [Issues]
* Defect #35134: Change total spent time link to global time entries when issue has subtasks that can be on non descendent projects
=== [Issues filter]
* Defect #35201: Duplicate entries in issue filter values
=== [Rails support]
* Patch #35214: Update Rails to 5.2.6
=== [Time tracking]
* Defect #34856: Time entry error on private issue
== 2021-04-26 v4.1.3
=== [Activity view]
* Defect #34933: Atom feed of the activity page does not contain items after the second page
=== [Email receiving]
* Defect #35100: MailHandler raises NameError exception when generating error message
=== [Gems support]
* Patch #34969: Remove dependency on MimeMagic
=== [Issues]
* Defect #34921: Do not journalize attachments that are added during a "Copy Issue" operation
=== [Performance]
* Patch #35034: Improve loading speed of workflow page
=== [Rails support]
* Patch #34966: Update Rails to 5.2.5
=== [Security]
* Defect #34367: Allowed filename extensions of attachments can be circumvented
* Defect #34950: SysController and MailHandlerController are vulnerable to timing attack
* Defect #35045: Mail handler bypasses add_issue_notes permission
* Defect #35085: Arbitrary file read in Git adapter
=== [Text formatting]
* Defect #34894: User link using @ not working at the end of line
=== [UI]
* Patch #34955: Update copyright year in the footer to 2021
== 2021-03-21 v4.1.2
=== [Accounts / authentication]
* Defect #33926: Rake tasks "db:encrypt" and "db:decrypt" may fail due to validation error
=== [Administration]
* Defect #33310: Warnings while running redmine:load_default_data rake task
* Defect #33339: Broken layout of the preview tab of "Welcome text" setting due to unexpectedly applied padding-left
* Defect #33355: TypeError when attempting to update a user with a blank email address
* Defect #34247: Web browser freezes when displaying workflow page with a large number of issue statuses
* Patch #32341: Show tooltip when hovering on repeat-value link in Field permission tab
=== [Attachments]
* Defect #33283: Thumbnail support for PDF attachments may not be detected
* Defect #33459: The order of thumbnails in journals does not match the order of file name list
* Defect #33639: Cannot paste image from clipboard when copying the image from web browsers or some apps
* Defect #33769: When creating more than two identical attachments in a single db transaction, the first one always ends up unreadable
* Patch #34479: Fix possible race condition with parallel, identical file uploads
=== [Custom fields]
* Defect #33275: Possible values field in list format custom field form is not marked as required
* Defect #33550: Per role visibility settings for spent time custom fields is not properly checked
=== [Documentation]
* Defect #33939: Unnecessary translation of {{toc}} macros in Russian Wiki formatting help
=== [Filters]
* Defect #33281: Totals of custom fields may not be sorted as configured
* Defect #34375: "is not" operator for Subproject filter incorrectly excludes closed subprojects
=== [Gantt]
* Defect #33140: Gantt bar is not displayed if the due date is the leftmost date or the start date is the rightmost date
* Defect #33175: Starting or ending marker is not displayed if they are on the leftmost or rightmost boundary of the gantt
* Defect #33220: Parent task subject column in gantt is not fully displayed when the column is widened
* Defect #33724: Selected gantt columns are not displayed with MS Edge Legacy
=== [Gems support]
* Defect #33206: Unable to autoload constant Version.table_name if gems uses Version class
* Defect #33768: Bundler may fail to install stringio if Ruby prior to 2.5 is used
* Patch #34461: Update Redcarpet to 3.5.1
* Patch #34619: Update Nokogiri to 1.11
=== [I18n]
* Defect #33452: Untranslated string "diff" in journal detail
=== [Issues]
* Defect #33338: Property changes tab does not show journals with both property changes and notes
* Defect #33576: Done ratio of a parent issue may be shown as 99% even though all subtasks are completed
=== [Issues list]
* Defect #33273: Total estimated time column shows up as decimal value regardless of time setting
* Defect #33548: Column header is clickable even when the column is not actually sortable
* Defect #34297: Subprojects issues are not displayed on main project when all subprojects are closed
=== [Projects]
* Defect #33889: Do not show list for custom fields without list entry on project overview
* Patch #34595: Filter list of recent projects in the project jump box
=== [REST API]
* Defect #33417: Updating an issue via REST API causes internal server error if invalid project id is specified
* Defect #34615: 'Search' falsy parameters are not respected
=== [Security]
* Defect #33846: Inline issue auto complete doesn't sanitize HTML tags
=== [SEO]
* Defect #6734: robots.txt: disallow crawling issues list with a query string
=== [Security]
* Defect #33360: Names of private projects are leaked by issue journal details that contain project_id changes
* Defect #33689: Issues API bypasses add_issue_notes permission
* Feature #33906: Upgrade Rails to 5.2.4.5
=== [Themes]
* Defect #8251: Classic Theme: Missed base line
=== [Time tracking]
* Defect #33341: Time entry user is shown twice in the User drop-down when editing spent time
=== [Translations]
* Defect #34447: Typo in translation string 'setting_issue_list_default_columns': s//Isuses/Issues
* Patch #34200: Portuguese (Brazil) translation for 4.1-stable
* Patch #34439: Spanish translation update for 4.1-stable
=== [UI]
* Defect #33563: File selection buttons are not fully displayed with Google Chrome in some language
* Feature #34123: System tests for inline auto complete feature
* Patch #33958: Jump to end of line in editor when starting list or quote
== 2020-04-06 v4.1.1 == 2020-04-06 v4.1.1

View file

@ -107,7 +107,7 @@ module Redmine
end end
next unless a next unless a
a.description = attachment['description'].to_s.strip a.description = attachment['description'].to_s.strip
if a.new_record? if a.new_record? || a.invalid?
unsaved_attachments << a unsaved_attachments << a
else else
saved_attachments << a saved_attachments << a

View file

@ -74,7 +74,7 @@ module Redmine
all.each do |object| all.each do |object|
clear = object.send(attribute) clear = object.send(attribute)
object.send "#{attribute}=", clear object.send "#{attribute}=", clear
raise(ActiveRecord::Rollback) unless object.save(:validation => false) raise(ActiveRecord::Rollback) unless object.save(validate: false)
end end
end ? true : false end ? true : false
end end
@ -84,7 +84,7 @@ module Redmine
all.each do |object| all.each do |object|
clear = object.send(attribute) clear = object.send(attribute)
object.send :write_attribute, attribute, clear object.send :write_attribute, attribute, clear
raise(ActiveRecord::Rollback) unless object.save(:validation => false) raise(ActiveRecord::Rollback) unless object.save(validate: false)
end end
end ? true : false end ? true : false
end end

View file

@ -341,6 +341,7 @@ module Redmine
if options[:format] == :html if options[:format] == :html
data_options = {} data_options = {}
data_options[:collapse_expand] = "issue-#{issue.id}" data_options[:collapse_expand] = "issue-#{issue.id}"
data_options[:number_of_rows] = number_of_rows
style = "position: absolute;top: #{options[:top]}px; font-size: 0.8em;" 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) 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) @columns[options[:column].name] << content if @columns.has_key?(options[:column].name)
@ -378,6 +379,9 @@ module Redmine
unless Redmine::Configuration['rmagick_font_path'].nil? unless Redmine::Configuration['rmagick_font_path'].nil?
font_path = Redmine::Configuration['minimagick_font_path'].presence || Redmine::Configuration['rmagick_font_path'].presence font_path = Redmine::Configuration['minimagick_font_path'].presence || Redmine::Configuration['rmagick_font_path'].presence
img = MiniMagick::Image.create(".#{format}", false) img = MiniMagick::Image.create(".#{format}", false)
if Redmine::Configuration['imagemagick_convert_command'].present?
MiniMagick.cli_path = File.dirname(Redmine::Configuration['imagemagick_convert_command'])
end
MiniMagick::Tool::Convert.new do |gc| MiniMagick::Tool::Convert.new do |gc|
gc.size('%dx%d' % [subject_width + g_width + 1, height]) gc.size('%dx%d' % [subject_width + g_width + 1, height])
gc.xc('white') gc.xc('white')
@ -623,14 +627,14 @@ module Redmine
def coordinates(start_date, end_date, progress, zoom=nil) def coordinates(start_date, end_date, progress, zoom=nil)
zoom ||= @zoom zoom ||= @zoom
coords = {} coords = {}
if start_date && end_date && start_date < self.date_to && end_date > self.date_from if start_date && end_date && start_date <= self.date_to && end_date >= self.date_from
if start_date > self.date_from if start_date >= self.date_from
coords[:start] = start_date - self.date_from coords[:start] = start_date - self.date_from
coords[:bar_start] = start_date - self.date_from coords[:bar_start] = start_date - self.date_from
else else
coords[:bar_start] = 0 coords[:bar_start] = 0
end end
if end_date < self.date_to if end_date <= self.date_to
coords[:end] = end_date - self.date_from + 1 coords[:end] = end_date - self.date_from + 1
coords[:bar_end] = end_date - self.date_from + 1 coords[:bar_end] = end_date - self.date_from + 1
else else
@ -768,6 +772,7 @@ module Redmine
:top_increment => params[:top_increment], :top_increment => params[:top_increment],
:obj_id => "#{object.class}-#{object.id}".downcase, :obj_id => "#{object.class}-#{object.id}".downcase,
}, },
:number_of_rows => number_of_rows,
} }
end end
if has_children if has_children
@ -823,7 +828,10 @@ module Redmine
def html_task(params, coords, markers, label, object) def html_task(params, coords, markers, label, object)
output = +'' output = +''
data_options = {} data_options = {}
data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase if object if object
data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase
data_options[:number_of_rows] = number_of_rows
end
css = "task " + css = "task " +
case object case object
when Project when Project

View file

@ -35,6 +35,7 @@ module Redmine
projects = projects.like(query) projects = projects.like(query)
end end
projects. projects.
visible.
index_by(&:id). index_by(&:id).
values_at(*project_ids). # sort according to stored order values_at(*project_ids). # sort according to stored order
compact compact

View file

@ -178,6 +178,14 @@ module Redmine
(path[-1,1] == "/") ? path[0..-2] : path (path[-1,1] == "/") ? path[0..-2] : path
end end
def valid_name?(name)
return true if name.nil?
return true if name.is_a?(Integer) && name > 0
return true if name.is_a?(String) && name =~ /\A[0-9]*\z/
false
end
private private
def retrieve_root_url def retrieve_root_url

View file

@ -388,6 +388,18 @@ module Redmine
nil nil
end end
def valid_name?(name)
return false unless name.is_a?(String)
return false if name.start_with?('-', '/', 'refs/heads/', 'refs/remotes/')
return false if name == 'HEAD'
git_cmd ['show-ref', '--heads', '--tags', '--quiet', '--', name]
true
rescue ScmCommandAborted
false
end
class Revision < Redmine::Scm::Adapters::Revision class Revision < Redmine::Scm::Adapters::Revision
# Returns the readable identifier # Returns the readable identifier
def format_identifier def format_identifier

View file

@ -291,6 +291,15 @@ module Redmine
Annotate.new Annotate.new
end end
def valid_name?(name)
return false unless name.nil? || name.is_a?(String)
# Mercurials names don't need to be checked further as its CLI
# interface is restrictive enough to reject any invalid names on its
# own.
true
end
class Revision < Redmine::Scm::Adapters::Revision class Revision < Redmine::Scm::Adapters::Revision
# Returns the readable identifier # Returns the readable identifier
def format_identifier def format_identifier

View file

@ -18,7 +18,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'fileutils' require 'fileutils'
require 'mimemagic'
module Redmine module Redmine
module Thumbnail module Thumbnail
@ -33,15 +32,11 @@ module Redmine
return nil unless convert_available? return nil unless convert_available?
return nil if is_pdf && !gs_available? return nil if is_pdf && !gs_available?
unless File.exists?(target) unless File.exists?(target)
mime_type = File.open(source) {|f| MimeMagic.by_magic(f).try(:type) } # Make sure we only invoke Imagemagick if the file type is allowed
return nil if mime_type.nil? mime_type = File.open(source) {|f| Marcel::MimeType.for(f)}
return nil if !ALLOWED_TYPES.include? mime_type return nil if !ALLOWED_TYPES.include? mime_type
return nil if is_pdf && mime_type != "application/pdf" 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
end
directory = File.dirname(target) directory = File.dirname(target)
unless File.exists?(directory) unless File.exists?(directory)
FileUtils.mkdir_p directory FileUtils.mkdir_p directory

View file

@ -7,7 +7,7 @@ module Redmine
module VERSION module VERSION
MAJOR = 4 MAJOR = 4
MINOR = 1 MINOR = 1
TINY = 1 TINY = 7
# Branch values: # Branch values:
# * official release: nil # * official release: nil

View file

@ -2,7 +2,6 @@ desc 'Load Redmine default configuration data. Language is chosen interactively
namespace :redmine do namespace :redmine do
task :load_default_data => :environment do task :load_default_data => :environment do
require 'custom_field'
include Redmine::I18n include Redmine::I18n
set_language_if_valid('en') set_language_if_valid('en')

View file

@ -295,8 +295,8 @@ bq. Rails - это полноценный, многоуровневый фрей
<h3><a name="11" class="wiki-page"></a>Содержание</h3> <h3><a name="11" class="wiki-page"></a>Содержание</h3>
<pre> <pre>
{{Содержание}} =&gt; содержание, выровненное по левому краю {{toc}} =&gt; содержание, выровненное по левому краю
{{&gt;Содержание}} =&gt; содержание, выровненное по правому краю {{&gt;toc}} =&gt; содержание, выровненное по правому краю
</pre> </pre>
<h3><a name="14" class="wiki-page"></a>Horizontal Rule</h3> <h3><a name="14" class="wiki-page"></a>Horizontal Rule</h3>

View file

@ -8,6 +8,12 @@ $.ajaxPrefilter(function (s) {
} }
}); });
function sanitizeHTML(string) {
var temp = document.createElement('span');
temp.textContent = string;
return temp.innerHTML;
}
function checkAll(id, checked) { function checkAll(id, checked) {
$('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked); $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
} }
@ -371,15 +377,29 @@ function showIssueHistory(journal, url) {
switch(journal) { switch(journal) {
case 'notes': case 'notes':
tab_content.find('.journal').show();
tab_content.find('.journal:not(.has-notes)').hide(); tab_content.find('.journal:not(.has-notes)').hide();
tab_content.find('.journal.has-notes').show(); tab_content.find('.journal .wiki').show();
tab_content.find('.journal .contextual .journal-actions').show();
// always show thumbnails in notes tab
var thumbnails = tab_content.find('.journal .thumbnails');
thumbnails.show();
// show journals without notes, but with thumbnails
thumbnails.parents('.journal').show();
break; break;
case 'properties': case 'properties':
tab_content.find('.journal.has-notes').hide(); tab_content.find('.journal').show();
tab_content.find('.journal:not(.has-notes)').show(); tab_content.find('.journal:not(.has-details)').hide();
tab_content.find('.journal .wiki').hide();
tab_content.find('.journal .thumbnails').hide();
tab_content.find('.journal .contextual .journal-actions').hide();
break; break;
default: default:
tab_content.find('.journal').show(); tab_content.find('.journal').show();
tab_content.find('.journal .wiki').show();
tab_content.find('.journal .thumbnails').show();
tab_content.find('.journal .contextual .journal-actions').show();
} }
return false; return false;
@ -933,7 +953,7 @@ $(document).ready(function(){
$('#history .tabs').on('click', 'a', function(e){ $('#history .tabs').on('click', 'a', function(e){
var tab = $(e.target).attr('id').replace('tab-',''); var tab = $(e.target).attr('id').replace('tab-','');
document.cookie = 'history_last_tab=' + tab document.cookie = 'history_last_tab=' + tab + '; SameSite=Lax'
}); });
}); });
@ -997,7 +1017,7 @@ function setupAttachmentDetail() {
$(function () { $(function () {
$('[title]').tooltip({ $("[title]:not(.no-tooltip)").tooltip({
show: { show: {
delay: 400 delay: 400
}, },
@ -1048,6 +1068,9 @@ function inlineAutoComplete(element) {
requireLeadingSpace: true, requireLeadingSpace: true,
selectTemplate: function (issue) { selectTemplate: function (issue) {
return '#' + issue.original.id; return '#' + issue.original.id;
},
menuItemTemplate: function (issue) {
return sanitizeHTML(issue.original.label);
} }
}); });

View file

@ -257,13 +257,12 @@ function copyImageFromClipboard(e) {
if (!$(e.target).hasClass('wiki-edit')) { return; } if (!$(e.target).hasClass('wiki-edit')) { return; }
var clipboardData = e.clipboardData || e.originalEvent.clipboardData var clipboardData = e.clipboardData || e.originalEvent.clipboardData
if (!clipboardData) { return; } if (!clipboardData) { return; }
if (clipboardData.types.some(function(t){ return /^text/.test(t); })) { return; } if (clipboardData.types.some(function(t){ return /^text\/plain$/.test(t); })) { return; }
var items = clipboardData.items var files = clipboardData.files
for (var i = 0 ; i < items.length ; i++) { for (var i = 0 ; i < files.length ; i++) {
var item = items[i]; var file = files[i];
if (item.type.indexOf("image") != -1) { if (file.type.indexOf("image") != -1) {
var blob = item.getAsFile();
var date = new Date(); var date = new Date();
var filename = 'clipboard-' var filename = 'clipboard-'
+ date.getFullYear() + date.getFullYear()
@ -272,9 +271,8 @@ function copyImageFromClipboard(e) {
+ ('0'+date.getHours()).slice(-2) + ('0'+date.getHours()).slice(-2)
+ ('0'+date.getMinutes()).slice(-2) + ('0'+date.getMinutes()).slice(-2)
+ '-' + randomKey(5).toLocaleLowerCase() + '-' + randomKey(5).toLocaleLowerCase()
+ '.' + blob.name.split('.').pop(); + '.' + file.name.split('.').pop();
var file = new Blob([blob], {type: blob.type});
file.name = filename;
var inputEl = $('input:file.filedrop').first() var inputEl = $('input:file.filedrop').first()
handleFileDropEvent.target = e.target; handleFileDropEvent.target = e.target;
addFile(inputEl, file, true); addFile(inputEl, file, true);

View file

@ -46,6 +46,7 @@ function contextMenuClick(event) {
} else { } else {
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
contextMenuToggleSelection(tr); contextMenuToggleSelection(tr);
contextMenuClearDocumentSelection();
} else if (event.shiftKey) { } else if (event.shiftKey) {
lastSelected = contextMenuLastSelected(); lastSelected = contextMenuLastSelected();
if (lastSelected.length) { if (lastSelected.length) {
@ -53,6 +54,7 @@ function contextMenuClick(event) {
$('.hascontextmenu').each(function(){ $('.hascontextmenu').each(function(){
if (toggling || $(this).is(tr)) { if (toggling || $(this).is(tr)) {
contextMenuAddSelection($(this)); contextMenuAddSelection($(this));
contextMenuClearDocumentSelection();
} }
if ($(this).is(tr) || $(this).is(lastSelected)) { if ($(this).is(tr) || $(this).is(lastSelected)) {
toggling = !toggling; toggling = !toggling;
@ -191,7 +193,6 @@ function contextMenuToggleSelection(tr) {
function contextMenuAddSelection(tr) { function contextMenuAddSelection(tr) {
tr.addClass('context-menu-selection'); tr.addClass('context-menu-selection');
contextMenuCheckSelectionBox(tr, true); contextMenuCheckSelectionBox(tr, true);
contextMenuClearDocumentSelection();
} }
function contextMenuRemoveSelection(tr) { function contextMenuRemoveSelection(tr) {

View file

@ -253,13 +253,16 @@ ganttEntryClick = function(e){
subject.nextAll('div').each(function(_, element){ subject.nextAll('div').each(function(_, element){
var el = $(element); var el = $(element);
var json = el.data('collapse-expand'); var json = el.data('collapse-expand');
var number_of_rows = el.data('number-of-rows');
var el_task_bars = '#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]';
var el_selected_columns = 'td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]';
if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){ if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){
out_of_hierarchy = true; out_of_hierarchy = true;
if(target_shown == null) return false; if(target_shown == null) return false;
var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1); var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1);
el.css('top', new_top_val); el.css('top', new_top_val);
$('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"], td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, el){ $([el_task_bars, el_selected_columns].join()).each(function(_, el){
$(el).css('top', new_top_val); $(el).css('top', new_top_val);
}); });
return true; return true;
@ -272,15 +275,14 @@ ganttEntryClick = function(e){
total_height = 0; total_height = 0;
} }
if(is_shown == target_shown){ if(is_shown == target_shown){
$('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, task) { $(el_task_bars).each(function(_, task) {
var el_task = $(task); var el_task = $(task);
if(!is_shown) if(!is_shown)
el_task.css('top', target_top + total_height); el_task.css('top', target_top + total_height);
if(!el_task.hasClass('tooltip')) if(!el_task.hasClass('tooltip'))
el_task.toggle(!is_shown); el_task.toggle(!is_shown);
}); });
$('td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]' $(el_selected_columns).each(function (_, attr) {
).each(function (_, attr) {
var el_attr = $(attr); var el_attr = $(attr);
if (!is_shown) if (!is_shown)
el_attr.css('top', target_top + total_height); el_attr.css('top', target_top + total_height);

File diff suppressed because one or more lines are too long

View file

@ -338,7 +338,7 @@ jsToolBar.prototype = {
} else if (typeof(this.textarea["setSelectionRange"]) != "undefined") { } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
this.textarea.value = this.textarea.value.substring(0, start) + subst + this.textarea.value = this.textarea.value.substring(0, start) + subst +
this.textarea.value.substring(end); this.textarea.value.substring(end);
if (sel) { if (sel || (!prefix && start === end)) {
this.textarea.setSelectionRange(start + subst.length, start + subst.length); this.textarea.setSelectionRange(start + subst.length, start + subst.length);
} else { } else {
this.textarea.setSelectionRange(start + prefix.length, start + prefix.length); this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);

View file

@ -474,7 +474,7 @@ select {
background-position: calc(100% - 7px) 50%; background-position: calc(100% - 7px) 50%;
padding-right: 20px; padding-right: 20px;
} }
input[type="file"] {border: 0; padding-left: 0; padding-right: 0; background-color: initial; } input[type="file"] {border: 0; padding-left: 0; padding-right: 0; height: initial; background-color: initial; }
input[type="submit"], button[type="submit"] { input[type="submit"], button[type="submit"] {
-webkit-appearance: button; -webkit-appearance: button;
cursor: pointer; cursor: pointer;
@ -528,6 +528,8 @@ div.issue .attributes {margin-top: 2em;}
div.issue .attributes .attribute {padding-left:180px; clear:left; min-height: 1.8em;} div.issue .attributes .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
div.issue .attributes .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left; overflow:hidden; text-overflow: ellipsis;} div.issue .attributes .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left; overflow:hidden; text-overflow: ellipsis;}
div.issue .attribute .value {overflow:auto; text-overflow: ellipsis;} div.issue .attribute .value {overflow:auto; text-overflow: ellipsis;}
div.issue .attribute.string_cf .value .wiki p {margin-top: 0; margin-bottom: 0;}
div.issue .attribute.text_cf .value .wiki p:first-of-type {margin-top: 0;}
div.issue.overdue .due-date .value { color: #c22; } div.issue.overdue .due-date .value { color: #c22; }
#issue_tree table.issues, #relations table.issues { border: 0; } #issue_tree table.issues, #relations table.issues { border: 0; }
@ -833,6 +835,7 @@ input#months { width: 46px; }
.tabular .wiki-preview, .tabular .jstTabs {width: 95%;} .tabular .wiki-preview, .tabular .jstTabs {width: 95%;}
.tabular.settings .wiki-preview, .tabular.settings .jstTabs { width: 99%; } .tabular.settings .wiki-preview, .tabular.settings .jstTabs { width: 99%; }
.tabular.settings .wiki-preview p {padding-left: 0 !important}
.tabular .wiki-preview p { .tabular .wiki-preview p {
min-height: initial; min-height: initial;
padding: 0; padding: 0;
@ -1438,7 +1441,8 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
font-size: 0.9em; font-size: 0.9em;
border-radius: 3px; border-radius: 3px;
border: 0; border: 0;
box-shadow: none box-shadow: none;
white-space: pre-wrap;
} }
/***** Icons *****/ /***** Icons *****/

View file

@ -29,7 +29,6 @@ body{ color:#303030; background:#e8eaec; }
#main a { font-weight: bold; color: #467aa7;} #main a { font-weight: bold; color: #467aa7;}
#main a:hover { color: #2a5a8a; text-decoration: underline; } #main a:hover { color: #2a5a8a; text-decoration: underline; }
#content { background: #fff; } #content { background: #fff; }
#content .tabs ul { bottom:-1px; }
h2, h3, h4, .wiki h1, .wiki h2, .wiki h3 { border-bottom: 0px; color:#606060; font-family: Trebuchet MS,Georgia,"Times New Roman",serif; } h2, h3, h4, .wiki h1, .wiki h2, .wiki h3 { border-bottom: 0px; color:#606060; font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
h2, .wiki h1 { letter-spacing:-1px; } h2, .wiki h1 { letter-spacing:-1px; }

View file

@ -28,7 +28,8 @@ class ActivitiesControllerTest < Redmine::ControllerTest
:members, :members,
:groups_users, :groups_users,
:enabled_modules, :enabled_modules,
:journals, :journal_details :journals, :journal_details,
:attachments, :changesets, :documents, :messages, :news, :time_entries, :wiki_content_versions
def test_project_index def test_project_index
get :index, :params => { get :index, :params => {
@ -95,6 +96,18 @@ class ActivitiesControllerTest < Redmine::ControllerTest
assert_response 404 assert_response 404
end end
def test_user_index_with_non_visible_user_id_should_respond_404
Role.anonymous.update! :users_visibility => 'members_of_visible_projects'
user = User.generate!
@request.session[:user_id] = nil
get :index, :params => {
:user_id => user.id
}
assert_response 404
end
def test_index_atom_feed def test_index_atom_feed
get :index, :params => { get :index, :params => {
:format => 'atom', :format => 'atom',
@ -111,6 +124,22 @@ class ActivitiesControllerTest < Redmine::ControllerTest
end end
end end
def test_index_atom_feed_should_respect_feeds_limit_setting
with_settings :feeds_limit => '20' do
get(
:index,
:params => {
:format => 'atom'
}
)
end
assert_response :success
assert_select 'feed' do
assert_select 'entry', :count => 20
end
end
def test_index_atom_feed_with_explicit_selection def test_index_atom_feed_with_explicit_selection
get :index, :params => { get :index, :params => {
:format => 'atom', :format => 'atom',

View file

@ -531,6 +531,23 @@ class AttachmentsControllerTest < Redmine::ControllerTest
assert_response 403 assert_response 403
end end
def test_edit_all_issue_attachment_by_user_without_edit_issue_permission_on_tracker_should_return_404
role = Role.find(2)
role.set_permission_trackers 'edit_issues', [2, 3]
role.save!
@request.session[:user_id] = 2
get(
:edit_all,
:params => {
:object_type => 'issues',
:object_id => '4'
}
)
assert_response 404
end
def test_update_all def test_update_all
@request.session[:user_id] = 2 @request.session[:user_id] = 2
patch :update_all, :params => { patch :update_all, :params => {
@ -659,4 +676,25 @@ class AttachmentsControllerTest < Redmine::ControllerTest
assert_response 302 assert_response 302
assert Attachment.find_by_id(3) assert Attachment.find_by_id(3)
end end
def test_destroy_issue_attachment_by_user_without_edit_issue_permission_on_tracker
role = Role.find(2)
role.set_permission_trackers 'edit_issues', [2, 3]
role.save!
@request.session[:user_id] = 2
set_tmp_attachments_directory
assert_no_difference 'Attachment.count' do
delete(
:destroy,
:params => {
:id => 7
}
)
end
assert_response 403
assert Attachment.find_by_id(7)
end
end end

View file

@ -124,6 +124,49 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'a[href="/issues/6"]', 0 assert_select 'a[href="/issues/6"]', 0
end end
def test_index_should_list_issues_of_closed_subprojects
@request.session[:user_id] = 1
project = Project.find(1)
with_settings :display_subprojects_issues => '1' do
# One of subprojects is closed
Project.find_by(:identifier => 'subproject1').close
get(:index, :params => {:project_id => project.id})
assert_response :success
assert_equal 10, issues_in_list.count
# All subprojects are closed
project.descendants.each(&:close)
get(:index, :params => {:project_id => project.id})
assert_response :success
assert_equal 10, issues_in_list.count
end
end
def test_index_with_subproject_filter_should_not_exclude_closed_subprojects_issues
subproject1 = Project.find(3)
subproject2 = Project.find(4)
subproject1.close
with_settings :display_subprojects_issues => '1' do
get(
:index,
:params => {
:project_id => 1,
:set_filter => 1,
:f => ['subproject_id'],
:op => {'subproject_id' => '!'},
:v => {'subproject_id' => [subproject2.id.to_s]},
:c => ['project']
}
)
end
assert_response :success
column_values = columns_values_in_list('project')
assert_includes column_values, subproject1.name
assert_equal 9, column_values.size
end
def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission
@request.session[:user_id] = 2 @request.session[:user_id] = 2
Setting.display_subprojects_issues = 1 Setting.display_subprojects_issues = 1
@ -1664,6 +1707,22 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select '#content a.new-issue[href="/issues/new"]', :text => 'New issue' assert_select '#content a.new-issue[href="/issues/new"]', :text => 'New issue'
end end
def test_index_should_show_setting_link_with_edit_project_permission
role = Role.find(1)
role.add_permission! :edit_project
@request.session[:user_id] = 2
get(:index, :params => {:project_id => 1})
assert_select '#content a.icon-settings[href="/projects/ecookbook/settings/issues"]', 1
end
def test_index_should_not_show_setting_link_without_edit_project_permission
role = Role.find(1)
role.remove_permission! :edit_project
@request.session[:user_id] = 2
get(:index, :params => {:project_id => 1})
assert_select '#content a.icon-settings[href="/projects/ecookbook/settings/issues"]', 0
end
def test_index_should_not_include_new_issue_tab_when_disabled def test_index_should_not_include_new_issue_tab_when_disabled
with_settings :new_item_menu_tab => '0' do with_settings :new_item_menu_tab => '0' do
@request.session[:user_id] = 2 @request.session[:user_id] = 2
@ -1720,6 +1779,22 @@ class IssuesControllerTest < Redmine::ControllerTest
end end
end end
def test_index_should_respect_timespan_format
with_settings :timespan_format => 'minutes' do
get(
:index,
:params => {
:set_filter => 1,
:c => %w(estimated_hours total_estimated_hours spent_hours total_spent_hours)
}
)
assert_select 'table.issues tr#issue-1 td.estimated_hours', :text => '200:00'
assert_select 'table.issues tr#issue-1 td.total_estimated_hours', :text => '200:00'
assert_select 'table.issues tr#issue-1 td.spent_hours', :text => '154:15'
assert_select 'table.issues tr#issue-1 td.total_spent_hours', :text => '154:15'
end
end
def test_show_by_anonymous def test_show_by_anonymous
get :show, :params => { get :show, :params => {
:id => 1 :id => 1
@ -2615,6 +2690,32 @@ class IssuesControllerTest < Redmine::ControllerTest
end end
end end
def test_show_should_not_display_edit_attachment_icon_for_user_without_edit_issue_permission_on_tracker
role = Role.find(2)
role.set_permission_trackers 'edit_issues', [2, 3]
role.save!
@request.session[:user_id] = 2
get :show, params: {id: 4}
assert_response :success
assert_select 'div.attachments .icon-edit', 0
end
def test_show_should_not_display_delete_attachment_icon_for_user_without_edit_issue_permission_on_tracker
role = Role.find(2)
role.set_permission_trackers 'edit_issues', [2, 3]
role.save!
@request.session[:user_id] = 2
get :show, params: {id: 4}
assert_response :success
assert_select 'div.attachments .icon-del', 0
end
def test_get_new def test_get_new
@request.session[:user_id] = 2 @request.session[:user_id] = 2
get :new, :params => { get :new, :params => {
@ -4816,6 +4917,41 @@ class IssuesControllerTest < Redmine::ControllerTest
end end
end end
def test_get_edit_should_display_visible_spent_time_custom_field
@request.session[:user_id] = 2
get(
:edit,
:params => {
:id => 13,
}
)
assert_response :success
assert_select '#issue-form select#time_entry_custom_field_values_10', 1
end
def test_get_edit_should_not_display_spent_time_custom_field_not_visible
cf = TimeEntryCustomField.find(10)
cf.visible = false
cf.role_ids = [1]
cf.save!
@request.session[:user_id] = 2
get(
:edit,
:params => {
:id => 13,
}
)
assert_response :success
assert_select '#issue-form select#time_entry_custom_field_values_10', 0
end
def test_update_form_for_existing_issue def test_update_form_for_existing_issue
@request.session[:user_id] = 2 @request.session[:user_id] = 2
patch :edit, :params => { patch :edit, :params => {
@ -5222,6 +5358,24 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_equal spent_hours_before + 2.5, issue.spent_hours assert_equal spent_hours_before + 2.5, issue.spent_hours
end end
def test_put_update_should_check_add_issue_notes_permission
role = Role.find(1)
role.remove_permission! :add_issue_notes
@request.session[:user_id] = 2
assert_no_difference 'Journal.count' do
put(
:update,
:params => {
:id => 1,
:issue => {
:notes => 'New note'
}
}
)
end
end
def test_put_update_should_preserve_parent_issue_even_if_not_visible def test_put_update_should_preserve_parent_issue_even_if_not_visible
parent = Issue.generate!(:project_id => 1, :is_private => true) parent = Issue.generate!(:project_id => 1, :is_private => true)
issue = Issue.generate!(:parent_issue_id => parent.id) issue = Issue.generate!(:parent_issue_id => parent.id)
@ -5528,6 +5682,29 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'this is my comment' assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'this is my comment'
end end
def test_put_with_spent_time_when_assigned_to_of_private_issue_is_update_at_the_same_time
@request.session[:user_id] = 3
Role.find(2).update! :issues_visibility => 'own'
private_issue = Issue.find(3)
assert_difference('TimeEntry.count', 1) do
put(
:update,
params: {
id: private_issue.id,
issue: { assigned_to_id: nil },
time_entry: {
comments: "add spent time", activity_id: TimeEntryActivity.first.id, hours: 1
}
}
)
end
assert_select '#errorExplanation', {text: /Log time is invalid/, count: 0}
assert_select '#errorExplanation', {text: /Issue is invalid/, count: 0}
assert_redirected_to action: 'show', id: private_issue.id
assert_not private_issue.reload.visible?
end
def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
issue = Issue.find(2) issue = Issue.find(2)
@request.session[:user_id] = 2 @request.session[:user_id] = 2

View file

@ -818,4 +818,28 @@ class QueriesControllerTest < Redmine::ControllerTest
assert_include ["Dave Lopper", "3", "active"], json assert_include ["Dave Lopper", "3", "active"], json
assert_include ["Dave2 Lopper2", "5", "locked"], json assert_include ["Dave2 Lopper2", "5", "locked"], json
end end
def test_activity_filter_should_return_active_and_system_activity_ids
TimeEntryActivity.create!(:name => 'Design', :parent_id => 9, :project_id => 1)
TimeEntryActivity.create!(:name => 'QA', :active => false, :parent_id => 11, :project_id => 1)
TimeEntryActivity.create!(:name => 'Inactive Activity', :active => true, :parent_id => 14, :project_id => 1)
@request.session[:user_id] = 2
get(
:filter,
:params => {
:project_id => 1,
:type => 'TimeEntryQuery',
:name => 'activity_id'
}
)
assert_response :success
assert_equal 'application/json', response.media_type
json = ActiveSupport::JSON.decode(response.body)
assert_equal 3, json.count
assert_include ["Design", "9"], json
assert_include ["Development", "10"], json
assert_include ["Inactive Activity", "14"], json
end
end end

View file

@ -428,4 +428,19 @@ class SearchControllerTest < Redmine::ControllerTest
assert_select 'dd span.highlight', :text => 'highlighted' assert_select 'dd span.highlight', :text => 'highlighted'
end end
end end
def test_search_should_exclude_empty_modules_params
@request.session[:user_id] = 1
get :index, params: {
q: "private",
scope: "all",
issues: "1",
projects: nil
}
assert_response :success
assert_select '#search-results dt.project', 0
end
end end

View file

@ -226,7 +226,7 @@ class TimelogControllerTest < Redmine::ControllerTest
assert_response :success assert_response :success
assert_select 'select[name=?]', 'time_entry[user_id]' do assert_select 'select[name=?]', 'time_entry[user_id]' do
assert_select 'option[value="2"][selected=selected]' assert_select 'option[value="2"][selected=selected]', 1
end end
end end

View file

@ -237,6 +237,7 @@ class UsersControllerTest < Redmine::ControllerTest
get :new get :new
assert_response :success assert_response :success
assert_select 'input[name=?]', 'user[login]' assert_select 'input[name=?]', 'user[login]'
assert_select 'label[for=?]>span.required', 'user_password', 1
end end
def test_create def test_create
@ -427,6 +428,7 @@ class UsersControllerTest < Redmine::ControllerTest
assert_response :success assert_response :success
assert_select 'h2>a+img.gravatar' assert_select 'h2>a+img.gravatar'
assert_select 'input[name=?][value=?]', 'user[login]', 'jsmith' assert_select 'input[name=?][value=?]', 'user[login]', 'jsmith'
assert_select 'label[for=?]>span.required', 'user_password', 0
end end
def test_edit_registered_user def test_edit_registered_user
@ -708,6 +710,19 @@ class UsersControllerTest < Redmine::ControllerTest
assert_response 404 assert_response 404
end end
def test_update_with_blank_email_should_not_raise_exception
assert_no_difference 'User.count' do
with_settings :gravatar_enabled => '1' do
put :update, :params => {
:id => 2,
:user => {:mail => ''}
}
end
end
assert_response :success
assert_select_error /Email cannot be blank/
end
def test_destroy def test_destroy
assert_difference 'User.count', -1 do assert_difference 'User.count', -1 do
delete :destroy, :params => {:id => 2} delete :destroy, :params => {:id => 2}

View file

@ -160,6 +160,12 @@ class WikiControllerTest < Redmine::ControllerTest
assert_select 'select[name=?] option[value="2"][selected=selected]', 'wiki_page[parent_id]' assert_select 'select[name=?] option[value="2"][selected=selected]', 'wiki_page[parent_id]'
end end
def test_show_unexistent_version_page
@request.session[:user_id] = 2
get :show, :params => {:project_id => 1, :id => 'CookBook_documentation', :version => 100}
assert_response 404
end
def test_show_should_not_show_history_without_permission def test_show_should_not_show_history_without_permission
Role.anonymous.remove_permission! :view_wiki_edits Role.anonymous.remove_permission! :view_wiki_edits
get :show, :params => {:project_id => 1, :id => 'Page with sections', :version => 2} get :show, :params => {:project_id => 1, :id => 'Page with sections', :version => 2}

View file

@ -125,7 +125,7 @@ class WorkflowsControllerTest < Redmine::ControllerTest
assert_select 'table.workflows.transitions-always tbody tr:nth-child(2)' do assert_select 'table.workflows.transitions-always tbody tr:nth-child(2)' do
assert_select 'td.name', :text => 'New' assert_select 'td.name', :text => 'New'
# assert that the td is enabled # assert that the td is enabled
assert_select "td[title='New » New'][class=?]", 'enabled' assert_select "td.enabled[title='New » New']"
# assert that the checkbox is disabled and checked # assert that the checkbox is disabled and checked
assert_select "input[name='transitions[1][1][always]'][checked=?][disabled=?]", 'checked', 'disabled', 1 assert_select "input[name='transitions[1][1][always]'][checked=?][disabled=?]", 'checked', 'disabled', 1
end end

View file

@ -456,6 +456,7 @@ class ApplicationHelperTest < Redmine::HelperTest
'user:JSMITH' => link_to_user(User.find_by_id(2)), 'user:JSMITH' => link_to_user(User.find_by_id(2)),
'user#2' => link_to_user(User.find_by_id(2)), 'user#2' => link_to_user(User.find_by_id(2)),
'@jsmith' => link_to_user(User.find_by_id(2)), '@jsmith' => link_to_user(User.find_by_id(2)),
'@jsmith.' => "#{link_to_user(User.find_by_id(2))}.",
'@JSMITH' => link_to_user(User.find_by_id(2)), '@JSMITH' => link_to_user(User.find_by_id(2)),
'@abcd@example.com' => link_to_user(User.find_by_id(u_email_id)), '@abcd@example.com' => link_to_user(User.find_by_id(u_email_id)),
'user:abcd@example.com' => link_to_user(User.find_by_id(u_email_id)), 'user:abcd@example.com' => link_to_user(User.find_by_id(u_email_id)),

View file

@ -143,12 +143,20 @@ class IssuesHelperTest < Redmine::HelperTest
end end
test 'show_detail should show old and new values with a project attribute' do test 'show_detail should show old and new values with a project attribute' do
User.current = User.find(2)
detail = JournalDetail.new(:property => 'attr', :prop_key => 'project_id', detail = JournalDetail.new(:property => 'attr', :prop_key => 'project_id',
:old_value => 1, :value => 2) :old_value => 1, :value => 2)
assert_match 'eCookbook', show_detail(detail, true) assert_match 'eCookbook', show_detail(detail, true)
assert_match 'OnlineStore', show_detail(detail, true) assert_match 'OnlineStore', show_detail(detail, true)
end end
test 'show_detail with a project attribute should show project ID if project is not visible' do
detail = JournalDetail.new(:property => 'attr', :prop_key => 'project_id',
:old_value => 1, :value => 2)
assert_match 'eCookbook', show_detail(detail, true)
assert_match '2', show_detail(detail, true)
end
test 'show_detail should show old and new values with a issue status attribute' do test 'show_detail should show old and new values with a issue status attribute' do
detail = JournalDetail.new(:property => 'attr', :prop_key => 'status_id', detail = JournalDetail.new(:property => 'attr', :prop_key => 'status_id',
:old_value => 1, :value => 2) :old_value => 1, :value => 2)
@ -352,4 +360,26 @@ class IssuesHelperTest < Redmine::HelperTest
assert_equal '06/06/2019', issue_due_date_details(issue) assert_equal '06/06/2019', issue_due_date_details(issue)
end end
end end
def test_issue_spent_hours_details_should_link_to_project_time_entries_depending_on_cross_project_setting
%w(descendants).each do |setting|
with_settings :cross_project_subtasks => setting do
TimeEntry.generate!(:issue => Issue.generate!(:parent_issue_id => 1), :hours => 3)
TimeEntry.generate!(:issue => Issue.generate!(:parent_issue_id => 1), :hours => 4)
assert_match "href=\"/projects/ecookbook/time_entries?issue_id=~1\"", CGI.unescape(issue_spent_hours_details(Issue.find(1)))
end
end
end
def test_issue_spent_hours_details_should_link_to_global_time_entries_depending_on_cross_project_setting
%w(system tree hierarchy).each do |setting|
with_settings :cross_project_subtasks => setting do
TimeEntry.generate!(:issue => Issue.generate!(:parent_issue_id => 1), :hours => 3)
TimeEntry.generate!(:issue => Issue.generate!(:parent_issue_id => 1), :hours => 4)
assert_match "href=\"/time_entries?issue_id=~1\"", CGI.unescape(issue_spent_hours_details(Issue.find(1)))
end
end
end
end end

View file

@ -48,4 +48,30 @@ class JournalsHelperTest < Redmine::HelperTest
assert_kind_of Attachment, thumbnails.first assert_kind_of Attachment, thumbnails.first
assert_equal 'image.png', thumbnails.first.filename assert_equal 'image.png', thumbnails.first.filename
end end
def test_journal_thumbnail_attachments_should_be_in_the_same_order_as_the_journal_details
skip unless convert_installed?
set_tmp_attachments_directory
issue = Issue.generate!
# Thumbnails should be displayed in the same order as Journal.detail, not in attachment id order.
attachment1 = Attachment.generate!(:file => mock_file_with_options(:original_filename => 'image1.png'), :author => User.find(1))
attachment2 = Attachment.generate!(:file => mock_file_with_options(:original_filename => 'image2.png'), :author => User.find(1))
journal = Journal.create!(:journalized => issue, :user_id => 1)
JournalDetail.create!(
:journal => journal, :property => 'attachment',
:prop_key => attachment2.id.to_s,
:value => 'image2.png'
)
JournalDetail.create!(
:journal => journal, :property => 'attachment',
:prop_key => attachment1.id.to_s,
:value => 'image1.png'
)
journal.reload
thumbnails = journal_thumbnail_attachments(journal)
assert_equal 2, thumbnails.count
assert_equal 2, journal.details.count
assert_equal journal.details.map(&:value), thumbnails.map(&:filename)
end
end end

View file

@ -229,4 +229,51 @@ class Redmine::ApiTest::AttachmentsTest < Redmine::ApiTest::Base
assert attachment.digest.present? assert attachment.digest.present?
assert File.exist? attachment.diskfile assert File.exist? attachment.diskfile
end end
test "POST /uploads.json should be compatible with an fcgi's input" do
set_tmp_attachments_directory
assert_difference 'Attachment.count' do
post(
'/uploads.json',
:headers => {
"CONTENT_TYPE" => 'application/octet-stream',
"CONTENT_LENGTH" => '12',
"rack.input" => Rack::RewindableInput.new(StringIO.new('File content'))
}.merge(credentials('jsmith'))
)
assert_response :created
end
json = ActiveSupport::JSON.decode(response.body)
assert_kind_of Hash, json['upload']
token = json['upload']['token']
assert token.present?
assert attachment = Attachment.find_by_token(token)
assert_equal 12, attachment.filesize
assert File.exist? attachment.diskfile
end
test "POST /uploads.json should be compatible with a uwsgi's input" do
set_tmp_attachments_directory
assert_difference 'Attachment.count' do
request_body = Rack::RewindableInput.new(StringIO.new('File content'))
# Uwsgi_IO object does not have size method
request_body.instance_eval('undef :size', __FILE__, __LINE__)
post(
'/uploads.json',
:headers => {
"CONTENT_TYPE" => 'application/octet-stream',
"CONTENT_LENGTH" => '12',
"rack.input" => request_body
}.merge(credentials('jsmith'))
)
assert_response :created
end
json = ActiveSupport::JSON.decode(response.body)
assert_kind_of Hash, json['upload']
token = json['upload']['token']
assert token.present?
assert attachment = Attachment.find_by_token(token)
assert_equal 12, attachment.filesize
assert File.exist? attachment.diskfile
end
end end

View file

@ -653,6 +653,34 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base
assert_response 422 assert_response 422
end end
test "POST /issues.json with invalid project_id and any assigned_to_id should respond with 422" do
post(
'/issues.json',
:params => {
:issue => {
:project_id => 999,
:assigned_to_id => 1,
:subject => 'API'
}
},
:headers => credentials('jsmith'))
assert_response 422
end
test "POST /issues.json with invalid project_id and any fixed_version_id should respond with 422" do
post(
'/issues.json',
:params => {
:issue => {
:project_id => 999,
:fixed_version_id => 1,
:subject => 'API'
}
},
:headers => credentials('jsmith'))
assert_response 422
end
test "PUT /issues/:id.xml" do test "PUT /issues/:id.xml" do
assert_difference('Journal.count') do assert_difference('Journal.count') do
put( put(

View file

@ -119,6 +119,17 @@ class Redmine::ApiTest::WikiPagesTest < Redmine::ApiTest::Base
assert_equal 'jsmith', page.content.author.login assert_equal 'jsmith', page.content.author.login
end end
test "GET /projects/:project_id/wiki/:title/:version.xml should not includ author if not exists" do
WikiContentVersion.find_by_id(2).update(author_id: nil)
get '/projects/ecookbook/wiki/CookBook_documentation/2.xml'
assert_response 200
assert_equal 'application/xml', response.media_type
assert_select 'wiki_page' do
assert_select 'author', 0
end
end
test "PUT /projects/:project_id/wiki/:title.xml with current versino should update wiki page" do test "PUT /projects/:project_id/wiki/:title.xml with current versino should update wiki page" do
assert_no_difference 'WikiPage.count' do assert_no_difference 'WikiPage.count' do
assert_difference 'WikiContent::Version.count' do assert_difference 'WikiContent::Version.count' do

View file

@ -29,5 +29,7 @@ class WelcomeTest < Redmine::IntegrationTest
assert_equal 'text/plain', @response.content_type assert_equal 'text/plain', @response.content_type
# Redmine::Utils.relative_url_root does not effect on Rails 5.1.4. # Redmine::Utils.relative_url_root does not effect on Rails 5.1.4.
assert @response.body.match(%r{^Disallow: /projects/ecookbook/issues\r?$}) assert @response.body.match(%r{^Disallow: /projects/ecookbook/issues\r?$})
assert @response.body.match(%r{^Disallow: /issues\?sort=\r?$})
assert @response.body.match(%r{^Disallow: /issues\?\*set_filter=\r?$})
end end
end end

View file

@ -0,0 +1,145 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2020 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 File.expand_path('../../application_system_test_case', __FILE__)
class InlineAutocompleteSystemTest < ApplicationSystemTestCase
fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
:trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues,
:enumerations, :custom_fields, :custom_values, :custom_fields_trackers,
:watchers, :journals, :journal_details, :versions,
:workflows
def test_inline_autocomplete_for_issues
log_user('jsmith', 'jsmith')
visit 'issues/new'
fill_in 'Description', :with => '#'
within('.tribute-container') do
assert page.has_text? 'Bug #12: Closed issue on a locked version'
assert page.has_text? 'Bug #1: Cannot print recipes'
first('li').click
end
assert_equal '#12 ', find('#issue_description').value
end
def test_inline_autocomplete_filters_autocomplete_items
log_user('jsmith', 'jsmith')
visit 'issues/new'
fill_in 'Description', :with => '#Closed'
within('.tribute-container') do
assert page.has_text? 'Bug #12: Closed issue on a locked version'
assert page.has_text? 'Bug #11: Closed issue on a closed version'
assert_not page.has_text? 'Bug #1: Cannot print recipes'
end
end
def test_inline_autocomplete_on_issue_edit_description_should_show_autocomplete
log_user('jsmith', 'jsmith')
visit 'issues/1/edit'
within('#issue-form') do
click_link('Edit', match: :first)
fill_in 'Description', :with => '#'
end
page.has_css?('.tribute-container li', minimum: 1)
end
def test_inline_autocomplete_on_issue_edit_notes_should_show_autocomplete
log_user('jsmith', 'jsmith')
visit 'issues/1/edit'
# Prevent random fails because the element is not yet enabled
find('#issue_notes').click
fill_in 'issue[notes]', :with => '#'
page.has_css?('.tribute-container li', minimum: 1)
end
def test_inline_autocomplete_on_issue_custom_field_with_full_text_formatting_should_show_autocomplete
IssueCustomField.create!(
:name => 'Full width field',
:field_format => 'text', :full_width_layout => '1',
:tracker_ids => [1], :is_for_all => true, :text_formatting => 'full'
)
log_user('jsmith', 'jsmith')
visit 'issues/new'
fill_in 'Full width field', :with => '#'
page.has_css?('.tribute-container li', minimum: 1)
end
def test_inline_autocomplete_on_wiki_should_show_autocomplete
log_user('jsmith', 'jsmith')
visit 'projects/ecookbook/wiki/CookBook_documentation/edit'
# Prevent random fails because the element is not yet enabled
find('.wiki-edit').click
fill_in 'content[text]', :with => '#'
page.has_css?('.tribute-container li', minimum: 1)
end
def test_inline_autocomplete_on_news_description_should_show_autocomplete
log_user('jsmith', 'jsmith')
visit 'projects/ecookbook/news'
click_link 'Add news'
# Prevent random fails because the element is not yet enabled
find('.wiki-edit').click
fill_in 'Description', :with => '#'
page.has_css?('.tribute-container li', minimum: 1)
end
def test_inline_autocomplete_on_new_message_description_should_show_autocomplete
log_user('jsmith', 'jsmith')
visit 'projects/ecookbook/boards/1'
click_link 'New message'
# Prevent random fails because the element is not yet enabled
find('.wiki-edit').click
fill_in 'message[content]', :with => '#'
page.has_css?('.tribute-container li', minimum: 1)
end
def test_inline_autocomplete_for_issues_should_escape_html_elements
issue = Issue.generate!(subject: 'This issue has a <select> element', project_id: 1, tracker_id: 1)
log_user('jsmith', 'jsmith')
visit 'projects/1/issues/new'
fill_in 'Description', :with => '#This'
within('.tribute-container') do
assert page.has_text? "Bug ##{issue.id}: This issue has a <select> element"
end
end
end

View file

@ -152,6 +152,19 @@ class AttachmentTest < ActiveSupport::TestCase
end end
end end
def test_extension_update_should_be_validated_against_denied_extensions
with_settings :attachment_extensions_denied => "txt, png" do
a = Attachment.new(:container => Issue.find(1),
:file => mock_file_with_options(:original_filename => "test.jpeg"),
:author => User.find(1))
assert_save a
b = Attachment.find(a.id)
b.filename = "test.png"
assert !b.save
end
end
def test_valid_extension_should_be_case_insensitive def test_valid_extension_should_be_case_insensitive
with_settings :attachment_extensions_allowed => "txt, Png" do with_settings :attachment_extensions_allowed => "txt, Png" do
assert Attachment.valid_extension?(".pnG") assert Attachment.valid_extension?(".pnG")
@ -235,6 +248,23 @@ class AttachmentTest < ActiveSupport::TestCase
assert_not_equal a1.diskfile, a2.diskfile assert_not_equal a1.diskfile, a2.diskfile
end end
def test_identical_attachments_created_in_same_transaction_should_not_end_up_unreadable
attachments = []
Project.transaction do
3.times do
a = Attachment.create!(
:container => Issue.find(1), :author => User.find(1),
:file => mock_file(:filename => 'foo', :content => 'abcde')
)
attachments << a
end
end
attachments.each do |a|
assert a.readable?
end
assert_equal 1, attachments.map(&:diskfile).uniq.size
end
def test_filename_should_be_basenamed def test_filename_should_be_basenamed
a = Attachment.new(:file => mock_file(:original_filename => "path/to/the/file")) a = Attachment.new(:file => mock_file(:original_filename => "path/to/the/file"))
assert_equal 'file', a.filename assert_equal 'file', a.filename

View file

@ -241,6 +241,16 @@ class IssueSubtaskingTest < ActiveSupport::TestCase
end end
end end
def test_done_ratio_of_parent_with_completed_children_should_not_be_99
with_settings :parent_issue_done_ratio => 'derived' do
parent = Issue.generate!
parent.generate_child!(:estimated_hours => 8.0, :done_ratio => 100)
parent.generate_child!(:estimated_hours => 8.1, :done_ratio => 100)
# (8.0 * 100 + 8.1 * 100) / (8.0 + 8.1) => 99.99999999999999
assert_equal 100, parent.reload.done_ratio
end
end
def test_changing_parent_should_update_previous_parent_done_ratio def test_changing_parent_should_update_previous_parent_done_ratio
with_settings :parent_issue_done_ratio => 'derived' do with_settings :parent_issue_done_ratio => 'derived' do
first_parent = Issue.generate! first_parent = Issue.generate!

View file

@ -898,6 +898,23 @@ class IssueTest < ActiveSupport::TestCase
assert_equal Date.parse('2012-07-14'), issue.due_date assert_equal Date.parse('2012-07-14'), issue.due_date
end end
def test_safe_attributes_notes_should_check_add_issue_notes_permission
# With add_issue_notes permission
user = User.find(2)
issue = Issue.new(:project => Project.find(1))
issue.init_journal(user)
issue.send :safe_attributes=, {'notes' => 'note'}, user
assert_equal 'note', issue.notes
# Without add_issue_notes permission
Role.find(1).remove_permission!(:add_issue_notes)
issue = Issue.new(:project => Project.find(1))
user.reload
issue.init_journal(user)
issue.send :safe_attributes=, {'notes' => 'note'}, user
assert_equal '', issue.notes
end
def test_safe_attributes_should_accept_target_tracker_enabled_fields def test_safe_attributes_should_accept_target_tracker_enabled_fields
source = Tracker.find(1) source = Tracker.find(1)
source.core_fields = [] source.core_fields = []
@ -1459,6 +1476,23 @@ class IssueTest < ActiveSupport::TestCase
assert_equal [3, nil], copy.children.map(&:assigned_to_id) assert_equal [3, nil], copy.children.map(&:assigned_to_id)
end end
def test_copy_should_not_add_attachments_to_journal
set_tmp_attachments_directory
issue = Issue.generate!
copy = Issue.new
copy.init_journal User.find(1)
copy.copy_from issue
copy.project = issue.project
copy.save_attachments(
{ 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')} }
)
assert copy.save
assert j = copy.journals.last
assert_equal 1, j.details.size
assert_equal 'relation', j.details[0].property
end
def test_should_not_call_after_project_change_on_creation def test_should_not_call_after_project_change_on_creation
issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
:subject => 'Test', :author_id => 1) :subject => 'Test', :author_id => 1)

View file

@ -20,6 +20,7 @@
require File.expand_path('../../../../test_helper', __FILE__) require File.expand_path('../../../../test_helper', __FILE__)
class Redmine::CipheringTest < ActiveSupport::TestCase class Redmine::CipheringTest < ActiveSupport::TestCase
fixtures :auth_sources
def test_password_should_be_encrypted def test_password_should_be_encrypted
Redmine::Configuration.with 'database_cipher_key' => 'secret' do Redmine::Configuration.with 'database_cipher_key' => 'secret' do
@ -106,4 +107,12 @@ class Redmine::CipheringTest < ActiveSupport::TestCase
assert_equal 'bar', r.read_attribute(:password) assert_equal 'bar', r.read_attribute(:password)
end end
end end
def test_encrypt_all_and_decrypt_all_should_skip_validation
auth_source = auth_sources(:auth_sources_001)
# validator checks if AuthSource#host is present
auth_source.update_column(:host, nil)
assert AuthSource.encrypt_all(:account_password)
assert AuthSource.decrypt_all(:account_password)
end
end end

View file

@ -44,6 +44,12 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
def gantt_start def gantt_start
@gantt.date_from @gantt.date_from
end end
private :gantt_start
def gantt_end
@gantt.date_to
end
private :gantt_end
# Creates a Gantt chart for a 4 week span # Creates a Gantt chart for a 4 week span
def create_gantt(project=Project.generate!, options={}) def create_gantt(project=Project.generate!, options={})
@ -354,6 +360,26 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
assert_select 'div.task_todo[style*="left:28px"]', 1 assert_select 'div.task_todo[style*="left:28px"]', 1
end end
test "#line todo line should appear if it ends on the leftmost date in the gantt" do
create_gantt
[gantt_start - 1, gantt_start].each do |start_date|
@output_buffer = @gantt.line(start_date, gantt_start, 30, false, 'line', :format => :html, :zoom => 4)
# the leftmost date (Date.today - 14 days)
assert_select 'div.task_todo[style*="left:0px"]', 1, @output_buffer
assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer
end
end
test "#line todo line should appear if it starts on the rightmost date in the gantt" do
create_gantt
[gantt_end, gantt_end + 1].each do |end_date|
@output_buffer = @gantt.line(gantt_end, end_date, 30, false, 'line', :format => :html, :zoom => 4)
# the rightmost date (Date.today + 14 days)
assert_select 'div.task_todo[style*="left:112px"]', 1, @output_buffer
assert_select 'div.task_todo[style*="width:2px"]', 1, @output_buffer
end
end
test "#line todo line should be the total width" do test "#line todo line should be the total width" do
create_gantt create_gantt
@output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4) @output_buffer = @gantt.line(today - 7, today + 7, 30, false, 'line', :format => :html, :zoom => 4)
@ -424,6 +450,9 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
@output_buffer = @gantt.line(today - 7, today + 7, 30, true, 'line', :format => :html, :zoom => 4) @output_buffer = @gantt.line(today - 7, today + 7, 30, true, 'line', :format => :html, :zoom => 4)
assert_select "div.starting", 1 assert_select "div.starting", 1
assert_select 'div.starting[style*="left:28px"]', 1 assert_select 'div.starting[style*="left:28px"]', 1
# starting marker on the leftmost boundary of the gantt
@output_buffer = @gantt.line(gantt_start, today + 7, 30, true, 'line', :format => :html, :zoom => 4)
assert_select 'div.starting[style*="left:0px"]', 1
end end
test "#line starting marker should not appear if the start date is before gantt start date" do test "#line starting marker should not appear if the start date is before gantt start date" do
@ -437,6 +466,9 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
@output_buffer = @gantt.line(today - 7, today + 7, 30, true, 'line', :format => :html, :zoom => 4) @output_buffer = @gantt.line(today - 7, today + 7, 30, true, 'line', :format => :html, :zoom => 4)
assert_select "div.ending", 1 assert_select "div.ending", 1
assert_select 'div.ending[style*="left:88px"]', 1 assert_select 'div.ending[style*="left:88px"]', 1
# ending marker on the rightmost boundary of the gantt
@output_buffer = @gantt.line(today - 7, gantt_end, 30, true, 'line', :format => :html, :zoom => 4)
assert_select 'div.ending[style*="left:116px"]', 1
end end
test "#line ending marker should not appear if the end date is before gantt start date" do test "#line ending marker should not appear if the end date is before gantt start date" do

View file

@ -23,7 +23,8 @@ class Redmine::ProjectJumpBoxTest < ActiveSupport::TestCase
fixtures :users, :projects, :user_preferences fixtures :users, :projects, :user_preferences
def setup def setup
@user = User.find_by_login 'dlopper' @user = User.find_by_login 'jsmith'
User.current = @user
@ecookbook = Project.find 'ecookbook' @ecookbook = Project.find 'ecookbook'
@onlinestore = Project.find 'onlinestore' @onlinestore = Project.find 'onlinestore'
end end
@ -142,4 +143,16 @@ class Redmine::ProjectJumpBoxTest < ActiveSupport::TestCase
assert_equal @onlinestore, pjb.recently_used_projects.first assert_equal @onlinestore, pjb.recently_used_projects.first
assert_equal @ecookbook, pjb.recently_used_projects.last assert_equal @ecookbook, pjb.recently_used_projects.last
end end
def test_recents_list_should_include_only_visible_projects
@user = User.find_by_login 'dlopper'
User.current = @user
pjb = Redmine::ProjectJumpBox.new @user
pjb.project_used @ecookbook
pjb.project_used @onlinestore
assert_equal 1, pjb.recently_used_projects.size
assert_equal @ecookbook, pjb.recently_used_projects.first
end
end end

View file

@ -1005,6 +1005,18 @@ class MailHandlerTest < ActiveSupport::TestCase
end end
end end
def test_reply_to_an_issue_without_permission
set_tmp_attachments_directory
# "add_issue_notes" permission is explicit required to allow users to add notes
# "edit_issue" permission no longer includes the "add_issue_notes" permission
Role.all.each {|r| r.remove_permission! :add_issue_notes}
assert_no_difference 'Issue.count' do
assert_no_difference 'Journal.count' do
assert_not submit_email('ticket_reply_with_status.eml')
end
end
end
def test_reply_to_a_nonexitent_journal def test_reply_to_a_nonexitent_journal
journal_id = Issue.find(2).journals.last.id journal_id = Issue.find(2).journals.last.id
Journal.destroy(journal_id) Journal.destroy(journal_id)
@ -1056,6 +1068,13 @@ class MailHandlerTest < ActiveSupport::TestCase
end end
end end
def test_reply_to_a_topic_without_permission
Role.all.each {|r| r.remove_permission! :add_messages}
assert_no_difference('Message.count') do
assert_not submit_email('message_reply_by_subject.eml')
end
end
def test_should_convert_tags_of_html_only_emails def test_should_convert_tags_of_html_only_emails
with_settings :text_formatting => 'textile' do with_settings :text_formatting => 'textile' do
issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'}) issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})

View file

@ -344,6 +344,13 @@ class ProjectTest < ActiveSupport::TestCase
assert_equal parent.children.sort_by(&:name), parent.children.to_a assert_equal parent.children.sort_by(&:name), parent.children.to_a
end end
def test_validate_custom_field_values_of_project
User.current = User.find(3)
ProjectCustomField.generate!(:name => 'CustomFieldTest', :field_format => 'int', :is_required => true, :visible => false, :role_ids => [1])
p = Project.new(:name => 'Project test', :identifier => 'project-t')
assert p.save!
end
def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
# Parent issue with a hierarchy project's fixed version # Parent issue with a hierarchy project's fixed version
parent_issue = Issue.find(1) parent_issue = Issue.find(1)

Some files were not shown because too many files have changed in this diff Show more