Skip to content

Commit 8be66d2

Browse files
Release OpenProject 16.3.2
2 parents 6d98234 + f04b86f commit 8be66d2

File tree

157 files changed

+4681
-2931
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

157 files changed

+4681
-2931
lines changed

app/contracts/projects/base_contract.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,13 @@ def validate_changing_active
148148

149149
validate_and_merge_errors(contract)
150150
end
151+
152+
def all_available_custom_fields
153+
if user.admin?
154+
model.all_available_custom_fields
155+
else
156+
model.all_available_custom_fields.where(admin_only: false)
157+
end
158+
end
151159
end
152160
end

app/contracts/projects/update_contract.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ module Projects
3232
class UpdateContract < BaseContract
3333
def writable_attributes
3434
if allow_project_attributes_only
35-
with_custom_fields_only(super)
35+
with_available_custom_fields_only(super)
3636
elsif allow_edit_attributes_only
3737
without_custom_fields(super)
3838
elsif allow_all_attributes
39-
super
39+
# When all attributes are updated (API-only case), allow writing to all available custom
40+
# fields (including disabled ones) to maintain backward compatibility with the API.
41+
with_all_available_custom_fields_only(super)
4042
else
4143
[]
4244
end
@@ -63,7 +65,13 @@ def allow_all_attributes
6365

6466
def without_custom_fields(changes) = changes.grep_v(/^custom_field_/)
6567

66-
def with_custom_fields_only(changes) = changes.grep(/^custom_field_/)
68+
def with_available_custom_fields_only(changes) = changes & available_custom_fields.map(&:attribute_name)
69+
70+
def with_all_available_custom_fields_only(changes)
71+
allowed_attributes = changes.grep_v(/^custom_field_/)
72+
allowed_attributes += changes & all_available_custom_fields.map(&:attribute_name)
73+
allowed_attributes
74+
end
6775

6876
def manage_permission
6977
if changed_by_user == ["active"]

app/contracts/work_packages/base_contract.rb

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module WorkPackages
3232
class BaseContract < ::ModelContract
3333
include ::Attachments::ValidateReplacements
3434
include AssignableValuesContract
35+
include WorkPackages::SetAttributesService::ProgressValuesCalculations
3536

3637
attribute :subject
3738
attribute :description
@@ -397,9 +398,7 @@ def validate_percent_complete_is_set_when_work_and_remaining_work_are_set
397398
end
398399

399400
def validate_percent_complete_matches_work_and_remaining_work
400-
return if percent_complete_derivation_unapplicable?
401-
402-
if !percent_complete_range_derived_from_work_and_remaining_work.cover?(percent_complete)
401+
if correctable_percent_complete_value?(work:, remaining_work:, percent_complete:)
403402
errors.add(:done_ratio, :does_not_match_work_and_remaining_work)
404403
end
405404
end
@@ -475,21 +474,6 @@ def percent_complete_empty?
475474
percent_complete.nil?
476475
end
477476

478-
def percent_complete_derivation_unapplicable?
479-
WorkPackage.status_based_mode? || # only applicable in work-based mode
480-
work_empty? || remaining_work_empty? || percent_complete_empty? || # only applicable if all 3 values are set
481-
work == 0 || percent_complete == 100 # only applicable if not in special cases leading to divisions by zero
482-
end
483-
484-
def percent_complete_range_derived_from_work_and_remaining_work
485-
work_done = work - remaining_work
486-
percentage = (100 * work_done.to_f / work)
487-
488-
lower_bound = percentage.truncate
489-
upper_bound = lower_bound + 1
490-
lower_bound..upper_bound
491-
end
492-
493477
def validate_no_reopen_on_closed_version
494478
if model.version_id && model.reopened? && model.version.closed?
495479
errors.add :base, I18n.t(:error_can_not_reopen_work_package_on_closed_version)

app/forms/work_packages/activities_tab/journals/notes_form.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class NotesForm < ApplicationForm
4040
rich_text_options: {
4141
showAttachments: false,
4242
resource:,
43+
storageKey: "work_package-#{object.journable.id}-notes-#{object.id || 'new'}",
4344
editor_type: "constrained"
4445
}
4546
)

app/helpers/colors_helper.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@ def hl_background_class(name, model)
122122
end
123123

124124
def icon_for_color(color, options = {})
125-
return unless color
126-
return if color.hexcode.blank?
125+
return unless color&.valid_attribute?(:hexcode)
127126

