Skip to content

Commit

Permalink
Returns all possible tokens and its translations when available
Browse files Browse the repository at this point in the history
  • Loading branch information
mereghost committed Dec 30, 2024
1 parent 0628672 commit 426dbea
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 45 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ gem "turbo-rails", "~> 2.0.0"

gem "httpx"

# Brings actual deep freezing to most ruby objects
gem "ice_nine"

group :test do
gem "launchy", "~> 3.0.0"
gem "rack-test", "~> 2.1.0"
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,7 @@ DEPENDENCIES
i18n-js (~> 4.2.3)
i18n-tasks (~> 1.0.13)
ice_cube (~> 0.17.0)
ice_nine
json_schemer (~> 2.3.0)
json_spec (~> 1.1.4)
ladle
Expand Down
2 changes: 1 addition & 1 deletion app/models/type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def enabled_in?(object)
def replacement_patterns_defined?
return false if patterns.blank?

patterns.all_enabled.any?
enabled_patterns.any?
end

def enabled_patterns
Expand Down
10 changes: 3 additions & 7 deletions app/models/types/pattern_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,17 @@ def resolve(work_package)
private

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

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

def stringify(value)
case value
when Date, Time, DateTime
value.strftime("%Y-%m-%d")
when NilClass
"N/A"
"NA"
else
value.to_s
end
Expand Down
6 changes: 6 additions & 0 deletions app/models/types/patterns/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def self.build(pattern)

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

def context_key
return key unless custom_field?

key.to_s.gsub("#{context}_", "").to_sym
end

def context
return :work_package unless custom_field?

Expand Down
107 changes: 75 additions & 32 deletions app/models/types/patterns/token_property_mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,44 +31,87 @@
module Types
module Patterns
class TokenPropertyMapper
MAPPING = {
accountable: ->(wp) { wp.responsible&.name },
assignee: ->(wp) { wp.assigned_to&.name },
author: ->(wp) { wp.author&.name },
category: ->(wp) { wp.category&.name },
creation_date: ->(wp) { wp.created_at },
estimated_time: ->(wp) { wp.estimated_hours },
finish_date: ->(wp) { wp.due_date },
parent: ->(wp) { wp.parent&.id },
parent_author: ->(wp) { wp.parent&.author&.name },
parent_category: ->(wp) { wp.parent&.category&.name },
parent_creation_date: ->(wp) { wp.parent&.created_at },
parent_estimated_time: ->(wp) { wp.parent&.estimated_hours },
parent_finish_date: ->(wp) { wp.parent&.due_date },
parent_priority: ->(wp) { wp.parent&.priority },
priority: ->(wp) { wp.priority },
project: ->(wp) { wp.project_id },
project_active: ->(wp) { wp.project&.active? },
project_name: ->(wp) { wp.project&.name },
project_status: ->(wp) { wp.project&.status_code },
project_parent: ->(wp) { wp.project&.parent_id },
project_public: ->(wp) { wp.project&.public? },
start_date: ->(wp) { wp.start_date },
status: ->(wp) { wp.status&.name },
type: ->(wp) { wp.type&.name }
}.freeze
DEFAULT_FUNCTION = ->(key, context) { context.public_send(key.to_sym) }.curry

TOKEN_PROPERTY_MAP = IceNine.deep_freeze(
{
accountable: { fn: ->(wp) { wp.responsible&.name }, label: -> { WorkPackage.human_attribute_name(:responsible) } },
assignee: { fn: ->(wp) { wp.assigned_to&.name }, label: -> { WorkPackage.human_attribute_name(:assigned_to) } },
author: { fn: ->(wp) { wp.author&.name }, label: -> { WorkPackage.human_attribute_name(:author) } },
category: { fn: ->(wp) { wp.category&.name }, label: -> { WorkPackage.human_attribute_name(:category) } },
creation_date: { fn: ->(wp) { wp.created_at }, label: -> { WorkPackage.human_attribute_name(:created_at) } },
estimated_time: { fn: ->(wp) { wp.estimated_hours }, label: -> { WorkPackage.human_attribute_name(:estimated_hours) } },
finish_date: { fn: ->(wp) { wp.due_date }, label: -> { WorkPackage.human_attribute_name(:due_date) } },
parent: { fn: ->(wp) { wp.parent&.id }, label: -> { WorkPackage.human_attribute_name(:parent) } },
parent_author: { fn: ->(wp) { wp.parent&.author&.name }, label: -> { WorkPackage.human_attribute_name(:author) } },
parent_category: { fn: ->(wp) { wp.parent&.category&.name },
label: -> { WorkPackage.human_attribute_name(:category) } },
parent_creation_date: { fn: ->(wp) { wp.parent&.created_at },
label: -> { WorkPackage.human_attribute_name(:created_at) } },
parent_estimated_time: { fn: ->(wp) { wp.parent&.estimated_hours },
label: -> { WorkPackage.human_attribute_name(:estimated_hours) } },
parent_finish_date: { fn: ->(wp) { wp.parent&.due_date },
label: -> { WorkPackage.human_attribute_name(:due_date) } },
parent_priority: { fn: ->(wp) { wp.parent&.priority }, label: -> { WorkPackage.human_attribute_name(:priority) } },
priority: { fn: ->(wp) { wp.priority }, label: -> { WorkPackage.human_attribute_name(:priority) } },
project: { fn: ->(wp) { wp.project_id }, label: -> { WorkPackage.human_attribute_name(:project) } },
project_active: { fn: ->(wp) { wp.project&.active? }, label: -> { Project.human_attribute_name(:active) } },
project_name: { fn: ->(wp) { wp.project&.name }, label: -> { Project.human_attribute_name(:name) } },
project_status: { fn: ->(wp) { wp.project&.status_code }, label: -> { Project.human_attribute_name(:status_code) } },
project_parent: { fn: ->(wp) { wp.project&.parent_id }, label: -> { Project.human_attribute_name(:parent) } },
project_public: { fn: ->(wp) { wp.project&.public? }, label: -> { Project.human_attribute_name(:public) } },
start_date: { fn: ->(wp) { wp.start_date }, label: -> { WorkPackage.human_attribute_name(:start_date) } },
status: { fn: ->(wp) { wp.status&.name }, label: -> { WorkPackage.human_attribute_name(:status) } },
type: { fn: ->(wp) { wp.type&.name }, label: -> { WorkPackage.human_attribute_name(:type) } }
}
)

