Skip to content

Commit 3da006e

Browse files
authored
Merge pull request opf#17516 from opf/impl/generate-subjects-on-wp
Update Work Package subject using a Type defined blueprint
2 parents 70c5bba + 426dbea commit 3da006e

18 files changed

+579
-114
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ gem "turbo-rails", "~> 2.0.0"
237237

238238
gem "httpx"
239239

240+
# Brings actual deep freezing to most ruby objects
241+
gem "ice_nine"
242+
240243
group :test do
241244
gem "launchy", "~> 3.0.0"
242245
gem "rack-test", "~> 2.1.0"

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,7 @@ DEPENDENCIES
12731273
i18n-js (~> 4.2.3)
12741274
i18n-tasks (~> 1.0.13)
12751275
ice_cube (~> 0.17.0)
1276+
ice_nine
12761277
json_schemer (~> 2.3.0)
12771278
json_spec (~> 1.1.4)
12781279
ladle

app/models/type.rb

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
#-- copyright
24
# OpenProject is an open source project management software.
35
# Copyright (C) the OpenProject GmbH
@@ -34,10 +36,12 @@ class Type < ApplicationRecord
3436

3537
include ::Scopes::Scoped
3638

37-
attribute :patterns, Types::PatternCollectionType.new
39+
attribute :patterns, Types::Patterns::CollectionType.new
3840

3941
before_destroy :check_integrity
4042

43+
belongs_to :color, optional: true, class_name: "Color"
44+
4145
has_many :work_packages
4246
has_many :workflows, dependent: :delete_all do
4347
def copy_from_type(source_type)
@@ -52,21 +56,19 @@ def copy_from_type(source_type)
5256
join_table: "#{table_name_prefix}custom_fields_types#{table_name_suffix}",
5357
association_foreign_key: "custom_field_id"
5458

55-
belongs_to :color, optional: true, class_name: "Color"
56-
5759
acts_as_list
5860

5961
validates :name, presence: true, uniqueness: { case_sensitive: false }, length: { maximum: 255 }
60-
6162
validates :is_default, :is_milestone, inclusion: { in: [true, false] }
6263

6364
scopes :milestone
6465

6566
default_scope { order("position ASC") }
6667

6768
scope :without_standard, -> { where(is_standard: false).order(:position) }
69+
scope :default, -> { where(is_default: true) }
6870

69-
def to_s; name end
71+
delegate :to_s, to: :name
7072

7173
def <=>(other)
7274
name <=> other.name
@@ -81,36 +83,45 @@ def self.statuses(types)
8183
end
8284

8385
def self.standard_type
84-
::Type.where(is_standard: true).first
85-
end
86-
87-
def self.default
88-
::Type.where(is_default: true)
86+
where(is_standard: true).first
8987
end
9088

9189
def self.enabled_in(project)
92-
::Type.includes(:projects).where(projects: { id: project })
90+
includes(:projects).where(projects: { id: project })
9391
end
9492

9593
def statuses(include_default: false)
9694
if new_record?
9795
Status.none
9896
elsif include_default
99-
::Type
100-
.statuses([id])
101-
.or(Status.where_default)
97+
self.class.statuses([id]).or(Status.where_default)
10298
else
103-
::Type.statuses([id])
99+
self.class.statuses([id])
104100
end
105101
end
106102

107103
def enabled_in?(object)
108104
object.types.include?(self)
109105
end
110106

107+
def replacement_patterns_defined?
108+
return false if patterns.blank?
109+
110+
enabled_patterns.any?
111+
end
112+
113+
def enabled_patterns
114+
return {} if patterns.blank?
115+
116+
patterns.all_enabled
117+
end
118+
111119
private
112120

113121
def check_integrity
114-
raise "Can't delete type" if WorkPackage.where(type_id: id).any?
122+
throw :abort if is_standard?
123+
throw :abort if WorkPackage.exists?(type_id: id)
124+
125+
true
115126
end
116127
end

app/models/types/pattern.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030

3131
module Types
3232
Pattern = Data.define(:blueprint, :enabled) do
33-
def call(object)
34-
# calculate string using object
35-
blueprint.to_s + object.to_s
33+
def enabled? = !!enabled
34+
35+
def resolve(work_package)
36+
PatternResolver.new(blueprint).resolve(work_package)
3637
end
3738

3839
def to_h

