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'
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 'globalid', '~> 0.4.2' if Gem.ruby_version < Gem::Version.new('2.6.0')
gem "rouge", "~> 3.12.0"
gem "request_store", "~> 1.4.1"
gem "mini_mime", "~> 1.0.1"
gem "actionpack-xml_parser"
gem "roadie-rails", (RUBY_VERSION < "2.5" ? "~> 1.3.0" : "~> 2.1.0")
gem "mimemagic"
gem 'marcel'
gem "mail", "~> 2.7.1"
gem "csv", "~> 3.1.1"
gem "nokogiri", "~> 1.10.0"
gem 'csv', (RUBY_VERSION < '2.5' ? ['>= 3.1.1', '<= 3.1.5'] : '~> 3.1.1')
gem 'nokogiri', (RUBY_VERSION < '2.5' ? '~> 1.10.0' : '~> 1.11.1')
gem "i18n", "~> 1.6.0"
gem "rbpdf", "~> 1.20.0"
@ -38,7 +39,7 @@ end
# Optional Markdown support, not for JRuby
group :markdown do
gem "redcarpet", "~> 3.5.0"
gem 'redcarpet', '~> 3.5.1'
end
# Include database gems for the adapters found in the database
@ -47,7 +48,8 @@ require 'erb'
require 'yaml'
database_file = File.join(File.dirname(__FILE__), "config/database.yml")
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
if adapters.any?
adapters.each do |adapter|

0
Rakefile Normal file → Executable file
View file

View file

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

View file

@ -33,7 +33,7 @@ class ActivitiesController < ApplicationController
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
if params[:user_id].present?
@author = User.active.find(params[:user_id])
@author = User.visible.active.find(params[:user_id])
end
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
@ -55,7 +55,12 @@ class ActivitiesController < ApplicationController
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])
respond_to do |format|

View file

@ -101,7 +101,7 @@ class AttachmentsController < ApplicationController
return
end
@attachment = Attachment.new(:file => request.raw_post)
@attachment = Attachment.new(:file => raw_request_body)
@attachment.author = User.current
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
@attachment.content_type = params[:content_type].presence
@ -265,4 +265,14 @@ class AttachmentsController < ApplicationController
def update_all_params
params.permit(:attachments => [:filename, :description]).require(:attachments)
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

View file

@ -18,6 +18,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MailHandlerController < ActionController::Base
include ActiveSupport::SecurityUtils
before_action :check_credential
# Displays the email submission form
@ -39,7 +41,7 @@ class MailHandlerController < ActionController::Base
def check_credential
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
end
end

View file

@ -307,7 +307,7 @@ class RepositoriesController < ApplicationController
render_404
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
@project = Project.find(params[:id])
@ -318,14 +318,12 @@ class RepositoriesController < ApplicationController
end
(render_404; return false) unless @repository
@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)
if @repository.branches.blank?
raise InvalidRevisionParam
end
end
@rev = params[:rev].to_s.strip.presence || @repository.default_branch
raise InvalidRevisionParam unless valid_name?(@rev)
@rev_to = params[:rev_to].to_s.strip.presence
raise InvalidRevisionParam unless valid_name?(@rev_to)
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
@ -410,4 +408,11 @@ class RepositoriesController < ApplicationController
'attachment'
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

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)}
end
@scope = @object_types.select {|t| params[t]}
@scope = @object_types.select {|t| params[t].present?}
@scope = @object_types if @scope.empty?
fetcher = Redmine::Search::Fetcher.new(

View file

@ -18,6 +18,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SysController < ActionController::Base
include ActiveSupport::SecurityUtils
before_action :check_enabled
def projects
@ -76,7 +78,7 @@ class SysController < ActionController::Base
def check_enabled
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
return false
end

View file

@ -134,7 +134,9 @@ class WatchersController < ApplicationController
def find_objets_from_params
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]))
if klass.reflect_on_association(:project)

View file

@ -44,8 +44,6 @@ class WikiController < ApplicationController
helper :watchers
include Redmine::Export::PDF
include ActionView::Helpers::SanitizeHelper
# List of pages, sorted alphabetically and by parent (hierarchy)
def index
load_pages_for_index
@ -91,7 +89,7 @@ class WikiController < ApplicationController
end
@content = @page.content_for_version(params[:version])
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
render :action => 'edit'
else
@ -111,7 +109,7 @@ class WikiController < ApplicationController
send_data(export, :type => 'text/html', :filename => filename_for_content_disposition("#{@page.title}.html"))
return
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
end
end