def fetch(key)
MAPPING.fetch(key) { ->(context) { context.public_send(key.to_sym) } }
TOKEN_PROPERTY_MAP.dig(key, :fn) || DEFAULT_FUNCTION.call(key)
end

alias :[] :fetch

def tokens_for_type(_type)
[]
# Fetch all CustomFields for type
# Fetch all customFields prefixed as parent
# fetch all project attributes prefixed as project
def tokens_for_type(type)
base = default_tokens
base[:work_package].merge!(tokenize(work_package_cfs_for(type)))
base[:project].merge!(tokenize(project_attributes, "project_"))
base[:parent].merge!(tokenize(all_work_package_cfs, "parent_"))

base.transform_values { _1.sort_by(&:last).to_h }
end

private

def default_tokens
TOKEN_PROPERTY_MAP.keys.each_with_object({ project: {}, work_package: {}, parent: {} }) do |key, obj|
label = TOKEN_PROPERTY_MAP.dig(key, :label).call

case key.to_s
when /^project_/
obj[:project][key] = label
when /^parent_/
obj[:parent][key] = label
else
obj[:work_package][key] = label
end
end
end

def tokenize(custom_field_scope, prefix = nil)
custom_field_scope.pluck(:name, :id).to_h { |name, id| [:"#{prefix}custom_field_#{id}", name] }
end

def work_package_cfs_for(type)
all_work_package_cfs.where(type: type)
end

def all_work_package_cfs
WorkPackageCustomField.where(multi_value: false).order(:name)
end

def project_attributes
ProjectCustomField.where(admin_only: false, multi_value: false).order(:name)
end
end
end
Expand Down
31 changes: 29 additions & 2 deletions spec/models/types/pattern_resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@

RSpec.describe Types::PatternResolver do
let(:subject_pattern) { "ID Please: {{id}}" }
let(:work_package) { create(:work_package) }

shared_let(:work_package) { create(:work_package) }

subject(:resolver) { described_class.new(subject_pattern) }

Expand All @@ -45,7 +46,7 @@

it "resolves the pattern" do
expect(subject.resolve(work_package))
.to eq("#{work_package.id} | N/A | #{work_package.created_at.to_date.iso8601}")
.to eq("#{work_package.id} | NA | #{work_package.created_at.to_date.iso8601}")
end
end

Expand All @@ -57,4 +58,30 @@
.to eq("#{work_package.id} | #{work_package.author.name} | #{work_package.type.name}")
end
end

context "when the pattern has custom fields" do
let(:custom_field) { create(:string_wp_custom_field) }
let(:type) { create(:type, custom_fields: [custom_field]) }
let(:project) { create(:project, types: [type], work_package_custom_fields: [custom_field]) }
let(:project_custom_field) { create(:project_custom_field, projects: [project], field_format: "string") }

let(:subject_pattern) do
"{{project_custom_field_#{project_custom_field.id}}} A custom field value: {{custom_field_#{custom_field.id}}}"
end

let(:work_package) do
create(:work_package, type:, project:, custom_values: { custom_field.id => "Important Information" })
end

before do
project.public_send :"custom_field_#{project_custom_field.id}=", "PROSPEC"
project.save
end

it "resolves the pattern" do
User.current = SystemUser.first

expect(subject.resolve(work_package)).to eq("PROSPEC A custom field value: Important Information")
end
end
end
13 changes: 10 additions & 3 deletions spec/models/types/patterns/token_property_mapper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,17 @@
assigned_to: assignee, parent: work_package_parent, start_date: Time.zone.today, estimated_hours: 30)
end

described_class::MAPPING.each_pair do |key, fn|
described_class::TOKEN_PROPERTY_MAP.each_pair do |key, details|
it "the token named #{key} resolves successfully" do
expect { fn.call(work_package) }.not_to raise_error
expect(fn.call(work_package)).not_to be_nil
expect { details[:fn].call(work_package) }.not_to raise_error
expect(details[:fn].call(work_package)).not_to be_nil
end
end

it "returns all possible tokens" do
tokens = described_class.new.tokens_for_type(work_package.type)

expect(tokens.keys).to match_array(%i[work_package project parent])
expect(tokens[:project][:project_status]).to eq(Project.human_attribute_name(:status_code))
end
end

0 comments on commit 426dbea

Please sign in to comment.