Skip to content

Commit

Permalink
Changes the Update / Create work package contract.
Browse files Browse the repository at this point in the history
  • Loading branch information
mereghost committed Dec 20, 2024
1 parent ed3c5a9 commit 88c037c
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 119 deletions.
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?

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
53 changes: 46 additions & 7 deletions app/models/types/pattern_mapper.rb
Original file line number Diff line number Diff line change
@@ -1,40 +1,77 @@
# 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 = /{{[A-z_]+}}/
TOKEN_REGEX = /{{[0-9A-z_]+}}/

Check warning

Code scanning / CodeQL

Overly permissive regular expression range Medium

Suspicious character range that is equivalent to [A-Z\[\]^_`a-z].

MAPPING = {
assignee: ->(wp) { wp.assigned_to.name },
type: ->(wp) { wp.type.name },
assignee: ->(wp) { wp.assigned_to&.name },
created: ->(wp) { wp.created_at },
author: ->(wp) { wp.author.name }
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.new(token) }
@tokens = pattern.scan(TOKEN_REGEX).map { |token| Patterns::Token.build(token) }
end

def valid?(work_package)
@tokens.each { |token| fn(token.key).call(work_package) }
@tokens.each { |token| get_value(work_package, token) }
rescue NoMethodError
false
end

def resolve(work_package)
@tokens.inject(@pattern) do |pattern, token|
value = fn(token.key).call(work_package)
pattern.gsub(token.pattern, stringify(value))
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
Expand All @@ -43,6 +80,8 @@ def stringify(value)
case value
when Date, Time, DateTime
value.strftime("%Y-%m-%d")
when NilClass
"PITY DA FOOL!"
else
value.to_s
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,31 @@
#++

module Types
PatternCollection = Data.define(:patterns) do
private_class_method :new
module Patterns
Collection = Data.define(:patterns) 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(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
def initialize(patterns:)
transformed = patterns.transform_values { Pattern.new(**_1) }.freeze

super(patterns: transformed)
end
super(patterns: transformed)
end

def [](value)
patterns.fetch(value)
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)
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
48 changes: 46 additions & 2 deletions app/models/types/patterns/token.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
# 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
Token = Data.define(:pattern) do
def key = pattern.tr("{}", "").to_sym
Token = Data.define(:pattern, :key) do
private_class_method :new

def self.build(pattern)
new(pattern, pattern.tr("{}", "").to_sym)
end

def custom_field? = key.include?("custom_field")

def custom_field_id
return nil unless custom_field?

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?

context.to_sym
end
end
end
end
Loading

0 comments on commit 88c037c

Please sign in to comment.