Redmine 4.1.1

This commit is contained in:
Manuel Cillero 2020-11-22 21:20:06 +01:00
parent 33e7b881a5
commit 3d976f1b3b
1593 changed files with 36180 additions and 19489 deletions

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -54,8 +56,7 @@ class Issue < ActiveRecord::Base
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
attr_accessor :deleted_attachment_ids
attr_reader :current_journal
attr_writer :deleted_attachment_ids
delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
validates_presence_of :subject, :project, :tracker
@ -69,7 +70,6 @@ class Issue < ActiveRecord::Base
validates :start_date, :date => true
validates :due_date, :date => true
validate :validate_issue, :validate_required_fields, :validate_permissions
attr_protected :id
scope :visible, lambda {|*args|
joins(:project).
@ -77,7 +77,7 @@ class Issue < ActiveRecord::Base
}
scope :open, lambda {|*args|
is_closed = args.size > 0 ? !args.first : false
is_closed = !args.empty? ? !args.first : false
joins(:status).
where(:issue_statuses => {:is_closed => is_closed})
}
@ -108,34 +108,35 @@ class Issue < ActiveRecord::Base
before_validation :default_assign, on: :create
before_validation :clear_disabled_fields
before_save :close_duplicates, :update_done_ratio_from_issue_status,
:force_updated_on_change, :update_closed_on, :set_assigned_to_was
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
:force_updated_on_change, :update_closed_on
after_save {|issue| issue.send :after_project_change if !issue.saved_change_to_id? && issue.saved_change_to_project_id?}
after_save :reschedule_following_issues, :update_nested_set_attributes,
:update_parent_attributes, :delete_selected_attachments, :create_journal
# Should be after_create but would be called before previous after_save callbacks
after_save :after_create_from_copy
after_destroy :update_parent_attributes
after_create :send_notification
after_create_commit :send_notification
# Returns a SQL conditions string used to find all issues visible by the specified user
def self.visible_condition(user, options={})
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
sql = if user.id && user.logged?
case role.issues_visibility
when 'all'
'1=1'
when 'default'
user_ids = [user.id] + user.groups.map(&:id).compact
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
when 'own'
user_ids = [user.id] + user.groups.map(&:id).compact
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
sql =
if user.id && user.logged?
case role.issues_visibility
when 'all'
'1=1'
when 'default'
user_ids = [user.id] + user.groups.pluck(:id).compact
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
when 'own'
user_ids = [user.id] + user.groups.pluck(:id).compact
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
else
'1=0'
end
else
'1=0'
"(#{table_name}.is_private = #{connection.quoted_false})"
end
else
"(#{table_name}.is_private = #{connection.quoted_false})"
end
unless role.permissions_all_trackers?(:view_issues)
tracker_ids = role.permissions_tracker_ids(:view_issues)
if tracker_ids.any?
@ -151,20 +152,21 @@ class Issue < ActiveRecord::Base
# Returns true if usr or current user is allowed to view the issue
def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
visible = if user.logged?
case role.issues_visibility
when 'all'
true
when 'default'
!self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
when 'own'
self.author == user || user.is_or_belongs_to?(assigned_to)
visible =
if user.logged?
case role.issues_visibility
when 'all'
true
when 'default'
!self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
when 'own'
self.author == user || user.is_or_belongs_to?(assigned_to)
else
false
end
else
false
!self.is_private?
end
else
!self.is_private?
end
unless role.permissions_all_trackers?(:view_issues)
visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
end
@ -179,7 +181,9 @@ class Issue < ActiveRecord::Base
# Returns true if user or current user is allowed to edit the issue
def attributes_editable?(user=User.current)
user_tracker_permission?(user, :edit_issues)
user_tracker_permission?(user, :edit_issues) || (
user_tracker_permission?(user, :edit_own_issues) && author == user
)
end
# Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
@ -206,7 +210,7 @@ class Issue < ActiveRecord::Base
end
end
def create_or_update
def create_or_update(*args)
super
ensure
@status_was = nil
@ -260,6 +264,11 @@ class Issue < ActiveRecord::Base
end
end
# Overrides Redmine::Acts::Customizable::InstanceMethods#set_custom_field_default?
def set_custom_field_default?(custom_value)
new_record? || project_id_changed?|| tracker_id_changed?
end
# Copies attributes from another issue, arg can be an id or an Issue
def copy_from(arg, options={})
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
@ -445,7 +454,8 @@ class Issue < ActiveRecord::Base
write_attribute :estimated_hours, (h.is_a?(String) ? (h.to_hours || h) : h)
end
safe_attributes 'project_id',
safe_attributes(
'project_id',
'tracker_id',
'status_id',
'category_id',
@ -462,29 +472,31 @@ class Issue < ActiveRecord::Base
'custom_fields',
'lock_version',
'notes',
:if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
safe_attributes 'notes',
:if => lambda {|issue, user| issue.notes_addable?(user)}
safe_attributes 'private_notes',
:if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
safe_attributes 'watcher_user_ids',
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
safe_attributes 'is_private',
:if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user)})
safe_attributes(
'notes',
:if => lambda {|issue, user| issue.notes_addable?(user)})
safe_attributes(
'private_notes',
:if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)})
safe_attributes(
'watcher_user_ids',
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)})
safe_attributes(
'is_private',
:if => lambda {|issue, user|
user.allowed_to?(:set_issues_private, issue.project) ||
(issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
}
safe_attributes 'parent_issue_id',
:if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
user.allowed_to?(:manage_subtasks, issue.project)}
safe_attributes 'deleted_attachment_ids',
:if => lambda {|issue, user| issue.attachments_deletable?(user)}
})
safe_attributes(
'parent_issue_id',
:if => lambda {|issue, user|
(issue.new_record? || issue.attributes_editable?(user)) &&
user.allowed_to?(:manage_subtasks, issue.project)
})
safe_attributes(
'deleted_attachment_ids',
:if => lambda {|issue, user| issue.attachments_deletable?(user)})
def safe_attribute_names(user=nil)
names = super
@ -511,6 +523,10 @@ class Issue < ActiveRecord::Base
# attr_accessible is too rough because we still want things like
# Issue.new(:project => foo) to work
def safe_attributes=(attrs, user=User.current)
if attrs.respond_to?(:to_unsafe_hash)
attrs = attrs.to_unsafe_hash
end
@attributes_set_by = user
return unless attrs.is_a?(Hash)
@ -518,7 +534,7 @@ class Issue < ActiveRecord::Base
# Project and Tracker must be set before since new_statuses_allowed_to depends on it.
if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
if p.is_a?(String) && !p.match(/^\d*$/)
if p.is_a?(String) && !/^\d*$/.match?(p)
p_id = Project.find_by_identifier(p).try(:id)
else
p_id = p.to_i
@ -527,7 +543,7 @@ class Issue < ActiveRecord::Base
self.project_id = p_id
end
if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
if project_id_changed? && attrs['category_id'].present? && attrs['category_id'].to_s == category_id_was.to_s
# Discard submitted category on previous project
attrs.delete('category_id')
end
@ -563,8 +579,6 @@ class Issue < ActiveRecord::Base
if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
self.assigned_to_id = u
end
attrs = delete_unsafe_attributes(attrs, user)
return if attrs.empty?
@ -585,8 +599,7 @@ class Issue < ActiveRecord::Base
attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
end
# mass-assignment security bypass
assign_attributes attrs, :without_protection => true
assign_attributes attrs
end
def disabled_core_fields
@ -760,7 +773,7 @@ class Issue < ActiveRecord::Base
user = new_record? ? author : current_journal.try(:user)
required_attribute_names(user).each do |attribute|
if attribute =~ /^\d+$/
if /^\d+$/.match?(attribute)
attribute = attribute.to_i
v = custom_field_values.detect {|v| v.custom_field_id == attribute }
if v && Array(v.value).detect(&:present?).nil?
@ -1006,32 +1019,26 @@ class Issue < ActiveRecord::Base
statuses
end
# Returns the previous assignee (user or group) if changed
def assigned_to_was
# assigned_to_id_was is reset before after_save callbacks
user_id = @previous_assigned_to_id || assigned_to_id_was
if user_id && user_id != assigned_to_id
@assigned_to_was ||= Principal.find_by_id(user_id)
end
end
# Returns the original tracker
def tracker_was
Tracker.find_by_id(tracker_id_was)
Tracker.find_by_id(tracker_id_in_database)
end
# Returns the previous assignee whenever we're before the save
# or in after_* callbacks
def previous_assignee
if previous_assigned_to_id = assigned_to_id_change_to_be_saved.nil? ? assigned_to_id_before_last_save : assigned_to_id_in_database
Principal.find_by_id(previous_assigned_to_id)
end
end
# Returns the users that should be notified
def notified_users
notified = []
# Author and assignee are always notified unless they have been
# locked or don't want to be notified
notified << author if author
if assigned_to
notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
end
if assigned_to_was
notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
end
notified = [author, assigned_to, previous_assignee].compact.uniq
notified = notified.map {|n| n.is_a?(Group) ? n.users : n}.flatten
notified.uniq!
notified = notified.select {|u| u.active? && u.notify_about?(self)}
notified += project.notified_users
@ -1046,21 +1053,6 @@ class Issue < ActiveRecord::Base
notified_users.collect(&:mail)
end
def each_notification(users, &block)
if users.any?
if custom_field_values.detect {|value| !value.custom_field.visible?}
users_by_custom_field_visibility = users.group_by do |user|
visible_custom_field_values(user).map(&:custom_field_id).sort
end
users_by_custom_field_visibility.values.each do |users|
yield(users)
end
else
yield(users)
end
end
end
def notify?
@notify != false
end
@ -1076,11 +1068,12 @@ class Issue < ActiveRecord::Base
# Returns the total number of hours spent on this issue and its descendants
def total_spent_hours
@total_spent_hours ||= if leaf?
spent_hours
else
self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
end
@total_spent_hours ||=
if leaf?
spent_hours
else
self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
end
end
def total_estimated_hours
@ -1364,7 +1357,7 @@ class Issue < ActiveRecord::Base
# Returns a string of css classes that apply to the issue
def css_classes(user=User.current)
s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
s = +"issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
s << ' closed' if closed?
s << ' overdue' if overdue?
s << ' child' if child?
@ -1387,7 +1380,7 @@ class Issue < ActiveRecord::Base
# Unassigns issues from versions that are no longer shared
# after +project+ was moved
def self.update_versions_from_hierarchy_change(project)
moved_project_ids = project.self_and_descendants.reload.collect(&:id)
moved_project_ids = project.self_and_descendants.reload.pluck(:id)
# Update issues of the moved projects and issues assigned to a version of a moved project
Issue.update_versions(
["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
@ -1463,28 +1456,28 @@ class Issue < ActiveRecord::Base
end
end
def self.by_tracker(project)
count_and_group_by(:project => project, :association => :tracker)
def self.by_tracker(project, with_subprojects=false)
count_and_group_by(:project => project, :association => :tracker, :with_subprojects => with_subprojects)
end
def self.by_version(project)
count_and_group_by(:project => project, :association => :fixed_version)
def self.by_version(project, with_subprojects=false)
count_and_group_by(:project => project, :association => :fixed_version, :with_subprojects => with_subprojects)
end
def self.by_priority(project)
count_and_group_by(:project => project, :association => :priority)
def self.by_priority(project, with_subprojects=false)
count_and_group_by(:project => project, :association => :priority, :with_subprojects => with_subprojects)
end
def self.by_category(project)
count_and_group_by(:project => project, :association => :category)
def self.by_category(project, with_subprojects=false)
count_and_group_by(:project => project, :association => :category, :with_subprojects => with_subprojects)
end
def self.by_assigned_to(project)
count_and_group_by(:project => project, :association => :assigned_to)
def self.by_assigned_to(project, with_subprojects=false)
count_and_group_by(:project => project, :association => :assigned_to, :with_subprojects => with_subprojects)
end
def self.by_author(project)
count_and_group_by(:project => project, :association => :author)
def self.by_author(project, with_subprojects=false)
count_and_group_by(:project => project, :association => :author, :with_subprojects => with_subprojects)
end
def self.by_subproject(project)
@ -1521,8 +1514,15 @@ class Issue < ActiveRecord::Base
end
# Returns a scope of projects that user can assign the issue to
def allowed_target_projects(user=User.current)
current_project = new_record? ? nil : project
def allowed_target_projects(user=User.current, context=nil)
if new_record? && context.is_a?(Project) && !copy?
current_project = context.self_and_descendants
elsif new_record?
current_project = nil
else
current_project = project
end
self.class.allowed_target_projects(user, current_project)
end
@ -1530,8 +1530,10 @@ class Issue < ActiveRecord::Base
# If current_project is given, it will be included in the scope
def self.allowed_target_projects(user=User.current, current_project=nil)
condition = Project.allowed_to_condition(user, :add_issues)
if current_project
if current_project.is_a?(Project)
condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
elsif current_project
condition = ["(#{condition}) AND #{Project.table_name}.id IN (?)", current_project.map(&:id)]
end
Project.where(condition).having_trackers
end
@ -1589,7 +1591,7 @@ class Issue < ActiveRecord::Base
# Move subtasks that were in the same project
children.each do |child|
next unless child.project_id == project_id_was
next unless child.project_id == project_id_before_last_save
# Change project and keep project
child.send :project=, project, true
unless child.save
@ -1648,7 +1650,7 @@ class Issue < ActiveRecord::Base
end
def update_nested_set_attributes
if parent_id_changed?
if saved_change_to_parent_id?
update_nested_set_attributes_on_parent_change
end
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
@ -1656,7 +1658,7 @@ class Issue < ActiveRecord::Base
# Updates the nested set for when an existing issue is moved
def update_nested_set_attributes_on_parent_change
former_parent_id = parent_id_was
former_parent_id = parent_id_before_last_save
# delete invalid relations of all descendants
self_and_descendants.each do |issue|
issue.relations.each do |relation|
@ -1713,7 +1715,7 @@ class Issue < ActiveRecord::Base
estimated * ratio
}.sum
progress = done / (average * children.count)
p.done_ratio = progress.round
p.done_ratio = progress.floor
end
end
end
@ -1723,21 +1725,24 @@ class Issue < ActiveRecord::Base
end
end
# Update issues so their versions are not pointing to a
# fixed_version that is not shared with the issue's project
def self.update_versions(conditions=nil)
# Only need to update issues with a fixed_version from
# 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'").
where(conditions).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version)
issue.init_journal(User.current)
issue.fixed_version = nil
issue.save
# Singleton class method is public
class << self
# Update issues so their versions are not pointing to a
# fixed_version that is not shared with the issue's project
def update_versions(conditions=nil)
# Only need to update issues with a fixed_version from
# 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'").
where(conditions).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version)
issue.init_journal(User.current)
issue.fixed_version = nil
issue.save
end
end
end
end
@ -1793,7 +1798,7 @@ class Issue < ActiveRecord::Base
# Updates start/due dates of following issues
def reschedule_following_issues
if start_date_changed? || due_date_changed?
if saved_change_to_start_date? || saved_change_to_due_date?
relations_from.each do |relation|
relation.set_issue_to_dates(@current_journal)
end
@ -1802,7 +1807,7 @@ class Issue < ActiveRecord::Base
# Closes duplicates if the issue is being closed
def close_duplicates
if closing?
if Setting.close_duplicate_issues? && closing?
duplicates.each do |duplicate|
# Reload is needed in case the duplicate was updated by a previous duplicate
duplicate.reload
@ -1852,18 +1857,6 @@ class Issue < ActiveRecord::Base
end
end
# Stores the previous assignee so we can still have access
# to it during after_save callbacks (assigned_to_id_was is reset)
def set_assigned_to_was
@previous_assigned_to_id = assigned_to_id_was
end
# Clears the previous assignee at the end of after_save callbacks
def clear_assigned_to_was
@assigned_to_was = nil
@previous_assigned_to_id = nil
end
def clear_disabled_fields
if tracker
tracker.disabled_core_fields.each do |attribute|