moco-ruby 0.1.2 → 1.0.0.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +10 -3
- data/CHANGELOG.md +65 -4
- data/Gemfile +2 -1
- data/Gemfile.lock +70 -23
- data/README.md +199 -55
- data/examples/v2_api_example.rb +73 -0
- data/lib/moco/client.rb +47 -0
- data/lib/moco/collection_proxy.rb +190 -0
- data/lib/moco/connection.rb +62 -0
- data/lib/moco/entities/activity.rb +96 -0
- data/lib/moco/entities/base_entity.rb +303 -0
- data/lib/moco/entities/company.rb +28 -0
- data/lib/moco/entities/deal.rb +24 -0
- data/lib/moco/entities/expense.rb +29 -0
- data/lib/moco/entities/holiday.rb +25 -0
- data/lib/moco/entities/invoice.rb +53 -0
- data/lib/moco/entities/planning_entry.rb +26 -0
- data/lib/moco/entities/presence.rb +30 -0
- data/lib/moco/entities/project.rb +39 -0
- data/lib/moco/entities/schedule.rb +26 -0
- data/lib/moco/entities/task.rb +20 -0
- data/lib/moco/entities/user.rb +33 -0
- data/lib/moco/entities/web_hook.rb +27 -0
- data/lib/moco/entities.rb +11 -4
- data/lib/moco/entity_collection.rb +59 -0
- data/lib/moco/helpers.rb +1 -0
- data/lib/moco/nested_collection_proxy.rb +40 -0
- data/lib/moco/sync.rb +70 -29
- data/lib/moco/version.rb +1 -1
- data/lib/moco.rb +26 -2
- data/mocurl.rb +51 -34
- data/sync_activity.rb +4 -4
- metadata +43 -10
- data/lib/moco/api.rb +0 -194
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO deal
|
5
|
+
# Provides methods for deal-specific associations
|
6
|
+
class Deal < BaseEntity
|
7
|
+
# Associations
|
8
|
+
def company
|
9
|
+
association(:company) || association(:customer, "Company")
|
10
|
+
end
|
11
|
+
|
12
|
+
def user
|
13
|
+
association(:user)
|
14
|
+
end
|
15
|
+
|
16
|
+
def category
|
17
|
+
association(:category, "DealCategory")
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"#{name} (#{company&.name})"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO expense
|
5
|
+
# Provides methods for expense-specific operations and associations
|
6
|
+
class Expense < BaseEntity
|
7
|
+
# Class methods for bulk operations
|
8
|
+
def self.disregard(client, expense_ids:)
|
9
|
+
client.post("projects/expenses/disregard", { expense_ids: })
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.bulk_create(client, project_id, expenses)
|
13
|
+
client.post("projects/#{project_id}/expenses/bulk", { expenses: })
|
14
|
+
end
|
15
|
+
|
16
|
+
# Associations
|
17
|
+
def project
|
18
|
+
@project ||= client.projects.find(project_id) if project_id
|
19
|
+
end
|
20
|
+
|
21
|
+
def user
|
22
|
+
@user ||= client.users.find(user_id) if user_id
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"#{date} - #{title} (#{amount})"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO holiday entry
|
5
|
+
# Provides methods for holiday-specific associations
|
6
|
+
class Holiday < BaseEntity
|
7
|
+
# Override entity_path to match API path
|
8
|
+
def entity_path
|
9
|
+
"users/holidays"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Associations
|
13
|
+
def user
|
14
|
+
@user ||= client.users.find(user_id) if user_id
|
15
|
+
end
|
16
|
+
|
17
|
+
def creator
|
18
|
+
@creator ||= client.users.find(creator_id) if creator_id
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
"#{year} - #{title} - #{days} days (#{hours} hours) - #{user&.full_name}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO invoice
|
5
|
+
# Provides methods for invoice-specific operations and associations
|
6
|
+
class Invoice < BaseEntity
|
7
|
+
# Instance methods for invoice-specific operations
|
8
|
+
def update_status(status)
|
9
|
+
client.put("invoices/#{id}/update_status", { status: })
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def pdf
|
14
|
+
client.get("invoices/#{id}.pdf")
|
15
|
+
end
|
16
|
+
|
17
|
+
def timesheet
|
18
|
+
client.get("invoices/#{id}/timesheet")
|
19
|
+
end
|
20
|
+
|
21
|
+
def timesheet_pdf
|
22
|
+
client.get("invoices/#{id}/timesheet.pdf")
|
23
|
+
end
|
24
|
+
|
25
|
+
def expenses
|
26
|
+
client.get("invoices/#{id}/expenses")
|
27
|
+
end
|
28
|
+
|
29
|
+
def send_email(recipient:, subject:, text:, **options)
|
30
|
+
payload = {
|
31
|
+
recipient:,
|
32
|
+
subject:,
|
33
|
+
text:
|
34
|
+
}.merge(options)
|
35
|
+
|
36
|
+
client.post("invoices/#{id}/send_email", payload)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Associations
|
41
|
+
def company
|
42
|
+
association(:customer, "Company")
|
43
|
+
end
|
44
|
+
|
45
|
+
def project
|
46
|
+
association(:project)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"#{identifier} - #{title} (#{date})"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO planning entry
|
5
|
+
# Provides methods for planning entry-specific associations
|
6
|
+
class PlanningEntry < BaseEntity
|
7
|
+
# Associations
|
8
|
+
def user
|
9
|
+
@user ||= client.users.find(user_id) if user_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def project
|
13
|
+
@project ||= client.projects.find(project_id) if project_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def deal
|
17
|
+
@deal ||= client.deals.find(deal_id) if deal_id
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
period = starts_on == ends_on ? starts_on : "#{starts_on} to #{ends_on}"
|
22
|
+
resource = project || deal
|
23
|
+
"#{period} - #{hours_per_day}h/day - #{user&.full_name} - #{resource&.name}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO presence entry
|
5
|
+
# Provides methods for presence-specific operations and associations
|
6
|
+
class Presence < BaseEntity
|
7
|
+
# Define the specific API path for this entity as a class method
|
8
|
+
def self.entity_path
|
9
|
+
"users/presences"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Class methods for special operations
|
13
|
+
def self.touch(client, is_home_office: false, override: nil)
|
14
|
+
payload = {}
|
15
|
+
payload[:is_home_office] = is_home_office if is_home_office
|
16
|
+
payload[:override] = override if override
|
17
|
+
|
18
|
+
client.post("users/presences/touch", payload)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Associations
|
22
|
+
def user
|
23
|
+
@user ||= client.users.find(user_id) if user_id
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
"#{date} - #{from} to #{to} - #{user&.full_name}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
class Project < BaseEntity
|
5
|
+
def customer
|
6
|
+
# Use the association method to fetch the customer
|
7
|
+
association(:customer, "Company")
|
8
|
+
end
|
9
|
+
|
10
|
+
# Fetches activities associated with this project.
|
11
|
+
def activities
|
12
|
+
# Use the has_many method to fetch activities
|
13
|
+
has_many(:activities)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Fetches tasks associated with this project.
|
17
|
+
def tasks
|
18
|
+
# Check if tasks are already loaded in attributes
|
19
|
+
if attributes[:tasks].is_a?(Array) && attributes[:tasks].all? { |t| t.is_a?(MOCO::Task) }
|
20
|
+
# If tasks are already loaded, create a NestedCollectionProxy with the loaded tasks
|
21
|
+
@_tasks_proxy ||= begin
|
22
|
+
require_relative "../nested_collection_proxy"
|
23
|
+
proxy = MOCO::NestedCollectionProxy.new(client, self, :tasks, "Task")
|
24
|
+
# We need to manually set the loaded records since we already have them
|
25
|
+
proxy.instance_variable_set(:@records, attributes[:tasks])
|
26
|
+
proxy.instance_variable_set(:@loaded, true)
|
27
|
+
proxy
|
28
|
+
end
|
29
|
+
else
|
30
|
+
# Otherwise, use has_many with nested=true
|
31
|
+
has_many(:tasks, nil, nil, true)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def active?
|
36
|
+
status == "active"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO schedule entry
|
5
|
+
# Provides methods for schedule-specific associations
|
6
|
+
class Schedule < BaseEntity
|
7
|
+
# Associations
|
8
|
+
def user
|
9
|
+
@user ||= client.users.find(user_id) if user_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def assignment
|
13
|
+
return nil unless assignment_id
|
14
|
+
|
15
|
+
@assignment ||= if assignment_type == "Absence"
|
16
|
+
client.absences.find(assignment_id)
|
17
|
+
else
|
18
|
+
client.projects.find(assignment_id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"#{date} - #{user&.full_name} - #{assignment&.name}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO task
|
5
|
+
# Provides methods for task-specific associations
|
6
|
+
class Task < BaseEntity
|
7
|
+
# Associations
|
8
|
+
def project
|
9
|
+
@project ||= client.projects.find(project_id) if project_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def activities
|
13
|
+
client.activities.where(task_id: id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO user
|
5
|
+
# Provides methods for user-specific operations and associations
|
6
|
+
class User < BaseEntity
|
7
|
+
# Instance methods for user-specific operations
|
8
|
+
def performance_report
|
9
|
+
client.get("users/#{id}/performance_report")
|
10
|
+
end
|
11
|
+
|
12
|
+
# Associations
|
13
|
+
def activities
|
14
|
+
has_many(:activities)
|
15
|
+
end
|
16
|
+
|
17
|
+
def presences
|
18
|
+
has_many(:presences)
|
19
|
+
end
|
20
|
+
|
21
|
+
def holidays
|
22
|
+
has_many(:holidays)
|
23
|
+
end
|
24
|
+
|
25
|
+
def full_name
|
26
|
+
"#{firstname} #{lastname}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
full_name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Represents a MOCO webhook
|
5
|
+
# Provides methods for webhook-specific operations
|
6
|
+
class WebHook < BaseEntity
|
7
|
+
# Override entity_path to match API path
|
8
|
+
def entity_path
|
9
|
+
"account/web_hooks"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Instance methods for webhook-specific operations
|
13
|
+
def enable
|
14
|
+
client.put("account/web_hooks/#{id}/enable")
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def disable
|
19
|
+
client.put("account/web_hooks/#{id}/disable")
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{target} - #{url}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/moco/entities.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "helpers"
|
|
4
4
|
|
5
5
|
module MOCO
|
6
6
|
# Base entity class others inherit from, providing comparison, to_h, to_json
|
7
|
+
# @deprecated Use MOCO::BaseEntity from entities/base_entity.rb instead
|
7
8
|
class BaseEntity
|
8
9
|
def eql?(other)
|
9
10
|
return false unless other.is_a? self.class
|
@@ -28,7 +29,6 @@ module MOCO
|
|
28
29
|
hash
|
29
30
|
end
|
30
31
|
|
31
|
-
# rubocop:disable Metrics/MethodLength
|
32
32
|
def to_json(*arg)
|
33
33
|
to_h do |k, v|
|
34
34
|
if v.is_a? Hash
|
@@ -43,9 +43,9 @@ module MOCO
|
|
43
43
|
end.to_h.to_json(arg)
|
44
44
|
end
|
45
45
|
end
|
46
|
-
# rubocop:enable Metrics/MethodLength
|
47
46
|
|
48
47
|
# https://hundertzehn.github.io/mocoapp-api-docs/sections/projects.html
|
48
|
+
# @deprecated Use MOCO::Project from entities/project.rb instead
|
49
49
|
class Project < BaseEntity
|
50
50
|
attr_accessor :id, :active, :name, :customer, :tasks
|
51
51
|
|
@@ -55,6 +55,7 @@ module MOCO
|
|
55
55
|
end
|
56
56
|
|
57
57
|
# https://hundertzehn.github.io/mocoapp-api-docs/sections/project_tasks.html
|
58
|
+
# @deprecated Use MOCO::Task from entities/task.rb instead
|
58
59
|
class Task < BaseEntity
|
59
60
|
attr_accessor :id, :active, :name, :project_id, :billable
|
60
61
|
|
@@ -64,22 +65,28 @@ module MOCO
|
|
64
65
|
end
|
65
66
|
|
66
67
|
# https://hundertzehn.github.io/mocoapp-api-docs/sections/activities.html
|
68
|
+
# @deprecated Use MOCO::Activity from entities/activity.rb instead
|
67
69
|
class Activity < BaseEntity
|
68
70
|
attr_accessor :id, :active, :date, :description, :project, :task, :seconds, :hours, :billable, :billed, :user,
|
69
71
|
:customer, :tag
|
70
72
|
|
71
73
|
def to_s
|
72
|
-
|
73
|
-
|
74
|
+
description_part = description.empty? ? "" : " (#{description})"
|
75
|
+
status_part = "(#{%i[billable billed].map { |x| (send(x) ? "" : "not ") + x.to_s }.join(", ")})"
|
76
|
+
|
77
|
+
"#{date} - #{Helpers.decimal_hours_to_civil(hours)}h (#{seconds}s) - " \
|
78
|
+
"#{project&.name} - #{task&.name}#{description_part} #{status_part}"
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
77
82
|
# https://hundertzehn.github.io/mocoapp-api-docs/sections/companies.html
|
83
|
+
# @deprecated Use MOCO::Company from entities/company.rb instead
|
78
84
|
class Customer < BaseEntity
|
79
85
|
attr_accessor :id, :name
|
80
86
|
end
|
81
87
|
|
82
88
|
# https://hundertzehn.github.io/mocoapp-api-docs/sections/users.html
|
89
|
+
# @deprecated Use MOCO::User from entities/user.rb instead
|
83
90
|
class User < BaseEntity
|
84
91
|
attr_accessor :id, :firstname, :lastname
|
85
92
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "collection_proxy"
|
4
|
+
|
5
|
+
module MOCO
|
6
|
+
# Provides high-level collection operations for MOCO entities
|
7
|
+
class EntityCollection
|
8
|
+
include Enumerable
|
9
|
+
attr_reader :client, :path, :entity_class_name
|
10
|
+
|
11
|
+
def initialize(client, path, entity_class_name)
|
12
|
+
@client = client
|
13
|
+
@path = path
|
14
|
+
@entity_class_name = entity_class_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def all(params = {})
|
18
|
+
collection.all(params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(id)
|
22
|
+
collection.find(id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def where(filters = {})
|
26
|
+
collection.where(filters)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create(attributes)
|
30
|
+
collection.create(attributes)
|
31
|
+
end
|
32
|
+
|
33
|
+
def each(&)
|
34
|
+
all.each(&)
|
35
|
+
end
|
36
|
+
|
37
|
+
def first
|
38
|
+
all.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def count
|
42
|
+
all.count
|
43
|
+
end
|
44
|
+
|
45
|
+
def update(id, attributes)
|
46
|
+
collection.update(id, attributes)
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete(id)
|
50
|
+
collection.delete(id)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def collection
|
56
|
+
@collection ||= CollectionProxy.new(client, path, entity_class_name)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/moco/helpers.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MOCO
|
4
|
+
# Provides ActiveRecord-style query interface for nested MOCO entities
|
5
|
+
# For example, project.tasks is a nested collection of tasks under a project
|
6
|
+
class NestedCollectionProxy < CollectionProxy
|
7
|
+
attr_reader :parent, :records
|
8
|
+
|
9
|
+
def initialize(client, parent, path_or_entity_name, entity_class_name)
|
10
|
+
@parent = parent
|
11
|
+
super(client, path_or_entity_name, entity_class_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Override determine_base_path to include the parent's path
|
15
|
+
def determine_base_path(path_or_entity_name)
|
16
|
+
parent_type = ActiveSupport::Inflector.underscore(parent.class.name.split("::").last)
|
17
|
+
"#{parent_type.pluralize}/#{parent.id}/#{super}"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create a new entity in this nested collection
|
21
|
+
def create(attributes)
|
22
|
+
klass = entity_class
|
23
|
+
return nil unless klass && klass <= MOCO::BaseEntity
|
24
|
+
|
25
|
+
klass.new(client, client.post(@base_path, attributes))
|
26
|
+
end
|
27
|
+
|
28
|
+
# Delete all entities in this nested collection
|
29
|
+
def destroy_all
|
30
|
+
client.delete("#{@base_path}/destroy_all")
|
31
|
+
true
|
32
|
+
rescue StandardError => e
|
33
|
+
warn "Warning: Failed to destroy all entities in #{@base_path}: #{e.message}"
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Make these methods public so they can be accessed by Project#tasks
|
38
|
+
public :load_records, :loaded?
|
39
|
+
end
|
40
|
+
end
|
data/lib/moco/sync.rb
CHANGED
@@ -1,35 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "fuzzy_match"
|
4
|
-
require_relative "
|
4
|
+
require_relative "client"
|
5
5
|
|
6
6
|
module MOCO
|
7
7
|
# Match and map projects and tasks between MOCO instances and sync activities
|
8
8
|
class Sync
|
9
9
|
attr_reader :project_mapping, :task_mapping, :source_projects, :target_projects
|
10
10
|
attr_accessor :project_match_threshold, :task_match_threshold, :dry_run
|
11
|
-
|
12
|
-
def initialize(
|
13
|
-
@
|
14
|
-
@
|
11
|
+
|
12
|
+
def initialize(source_client, target_client, **args)
|
13
|
+
@source = source_client
|
14
|
+
@target = target_client
|
15
15
|
@project_match_threshold = args.fetch(:project_match_threshold, 0.8)
|
16
16
|
@task_match_threshold = args.fetch(:task_match_threshold, 0.45)
|
17
17
|
@filters = args.fetch(:filters, {})
|
18
18
|
@dry_run = args.fetch(:dry_run, false)
|
19
|
-
|
19
|
+
|
20
20
|
@project_mapping = {}
|
21
21
|
@task_mapping = {}
|
22
|
-
|
22
|
+
|
23
23
|
fetch_assigned_projects
|
24
24
|
build_initial_mappings
|
25
25
|
end
|
26
|
-
|
27
|
-
# rubocop:todo Metrics/
|
26
|
+
|
27
|
+
# rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
28
28
|
def sync(&callbacks)
|
29
29
|
results = []
|
30
30
|
|
31
|
-
source_activities_r = @
|
32
|
-
target_activities_r = @
|
31
|
+
source_activities_r = @source.activities.all(@filters.fetch(:source, {}))
|
32
|
+
target_activities_r = @target.activities.all(@filters.fetch(:target, {}))
|
33
33
|
|
34
34
|
source_activities_grouped = source_activities_r.group_by(&:date).transform_values do |activities|
|
35
35
|
activities.group_by(&:project)
|
@@ -70,14 +70,14 @@ module MOCO
|
|
70
70
|
end
|
71
71
|
callbacks&.call(:update, source_activity, best_match)
|
72
72
|
unless @dry_run
|
73
|
-
results << @
|
73
|
+
results << @target.activities.update(best_match)
|
74
74
|
callbacks&.call(:updated, source_activity, best_match, results.last)
|
75
75
|
end
|
76
76
|
when 0...60
|
77
77
|
# <60 - no good match found, create new entry
|
78
78
|
callbacks&.call(:create, source_activity, expected_target_activity)
|
79
79
|
unless @dry_run
|
80
|
-
results << @
|
80
|
+
results << @target.activities.create(expected_target_activity)
|
81
81
|
callbacks&.call(:created, source_activity, best_match, results.last)
|
82
82
|
end
|
83
83
|
end
|
@@ -89,15 +89,14 @@ module MOCO
|
|
89
89
|
end
|
90
90
|
|
91
91
|
source_activities_r.each do |source_activity|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
92
|
+
next if used_source_activities.include?(source_activity)
|
93
|
+
next unless @project_mapping[source_activity.project.id]
|
94
|
+
|
95
|
+
expected_target_activity = get_expected_target_activity(source_activity)
|
96
|
+
callbacks&.call(:create, source_activity, expected_target_activity)
|
97
|
+
unless @dry_run
|
98
|
+
results << @target.activities.create(expected_target_activity)
|
99
|
+
callbacks&.call(:created, source_activity, expected_target_activity, results.last)
|
101
100
|
end
|
102
101
|
end
|
103
102
|
|
@@ -119,7 +118,7 @@ module MOCO
|
|
119
118
|
source_activities.each do |source_activity|
|
120
119
|
target_activities.each do |target_activity|
|
121
120
|
score = score_activity_match(get_expected_target_activity(source_activity), target_activity)
|
122
|
-
matches << { activity: [source_activity, target_activity], score:
|
121
|
+
matches << { activity: [source_activity, target_activity], score: }
|
123
122
|
end
|
124
123
|
end
|
125
124
|
matches
|
@@ -152,8 +151,26 @@ module MOCO
|
|
152
151
|
# rubocop:enable Metrics/AbcSize
|
153
152
|
|
154
153
|
def fetch_assigned_projects
|
155
|
-
@source_projects = @
|
156
|
-
@target_projects = @
|
154
|
+
@source_projects = @source.projects.all(**@filters.fetch(:source, {}), active: "true")
|
155
|
+
@target_projects = @target.projects.all(**@filters.fetch(:target, {}), active: "true")
|
156
|
+
|
157
|
+
# Ensure we have proper collections
|
158
|
+
@source_projects = if @source_projects.is_a?(MOCO::EntityCollection)
|
159
|
+
@source_projects
|
160
|
+
else
|
161
|
+
MOCO::EntityCollection.new(@source,
|
162
|
+
"projects", "Project").tap do |c|
|
163
|
+
c.instance_variable_set(:@items, [@source_projects])
|
164
|
+
end
|
165
|
+
end
|
166
|
+
@target_projects = if @target_projects.is_a?(MOCO::EntityCollection)
|
167
|
+
@target_projects
|
168
|
+
else
|
169
|
+
MOCO::EntityCollection.new(@target,
|
170
|
+
"projects", "Project").tap do |c|
|
171
|
+
c.instance_variable_set(:@items, [@target_projects])
|
172
|
+
end
|
173
|
+
end
|
157
174
|
end
|
158
175
|
|
159
176
|
def build_initial_mappings
|
@@ -170,13 +187,37 @@ module MOCO
|
|
170
187
|
end
|
171
188
|
|
172
189
|
def match_project(target_project)
|
173
|
-
|
174
|
-
|
190
|
+
# Create array of search objects manually since we can't call map on EntityCollection
|
191
|
+
searchable_projects = []
|
192
|
+
|
193
|
+
# Manually iterate since we can't rely on Enumerable methods
|
194
|
+
@source_projects.each do |project|
|
195
|
+
warn project.inspect
|
196
|
+
searchable_projects << { original: project, name: project.name }
|
197
|
+
end
|
198
|
+
|
199
|
+
matcher = FuzzyMatch.new(searchable_projects, read: :name)
|
200
|
+
match = matcher.find(target_project.name, threshold: @project_match_threshold)
|
201
|
+
match[:original] if match
|
175
202
|
end
|
176
203
|
|
177
204
|
def match_task(target_task, source_project)
|
178
|
-
|
179
|
-
|
205
|
+
# Get tasks from the source project
|
206
|
+
tasks = source_project.tasks
|
207
|
+
|
208
|
+
# Create array of search objects manually since we can't rely on Enumerable methods
|
209
|
+
|
210
|
+
# Manually iterate through tasks
|
211
|
+
searchable_tasks = tasks.map do |task|
|
212
|
+
{ original: task, name: task.name }
|
213
|
+
end
|
214
|
+
|
215
|
+
# Only proceed if we have tasks to match against
|
216
|
+
return nil if searchable_tasks.empty?
|
217
|
+
|
218
|
+
matcher = FuzzyMatch.new(searchable_tasks, read: :name)
|
219
|
+
match = matcher.find(target_task.name, threshold: @task_match_threshold)
|
220
|
+
match[:original] if match
|
180
221
|
end
|
181
222
|
end
|
182
223
|
end
|