View file

@ -1190,7 +1190,7 @@ module ApplicationHelper
)|
(
(?<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
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'
tags << "\n".html_safe + javascript_tag("$(window).on('load', function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
end

View file

@ -217,6 +217,11 @@ module IssuesHelper
if issue.total_spent_hours == issue.spent_hours
link_to(l_hours_short(issue.spent_hours), path)
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 += " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
s.html_safe
@ -551,7 +556,7 @@ module IssuesHelper
unless no_html
diff_link =
link_to(
'diff',
l(:label_diff),
diff_journal_url(detail.journal_id, :detail_id => detail.id,
:only_path => options[:only_path]),
:title => l(:label_view_diff))
@ -577,6 +582,7 @@ module IssuesHelper
end
# 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)
return nil if id.blank?
@detail_value_name_by_reflection ||= Hash.new do |hash, key|
@ -584,7 +590,7 @@ module IssuesHelper
name = nil
if association
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')
end
end

View file

@ -22,7 +22,7 @@ module JournalsHelper
# Returns the attachments of a journal that are displayed as thumbnails
def journal_thumbnail_attachments(journal)
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
# Returns the action links for an issue journal

View file

@ -167,7 +167,7 @@ module QueriesHelper
def total_tag(column, value)
label = content_tag('span', "#{column.caption}:")
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)
else
format_object(value)
@ -238,7 +238,7 @@ module QueriesHelper
'span',
value.to_s(item) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
:class => value.css_classes_for(item))
when :hours, :estimated_hours
when :hours, :estimated_hours, :total_estimated_hours
format_hours(value)
when :spent_hours
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.
module SearchHelper
include ActionView::Helpers::SanitizeHelper
def highlight_tokens(text, tokens)
return text unless text && tokens && !tokens.empty?
re_tokens = tokens.collect {|t| Regexp.escape(t)}
regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
result = +''
text = strip_tags(text)
text.split(regexp).each_with_index do |words, i|
if result.length > 1200
# maximum length of the preview reached

View file

@ -44,7 +44,7 @@ module TimelogHelper
def user_collection_for_select_options(time_entry)
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)
end

View file

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

View file

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

View file

@ -513,7 +513,8 @@ class IssueQuery < Query
def sql_for_fixed_version_status_field(field, operator, value)
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} #{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)
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} #{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
unless handler_options[:no_permission_check]
unless user.allowed_to?(:add_issue_notes, issue.project) ||
user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction, "not allowed to add notes on issues to project [#{project.name}]"
unless issue.notes_addable?
raise UnauthorizedAction, "not allowed to add notes on issues to project [#{issue.project.name}]"
end
end
@ -276,7 +275,7 @@ class MailHandler < ActionMailer::Base
end
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
if !message.locked?

View file

@ -883,6 +883,17 @@ class Project < ActiveRecord::Base
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
def editable_custom_field_values(user=nil)
visible_custom_field_values(user)

View file

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

View file

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

View file

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

View file

@ -100,7 +100,8 @@ class Setting < ActiveRecord::Base
v = read_attribute(:value)
# Unserialize serialized settings
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)
end
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
end
@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
@invalid_issue_id = issue_id
end
@ -122,7 +129,14 @@ class TimeEntry < ActiveRecord::Base
else
@invalid_user_id = nil
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
attrs
end
@ -193,7 +207,7 @@ class TimeEntry < ActiveRecord::Base
# Returns the custom_field_values that can be edited by the given user
def editable_custom_field_values(user=nil)
visible_custom_field_values
visible_custom_field_values(user)
end
# 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)
add_available_filter(
"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(
"project.status",
@ -101,7 +101,7 @@ class TimeEntryQuery < Query
add_available_filter "comments", :type => :text
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_custom_fields_filters(issue_custom_fields, :issue)
add_associations_custom_fields_filters :user
@ -110,11 +110,11 @@ class TimeEntryQuery < Query
def available_columns
return @available_columns if @available_columns
@available_columns = self.class.available_columns.dup
@available_columns += TimeEntryCustomField.visible.
@available_columns += time_entry_custom_fields.visible.
map {|cf| QueryCustomFieldColumn.new(cf) }
@available_columns += issue_custom_fields.visible.
map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
@available_columns += ProjectCustomField.visible.
@available_columns += project_custom_fields.visible.
map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) }
@available_columns
end

