Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Work Package subject using a Type defined blueprint #17516

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions app/models/type.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
Expand Down Expand Up @@ -34,10 +36,12 @@ class Type < ApplicationRecord

include ::Scopes::Scoped

attribute :patterns, Types::PatternCollectionType.new
attribute :patterns, Types::Patterns::CollectionType.new

before_destroy :check_integrity

belongs_to :color, optional: true, class_name: "Color"

has_many :work_packages
has_many :workflows, dependent: :delete_all do
def copy_from_type(source_type)
Expand All @@ -52,21 +56,19 @@ def copy_from_type(source_type)
join_table: "#{table_name_prefix}custom_fields_types#{table_name_suffix}",
association_foreign_key: "custom_field_id"

belongs_to :color, optional: true, class_name: "Color"

acts_as_list

validates :name, presence: true, uniqueness: { case_sensitive: false }, length: { maximum: 255 }

validates :is_default, :is_milestone, inclusion: { in: [true, false] }

scopes :milestone

default_scope { order("position ASC") }

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

def to_s; name end
delegate :to_s, to: :name

def <=>(other)
name <=> other.name
Expand All @@ -81,36 +83,45 @@ def self.statuses(types)
end

def self.standard_type
::Type.where(is_standard: true).first
end

def self.default
::Type.where(is_default: true)
where(is_standard: true).first
end

def self.enabled_in(project)
::Type.includes(:projects).where(projects: { id: project })
includes(:projects).where(projects: { id: project })
end

def statuses(include_default: false)
if new_record?
Status.none
elsif include_default
::Type
.statuses([id])
.or(Status.where_default)
self.class.statuses([id]).or(Status.where_default)
else
::Type.statuses([id])
self.class.statuses([id])
end
end

def enabled_in?(object)
object.types.include?(self)
end

def replacement_patterns_defined?
return false if patterns.blank?
brunopagno marked this conversation as resolved.
Show resolved Hide resolved

patterns.all_enabled.any?
end

def enabled_patterns
return {} if patterns.blank?

patterns.all_enabled
end

private

def check_integrity
raise "Can't delete type" if WorkPackage.where(type_id: id).any?
throw :abort if is_standard?
throw :abort if WorkPackage.exists?(type_id: id)

true
end
end
7 changes: 4 additions & 3 deletions app/models/types/pattern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@

module Types
Pattern = Data.define(:blueprint, :enabled) do
def call(object)
# calculate string using object
blueprint.to_s + object.to_s
def enabled? = !!enabled

def resolve(work_package)
PatternResolver.new(blueprint).resolve(work_package)
end

def to_h
Expand Down
71 changes: 71 additions & 0 deletions app/models/types/pattern_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Types
class PatternResolver
TOKEN_REGEX = /{{[0-9A-Za-z_]+}}/
private_constant :TOKEN_REGEX

def initialize(pattern)
@mapper = Patterns::TokenPropertyMapper.new
@pattern = pattern
@tokens = pattern.scan(TOKEN_REGEX).map { |token| Patterns::Token.build(token) }
end

def resolve(work_package)
@tokens.inject(@pattern) do |pattern, token|
pattern.gsub(token.pattern, get_value(work_package, token))
end
end

private

def get_value(work_package, token)
context = if token.context == :work_package
work_package
else
work_package.public_send(token.context)
end

stringify(@mapper[token.key].call(context))
end

def stringify(value)
case value
when Date, Time, DateTime
value.strftime("%Y-%m-%d")
when NilClass
"N/A"
else
value.to_s
end
end
Comment on lines +60 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having ideas here, but we might want to build something clever to define default values for types alongside this structure here, so we only need to parse all types in one place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... this is very naive right now on purpose.

end
end
59 changes: 59 additions & 0 deletions app/models/types/patterns/collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Types
module Patterns
Collection = Data.define(:patterns) do
private_class_method :new

def self.build(patterns:, contract: CollectionContract.new)
contract.call(patterns).to_monad.fmap { |success| new(success.to_h) }
end

def initialize(patterns:)
transformed = patterns.transform_values { Pattern.new(**_1) }.freeze

super(patterns: transformed)
end

def all_enabled
patterns.select { |_, pattern| pattern.enabled? }
end

def [](value)
patterns.fetch(value)
end

def to_h
patterns.stringify_keys.transform_values(&:to_h)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
#++

module Types
class PatternCollectionContract < Dry::Validation::Contract
params do
required(:subject).hash do
required(:blueprint).filled(:string)
required(:enabled).filled(:bool)
module Patterns
class CollectionContract < Dry::Validation::Contract
params do
required(:subject).hash do
required(:blueprint).filled(:string)
required(:enabled).filled(:bool)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,28 @@
#++

module Types
PatternCollection = Data.define(:patterns) do
private_class_method :new
module Patterns
class CollectionType < ActiveModel::Type::Value
def assert_valid_value(value)
cast(value)
end

def self.build(patterns:, contract: PatternCollectionContract.new)
contract.call(patterns).to_monad.fmap { |success| new(success.to_h) }
end
def cast(value)
Collection.build(patterns: value).value_or { nil }
end

def initialize(patterns:)
transformed = patterns.transform_values { Pattern.new(**_1) }.freeze
def serialize(pattern)
return super if pattern.nil?

super(patterns: transformed)
end
YAML.dump(pattern.to_h)
end

def [](value)
patterns.fetch(value)
end
def deserialize(value)
return if value.blank?

def to_h
patterns.stringify_keys.transform_values(&:to_h)
data = YAML.safe_load(value)
cast(data)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,24 @@
#++

module Types
class PatternCollectionType < ActiveModel::Type::Value
def assert_valid_value(value)
cast(value)
end
module Patterns
Token = Data.define(:pattern, :key) do
private_class_method :new

def cast(value)
PatternCollection.build(patterns: value).value_or { nil }
end
def self.build(pattern)
new(pattern, pattern.tr("{}", "").to_sym)
end

def serialize(pattern)
return super if pattern.nil?
def custom_field? = key.to_s.include?("custom_field")

YAML.dump(pattern.to_h)
end
def context
return :work_package unless custom_field?

def deserialize(value)
return if value.blank?
context = key.to_s.gsub(/_?custom_field_\d+/, "")
return :work_package if context.blank?

data = YAML.safe_load(value)
cast(data)
context.to_sym
end
end
end
end
Loading
Loading