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 3 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)
PatternMapper.new(self).resolve(work_package)
end

def to_h
Expand Down
90 changes: 90 additions & 0 deletions app/models/types/pattern_mapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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 PatternMapper
TOKEN_REGEX = /{{[0-9A-z_]+}}/
Fixed Show fixed Hide fixed

MAPPING = {
type: ->(wp) { wp.type.name },
assignee: ->(wp) { wp.assigned_to&.name },
created: ->(wp) { wp.created_at },
author: ->(wp) { wp.author.name },
parent_id: ->(wp) { wp.parent&.id },
project_name: ->(wp) { wp.project.name }
}.freeze

private_constant :MAPPING

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

def valid?(work_package)
@tokens.each { |token| get_value(work_package, token) }
rescue NoMethodError
false
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)
raw_value = if token.custom_field? && token.context != work_package.context
fn(key).call(work_package.public_send(token.context))
else
fn(token.key).call(work_package)
end

stringify(raw_value)
end

def fn(key)
MAPPING.fetch(key) { ->(wp) { wp.public_send(key.to_sym) } }
end

def stringify(value)
case value
when Date, Time, DateTime
value.strftime("%Y-%m-%d")
when NilClass
"PITY DA FOOL!"
else
value.to_s
end
end
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,26 +29,28 @@
#++

module Types
class PatternCollectionType < ActiveModel::Type::Value
def assert_valid_value(value)
cast(value)
end
module Patterns
class CollectionType < ActiveModel::Type::Value
def assert_valid_value(value)
cast(value)
end

def cast(value)
PatternCollection.build(patterns: value).value_or { nil }
end
def cast(value)
Collection.build(patterns: value).value_or { nil }
end

def serialize(pattern)
return super if pattern.nil?
def serialize(pattern)
return super if pattern.nil?

YAML.dump(pattern.to_h)
end
YAML.dump(pattern.to_h)
end

def deserialize(value)
return if value.blank?
def deserialize(value)
return if value.blank?

data = YAML.safe_load(value)
cast(data)
data = YAML.safe_load(value)
cast(data)
end
end
end
end
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
Token = Data.define(:pattern, :key) do
private_class_method :new

def self.build(patterns:, contract: PatternCollectionContract.new)
contract.call(patterns).to_monad.fmap { |success| new(success.to_h) }
end
def self.build(pattern)
new(pattern, pattern.tr("{}", "").to_sym)
end

def initialize(patterns:)
transformed = patterns.transform_values { Pattern.new(**_1) }.freeze
def custom_field? = key.include?("custom_field")

super(patterns: transformed)
end
def custom_field_id
return nil unless custom_field?

def [](value)
patterns.fetch(value)
end
Integer(key.to_s.gsub(/\D+/, ""))
end

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

def to_h
patterns.stringify_keys.transform_values(&:to_h)
context.to_sym
end
end
end
end
Loading
Loading