View file

@ -115,11 +115,13 @@ class Token < ActiveRecord::Base
return nil unless action.present? && /\A[a-z0-9]+\z/i.match?(key)
token = Token.find_by(:action => action, :value => key)
if token && (token.action == action) && (token.value == key) && token.user
if validity_days.nil? || (token.created_on > validity_days.days.ago)
token
end
end
return unless token
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
end
def self.generate_token_value

View file

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

View file

@ -1,28 +1,27 @@
<%= call_hook :view_account_login_top %>
<div id="login-form">
<h2><%= l(:label_login) %></h2>
<%= form_tag(signin_path, onsubmit: 'return keepAnchorOnSignIn(this);') do %>
<%= back_url_hidden_field_tag %>
<label for="username"><%=l(:field_login)%></label>
<%= text_field_tag 'username', params[:username], :tabindex => '1' %>
<label for="password">
<%=l(:field_password)%>
<%= link_to l(:label_password_lost), lost_password_path, :class => "lost_password" if Setting.lost_password? %>
</label>
<%= password_field_tag 'password', nil, :tabindex => '2' %>
<% if Setting.openid? %>
<label for="openid_url"><%=l(:field_identity_url)%></label>
<%= text_field_tag "openid_url", nil, :tabindex => '3' %>
<% end %>
<% if Setting.autologin? %>
<label for="autologin"><%= check_box_tag 'autologin', 1, false, :tabindex => 4 %> <%= l(:label_stay_logged_in) %></label>
<% end %>
<input type="submit" name="login" value="<%=l(:button_login)%>" tabindex="5" id="login-submit" />
<% end %>
</div>

View file

@ -1,5 +1,5 @@
<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>
</p>
<p><%= f.text_field(:default_value) %></p>

View file

@ -1,24 +1,22 @@
<%= error_messages_for @document %>
<div class="box tabular">
<p><%= f.select :category_id, DocumentCategory.active.collect {|c| [c.name, c.id]} %></p>
<p><%= f.text_field :title, :required => true, :size => 60 %></p>
<p><%= f.text_area :description, :cols => 60, :rows => 15, :class => 'wiki-edit',
:data => {
:auto_complete => true,
:issues_url => auto_complete_issues_path(:project_id => @project, :q => '')
}
%></p>
<p><%= f.select :category_id, DocumentCategory.active.collect {|c| [c.name, c.id]} %></p>
<p><%= f.text_field :title, :required => true, :size => 60 %></p>
<p><%= f.text_area :description, :cols => 60, :rows => 15, :class => 'wiki-edit',
:data => {
:auto_complete => true,
:issues_url => auto_complete_issues_path(:project_id => @project, :q => '')
}
%></p>
<% @document.custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :document, value %></p>
<% end %>
<% @document.custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :document, value %></p>
<% end %>
<% if @document.new_record? %>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form', :locals => {:container => @document} %></p>
<% end %>
</div>
<%= 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 %>
</td>
<% @query.columns.each do |column| %>
<% next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name) %>
<td class="gantt_<%= column.name %>_column gantt_selected_column <%= 'last_gantt_selected_column' if @query.columns.last == column %>" id="<%= column.name %>">
<%
@query.columns.each do |column|
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 += "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 += 'overflow: hidden;'
@ -195,7 +198,7 @@
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, :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 %>
<% end %>
<% end %>

View file

@ -21,7 +21,7 @@
</div>
</div>
<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>
<% end %>
<% end %>

View file