app/models/types/pattern_resolver.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
module Types
32+
class PatternResolver
33+
TOKEN_REGEX = /{{[0-9A-Za-z_]+}}/
34+
private_constant :TOKEN_REGEX
35+
36+
def initialize(pattern)
37+
@mapper = Patterns::TokenPropertyMapper.new
38+
@pattern = pattern
39+
@tokens = pattern.scan(TOKEN_REGEX).map { |token| Patterns::Token.build(token) }
40+
end
41+
42+
def resolve(work_package)
43+
@tokens.inject(@pattern) do |pattern, token|
44+
pattern.gsub(token.pattern, get_value(work_package, token))
45+
end
46+
end
47+
48+
private
49+
50+
def get_value(work_package, token)
51+
context = token.context == :work_package ? work_package : work_package.public_send(token.context)
52+
53+
stringify(@mapper[token.context_key].call(context))
54+
end
55+
56+
def stringify(value)
57+
case value
58+
when Date, Time, DateTime
59+
value.strftime("%Y-%m-%d")
60+
when NilClass
61+
"NA"
62+
else
63+
value.to_s
64+
end
65+
end
66+
end
67+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
module Types
32+
module Patterns
33+
Collection = Data.define(:patterns) do
34+
private_class_method :new
35+
36+
def self.build(patterns:, contract: CollectionContract.new)
37+
contract.call(patterns).to_monad.fmap { |success| new(success.to_h) }
38+
end
39+
40+
def initialize(patterns:)
41+
transformed = patterns.transform_values { Pattern.new(**_1) }.freeze
42+
43+
super(patterns: transformed)
44+
end
45+
46+
def all_enabled
47+
patterns.select { |_, pattern| pattern.enabled? }
48+
end
49+
50+
def [](value)
51+
patterns.fetch(value)
52+
end
53+
54+
def to_h
55+
patterns.stringify_keys.transform_values(&:to_h)
56+
end
57+
end
58+
end
59+
end

app/models/types/pattern_collection_contract.rb renamed to app/models/types/patterns/collection_contract.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
#++
3030

3131
module Types
32-
class PatternCollectionContract < Dry::Validation::Contract
33-
params do
34-
required(:subject).hash do
35-
required(:blueprint).filled(:string)
36-
required(:enabled).filled(:bool)
32+
module Patterns
33+
class CollectionContract < Dry::Validation::Contract
34+
params do
35+
required(:subject).hash do
36+
required(:blueprint).filled(:string)
37+
required(:enabled).filled(:bool)
38+
end
3739
end
3840
end
3941
end

app/models/types/pattern_collection_type.rb renamed to app/models/types/patterns/collection_type.rb

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,28 @@
2929
#++
3030

3131
module Types
32-
class PatternCollectionType < ActiveModel::Type::Value
33-
def assert_valid_value(value)
34-
cast(value)
35-
end
32+
module Patterns
33+
class CollectionType < ActiveModel::Type::Value
34+
def assert_valid_value(value)
35+
cast(value)
36+
end
3637

37-
def cast(value)
38-
PatternCollection.build(patterns: value).value_or { nil }
39-
end
38+
def cast(value)
39+
Collection.build(patterns: value).value_or { nil }
40+
end
4041

41-
def serialize(pattern)
42-
return super if pattern.nil?
42+
def serialize(pattern)
43+
return super if pattern.nil?
4344

44-
YAML.dump(pattern.to_h)
45-
end
45+
YAML.dump(pattern.to_h)
46+
end
4647

47-
def deserialize(value)
48-
return if value.blank?
48+
def deserialize(value)
49+
return if value.blank?
4950

50-
data = YAML.safe_load(value)
51-
cast(data)
51+
data = YAML.safe_load(value)
52+
cast(data)
53+
end
5254
end
5355
end
5456
end

app/models/types/pattern_collection.rb renamed to app/models/types/patterns/token.rb

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,30 @@
2929
#++
3030

3131
module Types
32-
PatternCollection = Data.define(:patterns) do
33-
private_class_method :new
32+
module Patterns
33+
Token = Data.define(:pattern, :key) do
34+
private_class_method :new
3435

35-
def self.build(patterns:, contract: PatternCollectionContract.new)
36-
contract.call(patterns).to_monad.fmap { |success| new(success.to_h) }
37-
end
36+
def self.build(pattern)
37+
new(pattern, pattern.tr("{}", "").to_sym)
38+
end
3839

39-
def initialize(patterns:)
40-
transformed = patterns.transform_values { Pattern.new(**_1) }.freeze
40+
def custom_field? = key.to_s.include?("custom_field")
4141

42-
super(patterns: transformed)
43-
end
42+
def context_key
43+
return key unless custom_field?
4444

45-
def [](value)
46-
patterns.fetch(value)
47-
end
45+
key.to_s.gsub("#{context}_", "").to_sym
46+
end
47+
48+
def context
49+
return :work_package unless custom_field?
50+
51+
context = key.to_s.gsub(/_?custom_field_\d+/, "")
52+
return :work_package if context.blank?
4853

49-
def to_h
50-
patterns.stringify_keys.transform_values(&:to_h)
54+
context.to_sym
55+
end
5156
end
5257
end
5358
end

0 commit comments

Comments
 (0)