128127
style = join_style_arguments(
129128
"background-color: #{color.hexcode}",

app/models/application_record.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ def just_created?
4343
##
4444
# Returns whether the given attribute is free of errors
4545
def valid_attribute?(attribute)
46-
valid? # Ensure validations have run
46+
errors.clear
47+
48+
# run validations for specified attribute only.
49+
self.class.validators_on(attribute).each do |validator|
50+
validator.validate_each(self, attribute, public_send(attribute))
51+
end
4752

4853
errors[attribute].empty?
4954
end

app/models/color.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@ class Color < ApplicationRecord
3737
class_name: "Type",
3838
dependent: :nullify
3939

40-
after_initialize :normalize_hexcode
41-
before_validation :normalize_hexcode
42-
4340
validates :name, :hexcode, presence: true
41+
validates :name, length: { maximum: 255 }
42+
validates :hexcode, format: { with: RGB_HEX_FORMAT, message: :hexcode_invalid, allow_blank: true }
4443

45-
validates :name, length: { maximum: 255, unless: lambda { |e| e.name.blank? } }
46-
validates :hexcode, format: { with: /\A#[0-9A-F]{6}\z/, unless: lambda { |e| e.hexcode.blank? } }
44+
normalizes :hexcode, with: ::Colors::HexColor::Normalizer
4745
end

app/models/colors/hex_color.rb

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030

3131
module Colors
3232
module HexColor
33+
RGB_HEX_FORMAT = /\A#[0-9A-F]{6}\z/
34+
private_constant :RGB_HEX_FORMAT
35+
3336
##
3437
# Get the fill style for this color.
3538
# If the color is light, use a dark font.
@@ -76,11 +79,13 @@ def brightness_yiq
7679
end
7780

7881
##
79-
# Splits the hexcode into rbg color array
82+
# Splits the hexcode into rgb color array
8083
def rgb_colors
8184
hexcode
82-
.delete("#") # Remove trailing #
85+
.delete_prefix("#") # Remove leading #
86+
.ljust(6, "0") # Pad to at least 6 chars
8387
.scan(/../) # Pair hex chars
88+
.first(3)
8489
.map(&:hex) # to int
8590
end
8691

@@ -111,20 +116,22 @@ def blend(mix_value, opacity)
111116
"#%<r>02x%<g>02x%<b>02x" % { r:, g:, b: }
112117
end
113118

114-
# rubocop:disable Metrics/AbcSize
115-
def normalize_hexcode
116-
return unless hexcode.present? && hexcode_changed?
117-
118-
self.hexcode = hexcode.strip.upcase
119-
120-
unless hexcode.starts_with? "#"
121-
self.hexcode = "##{hexcode}"
119+
class Normalizer
120+
def self.call(...)
121+
new.call(...)
122122
end
123123

124-
if hexcode.size == 4 # =~ /#.../
125-
self.hexcode = hexcode.gsub(/([^#])/, '\1\1')
124+
def call(hex)
125+
hex = hex.strip.delete_prefix("#")
126+
case hex
127+
when /\A[0-9a-fA-F]{3}\z/ # short form: #abc
128+
"##{hex.chars.map { |c| c * 2 }.join.upcase}"
129+
when /\A[0-9a-fA-F]{6}\z/ # long form: #aabbcc
130+
"##{hex.upcase}"
131+
else # do nothing
132+
hex
133+
end
126134
end
127135
end
128-
# rubocop:enable Metrics/AbcSize
129136
end
130137
end

app/models/design_color.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
class DesignColor < ApplicationRecord
3232
include ::Colors::HexColor
3333

34-
before_validation :normalize_hexcode
3534
after_commit -> do
3635
# CustomStyle.current.updated_at determines the cache key for inline_css
3736
# in which the CSS color variables will be overwritten. That is why we need
@@ -44,9 +43,11 @@ class DesignColor < ApplicationRecord
4443
end
4544
end
4645

46+
validates :variable, :hexcode, presence: true
4747
validates :variable, uniqueness: true
48-
validates :hexcode, :variable, presence: true
49-
validates :hexcode, format: { with: /\A#[0-9A-F]{6}\z/, unless: lambda { |e| e.hexcode.blank? } }
48+
validates :hexcode, format: { with: RGB_HEX_FORMAT, message: :hexcode_invalid, allow_blank: true }
49+
50+
normalizes :hexcode, with: ::Colors::HexColor::Normalizer
5051

5152
class << self
5253
def setables

app/services/work_packages/activities_tab/comment_attachments_claims/set_attributes_service.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ class SetAttributesService < ::BaseServices::SetAttributes
3737
ATTACHMENT_CSS_SELECTOR = "img.op-uc-image"
3838

3939
def perform
40-
self.params = params.reverse_merge(attachment_ids: collect_attachment_ids_from_notes)
40+
ids_from_notes = collect_attachment_ids_from_notes
41+
claimable_ids = filter_claimable_attachment_ids(ids_from_notes)
42+
43+
self.params = params.reverse_merge(attachment_ids: claimable_ids)
4144
super
4245
end
4346

@@ -57,6 +60,20 @@ def collect_attachment_ids_from_notes
5760
end
5861
end
5962

63+
def filter_claimable_attachment_ids(ids)
64+
return [] if ids.blank?
65+
66+
# Only claim attachments that are actually claimable. We must not try to
67+
# reassign attachments that are already attached to another container
68+
# (e.g., the work package, or another comment), and we must only claim unattached files of
69+
# the current user to satisfy validation rules.
70+
Attachment
71+
.where(container: nil)
72+
.or(Attachment.where(container: model))
73+
.where(id: ids, author: User.current)
74+
.pluck(:id)
75+
end
76+
6077
def parser
6178
@parser ||= Nokogiri::HTML.fragment(model.notes)
6279
end

0 commit comments

Comments
 (0)