@ -13,7 +13,7 @@
<%= link_to_if_authorized l(:label_settings),
{: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 %>
</div>

View file

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

View file

@ -2,27 +2,22 @@
<html lang="<%= current_language %>">
<head>
<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>
<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="keywords" content="issue,bug,tracker" />
<%= csrf_meta_tag %>
<%= 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' %>
<% is_welcome = !User.current.logged? && current_page?(:controller => 'welcome', :action => 'index') %>
<%= stylesheet_link_tag 'frontpage', :media => 'all' if is_welcome %>
<%= javascript_heads %>
<script src="/themes/circlepro/javascripts/cookieconsent.min.js"></script>
<%= heads_for_theme %>
<%= call_hook :view_layouts_base_html_head %>
<!-- page specific tags -->
<%= yield :header_tags -%>
</head>
<body class="<%= body_css_classes %><%= ' is-preload' if is_welcome %>">
<body class="<%= body_css_classes %>">
<%= call_hook :view_layouts_base_body_top %>
<div id="wrapper">
@ -65,31 +60,20 @@
<div id="wrapper2">
<div id="wrapper3">
<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">
<%= render_menu :account_menu -%>
</div>
<%= 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? -%>
</div>
</div>
<div id="header">
<a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a>
<div id="wrapper-header">
<% 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 %>
<%= 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 %>
@ -107,17 +91,14 @@
<% end %>
<h1><%= page_header_title %></h1>
</div>
<% if display_main_menu?(@project) %>
<div id="main-menu" class="tabs">
<div id="wrapper-main-menu">
<%= render_main_menu(@project) %>
<div class="tabs-buttons" style="display:none;">
<button class="tab-left" onclick="moveTabLeft(this); return false;"></button>
<button class="tab-right" onclick="moveTabRight(this); return false;"></button>
</div>
</div>
</div>
<% end %>
</div>
@ -135,54 +116,16 @@
<div style="clear:both;"></div>
</div>
</div>
</div> <!-- #wrapper3 -->
<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 id="footer">
Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2022 Jean-Philippe Lang
</div>
</div>
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></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> <!-- #wrapper2 -->
</div> <!-- #wrapper -->
<%= 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>
</html>

View file

@ -31,7 +31,7 @@
<%= textilizable @project.description %>
</div>
<% 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>
<% 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>

View file

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

View file

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

View file

@ -33,7 +33,9 @@
</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) %>
<%= render :partial => 'related_issues' %>

View file

@ -32,13 +32,13 @@
<% end %>
<div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>">
<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>
<% 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>
<% end %>
</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 :must_change_passwd %></p>
</div>

View file

@ -7,7 +7,7 @@
<%=
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 %>

View file

@ -10,3 +10,6 @@ Disallow: <%= url_for(issues_gantt_path) %>
Disallow: <%= url_for(issues_calendar_path) %>
Disallow: <%= url_for(activity_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
api.text @content.text
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.created_on @page.created_on
api.updated_on @content.updated_on

View file

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

View file

@ -4,7 +4,7 @@
<th>
<%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled)')",
: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)%>
</th>
<th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
@ -15,7 +15,7 @@
<td style="width:<%= 75 / @statuses.size %>%;">
<%= 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)}",
:class => 'icon-only icon-checked') %>
:class => 'no-tooltip icon-only icon-checked') %>
<%= new_status.name %>
</td>
<% end %>
@ -29,7 +29,7 @@
<td class="name">
<%= 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)}",
:class => 'icon-only icon-checked') %>
:class => 'no-tooltip icon-only icon-checked') %>
<% if old_status %>
<% old_status_name = old_status.name %>
<%= old_status_name %>
@ -40,7 +40,7 @@
</td>
<% for new_status in @statuses -%>
<% 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 %>
</td>
<% end -%>

View file

@ -67,7 +67,7 @@
<% for status in @statuses -%>
<td class="<%= @permissions[status.id][field].try(:join, ' ') %>" title="<%= name %> (<%= status.name %>)">
<%= 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>
<% end -%>
</tr>

View file

@ -79,7 +79,8 @@ module RedmineApp
config.session_store :cookie_store,
: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'))
instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))

View file

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

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

View file

@ -2,7 +2,7 @@ class PopulateMemberRoles < ActiveRecord::Migration[4.2]
def self.up
MemberRole.delete_all
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

View file

@ -1,8 +1,279 @@
== Redmine changelog
Redmine - project management software
Copyright (C) 2006-2019 Jean-Philippe Lang
http://www.redmine.org/
Copyright (C) 2006-2021 Jean-Philippe Lang
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

View file

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

View file

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

View file

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

View file

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

View file

@ -178,6 +178,14 @@ module Redmine
(path[-1,1] == "/") ? path[0..-2] : path
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
def retrieve_root_url

View file

@ -388,6 +388,18 @@ module Redmine
nil
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
# Returns the readable identifier
def format_identifier

View file

@ -291,6 +291,15 @@ module Redmine
Annotate.new
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
# Returns the readable identifier
def format_identifier

View file

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

View file

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

View file

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

View file

@ -295,8 +295,8 @@ bq. Rails - это полноценный, многоуровневый фрей
<h3><a name="11" class="wiki-page"></a>Содержание</h3>
<pre>
{{Содержание}} =&gt; содержание, выровненное по левому краю
{{&gt;Содержание}} =&gt; содержание, выровненное по правому краю
{{toc}} =&gt; содержание, выровненное по левому краю
{{&gt;toc}} =&gt; содержание, выровненное по правому краю
</pre>
<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) {
$('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
}
@ -371,15 +377,29 @@ function showIssueHistory(journal, url) {
switch(journal) {
case 'notes':
tab_content.find('.journal').show();
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;
case 'properties':
tab_content.find('.journal.has-notes').hide();
tab_content.find('.journal:not(.has-notes)').show();
tab_content.find('.journal').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;
default:
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;
@ -933,7 +953,7 @@ $(document).ready(function(){
$('#history .tabs').on('click', 'a', function(e){
var tab = $(e.target).attr('id').replace('tab-','');
document.cookie = 'history_last_tab=' + tab
document.cookie = 'history_last_tab=' + tab + '; SameSite=Lax'
});
});
@ -997,15 +1017,15 @@ function setupAttachmentDetail() {
$(function () {
$('[title]').tooltip({
show: {
delay: 400
},
position: {
my: "center bottom-5",
at: "center top"
}
});
$("[title]:not(.no-tooltip)").tooltip({
show: {
delay: 400
},
position: {
my: "center bottom-5",
at: "center top"
}
});
});
function inlineAutoComplete(element) {
@ -1048,6 +1068,9 @@ function inlineAutoComplete(element) {
requireLeadingSpace: true,
selectTemplate: function (issue) {
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; }
var clipboardData = e.clipboardData || e.originalEvent.clipboardData
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
for (var i = 0 ; i < items.length ; i++) {
var item = items[i];
if (item.type.indexOf("image") != -1) {
var blob = item.getAsFile();
var files = clipboardData.files
for (var i = 0 ; i < files.length ; i++) {
var file = files[i];
if (file.type.indexOf("image") != -1) {
var date = new Date();
var filename = 'clipboard-'
+ date.getFullYear()
@ -272,9 +271,8 @@ function copyImageFromClipboard(e) {
+ ('0'+date.getHours()).slice(-2)
+ ('0'+date.getMinutes()).slice(-2)
+ '-' + randomKey(5).toLocaleLowerCase()
+ '.' + blob.name.split('.').pop();
var file = new Blob([blob], {type: blob.type});
file.name = filename;
+ '.' + file.name.split('.').pop();
var inputEl = $('input:file.filedrop').first()
handleFileDropEvent.target = e.target;
addFile(inputEl, file, true);

View file

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

View file

@ -253,13 +253,16 @@ ganttEntryClick = function(e){
subject.nextAll('div').each(function(_, element){
var el = $(element);
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){
out_of_hierarchy = true;
if(target_shown == null) return false;
var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1);
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);
});
return true;
@ -272,15 +275,14 @@ ganttEntryClick = function(e){
total_height = 0;
}
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);
if(!is_shown)
el_task.css('top', target_top + total_height);
if(!el_task.hasClass('tooltip'))
el_task.toggle(!is_shown);
});
$('td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]'
).each(function (_, attr) {
$(el_selected_columns).each(function (_, attr) {
var el_attr = $(attr);
if (!is_shown)
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") {
this.textarea.value = this.textarea.value.substring(0, start) + subst +
this.textarea.value.substring(end);
if (sel) {
if (sel || (!prefix && start === end)) {
this.textarea.setSelectionRange(start + subst.length, start + subst.length);
} else {
this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);

View file

@ -474,7 +474,7 @@ select {
background-position: calc(100% - 7px) 50%;
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"] {
-webkit-appearance: button;
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 .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.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; }
#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.settings .wiki-preview, .tabular.settings .jstTabs { width: 99%; }
.tabular.settings .wiki-preview p {padding-left: 0 !important}
.tabular .wiki-preview p {
min-height: initial;
padding: 0;
@ -841,7 +844,7 @@ input#months { width: 46px; }
overflow: initial;
}
.tabular.settings p{ padding-left: 300px; }
.tabular.settings p { padding-left: 300px; }
.tabular.settings label{ margin-left: -300px; width: 295px; }
.tabular.settings textarea, .tabular.settings .wiki-preview, .tabular.settings .jstTabs { width: 99%; }
@ -1438,7 +1441,8 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
font-size: 0.9em;
border-radius: 3px;
border: 0;
box-shadow: none
box-shadow: none;
white-space: pre-wrap;
}
/***** Icons *****/

View file

@ -29,7 +29,6 @@ body{ color:#303030; background:#e8eaec; }
#main a { font-weight: bold; color: #467aa7;}
#main a:hover { color: #2a5a8a; text-decoration: underline; }
#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, .wiki h1 { letter-spacing:-1px; }

View file

@ -28,7 +28,8 @@ class ActivitiesControllerTest < Redmine::ControllerTest
:members,
:groups_users,
:enabled_modules,
:journals, :journal_details
:journals, :journal_details,
:attachments, :changesets, :documents, :messages, :news, :time_entries, :wiki_content_versions
def test_project_index
get :index, :params => {
@ -95,6 +96,18 @@ class ActivitiesControllerTest < Redmine::ControllerTest
assert_response 404
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
get :index, :params => {
:format => 'atom',
@ -111,6 +124,22 @@ class ActivitiesControllerTest < Redmine::ControllerTest
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
get :index, :params => {
:format => 'atom',

View file

@ -531,6 +531,23 @@ class AttachmentsControllerTest < Redmine::ControllerTest
assert_response 403
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
@request.session[:user_id] = 2
patch :update_all, :params => {
@ -659,4 +676,25 @@ class AttachmentsControllerTest < Redmine::ControllerTest
assert_response 302
assert Attachment.find_by_id(3)
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

View file

@ -124,6 +124,49 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'a[href="/issues/6"]', 0
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
@request.session[:user_id] = 2
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'
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
with_settings :new_item_menu_tab => '0' do
@request.session[:user_id] = 2
@ -1720,6 +1779,22 @@ class IssuesControllerTest < Redmine::ControllerTest
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
get :show, :params => {
:id => 1
@ -2615,6 +2690,32 @@ class IssuesControllerTest < Redmine::ControllerTest
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
@request.session[:user_id] = 2
get :new, :params => {
@ -4816,6 +4917,41 @@ class IssuesControllerTest < Redmine::ControllerTest
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
@request.session[:user_id] = 2
patch :edit, :params => {
@ -5222,6 +5358,24 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_equal spent_hours_before + 2.5, issue.spent_hours
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
parent = Issue.generate!(:project_id => 1, :is_private => true)
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'
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
issue = Issue.find(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 ["Dave2 Lopper2", "5", "locked"], json
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

View file

@ -428,4 +428,19 @@ class SearchControllerTest < Redmine::ControllerTest
assert_select 'dd span.highlight', :text => 'highlighted'
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

View file

@ -226,7 +226,7 @@ class TimelogControllerTest < Redmine::ControllerTest
assert_response :success
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

View file

@ -237,6 +237,7 @@ class UsersControllerTest < Redmine::ControllerTest
get :new
assert_response :success
assert_select 'input[name=?]', 'user[login]'
assert_select 'label[for=?]>span.required', 'user_password', 1
end
def test_create
@ -427,6 +428,7 @@ class UsersControllerTest < Redmine::ControllerTest
assert_response :success
assert_select 'h2>a+img.gravatar'
assert_select 'input[name=?][value=?]', 'user[login]', 'jsmith'
assert_select 'label[for=?]>span.required', 'user_password', 0
end
def test_edit_registered_user
@ -708,6 +710,19 @@ class UsersControllerTest < Redmine::ControllerTest
assert_response 404
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
assert_difference 'User.count', -1 do
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]'
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
Role.anonymous.remove_permission! :view_wiki_edits
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 'td.name', :text => 'New'
# 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_select "input[name='transitions[1][1][always]'][checked=?][disabled=?]", 'checked', 'disabled', 1
end

View file

@ -456,6 +456,7 @@ class ApplicationHelperTest < Redmine::HelperTest
'user:JSMITH' => 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)),
'@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
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',
:old_value => 1, :value => 2)
assert_match 'eCookbook', show_detail(detail, true)
assert_match 'OnlineStore', show_detail(detail, true)
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
detail = JournalDetail.new(:property => 'attr', :prop_key => 'status_id',
:old_value => 1, :value => 2)
@ -352,4 +360,26 @@ class IssuesHelperTest < Redmine::HelperTest
assert_equal '06/06/2019', issue_due_date_details(issue)
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

View file

@ -48,4 +48,30 @@ class JournalsHelperTest < Redmine::HelperTest
assert_kind_of Attachment, thumbnails.first
assert_equal 'image.png', thumbnails.first.filename
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

View file

@ -229,4 +229,51 @@ class Redmine::ApiTest::AttachmentsTest < Redmine::ApiTest::Base
assert attachment.digest.present?
assert File.exist? attachment.diskfile
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

View file

@ -653,6 +653,34 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base
assert_response 422
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
assert_difference('Journal.count') do
put(

View file

@ -119,6 +119,17 @@ class Redmine::ApiTest::WikiPagesTest < Redmine::ApiTest::Base
assert_equal 'jsmith', page.content.author.login
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
assert_no_difference 'WikiPage.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
# 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: /issues\?sort=\r?$})
assert @response.body.match(%r{^Disallow: /issues\?\*set_filter=\r?$})
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
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
with_settings :attachment_extensions_allowed => "txt, Png" do
assert Attachment.valid_extension?(".pnG")
@ -235,6 +248,23 @@ class AttachmentTest < ActiveSupport::TestCase
assert_not_equal a1.diskfile, a2.diskfile
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
a = Attachment.new(:file => mock_file(:original_filename => "path/to/the/file"))
assert_equal 'file', a.filename

View file

@ -241,6 +241,16 @@ class IssueSubtaskingTest < ActiveSupport::TestCase
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
with_settings :parent_issue_done_ratio => 'derived' do
first_parent = Issue.generate!

View file

@ -898,6 +898,23 @@ class IssueTest < ActiveSupport::TestCase
assert_equal Date.parse('2012-07-14'), issue.due_date
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
source = Tracker.find(1)
source.core_fields = []
@ -1459,6 +1476,23 @@ class IssueTest < ActiveSupport::TestCase
assert_equal [3, nil], copy.children.map(&:assigned_to_id)
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
issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
:subject => 'Test', :author_id => 1)

View file

@ -20,6 +20,7 @@
require File.expand_path('../../../../test_helper', __FILE__)
class Redmine::CipheringTest < ActiveSupport::TestCase
fixtures :auth_sources
def test_password_should_be_encrypted
Redmine::Configuration.with 'database_cipher_key' => 'secret' do
@ -106,4 +107,12 @@ class Redmine::CipheringTest < ActiveSupport::TestCase
assert_equal 'bar', r.read_attribute(:password)
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

View file

@ -44,6 +44,12 @@ class Redmine::Helpers::GanttHelperTest < Redmine::HelperTest
def gantt_start
@gantt.date_from
end
private :gantt_start
def gantt_end
@gantt.date_to
end
private :gantt_end
# Creates a Gantt chart for a 4 week span
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
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
create_gantt
@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)
assert_select "div.starting", 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
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)
assert_select "div.ending", 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
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
def setup
@user = User.find_by_login 'dlopper'
@user = User.find_by_login 'jsmith'
User.current = @user
@ecookbook = Project.find 'ecookbook'
@onlinestore = Project.find 'onlinestore'
end
@ -142,4 +143,16 @@ class Redmine::ProjectJumpBoxTest < ActiveSupport::TestCase
assert_equal @onlinestore, pjb.recently_used_projects.first
assert_equal @ecookbook, pjb.recently_used_projects.last
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

View file

@ -1005,6 +1005,18 @@ class MailHandlerTest < ActiveSupport::TestCase
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
journal_id = Issue.find(2).journals.last.id
Journal.destroy(journal_id)
@ -1056,6 +1068,13 @@ class MailHandlerTest < ActiveSupport::TestCase
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
with_settings :text_formatting => 'textile' do
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
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
# Parent issue with a hierarchy project's fixed version
parent_issue = Issue.find(1)

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