Class: MOCO::Sync
- Inherits:
-
Object
- Object
- MOCO::Sync
- Defined in:
- lib/moco/sync.rb
Overview
Match and map projects and tasks between MOCO instances and sync activities
Instance Attribute Summary collapse
-
#debug ⇒ Object
Returns the value of attribute debug.
-
#dry_run ⇒ Object
Returns the value of attribute dry_run.
-
#project_mapping ⇒ Object
readonly
Returns the value of attribute project_mapping.
-
#project_match_threshold ⇒ Object
Returns the value of attribute project_match_threshold.
-
#source_projects ⇒ Object
readonly
Returns the value of attribute source_projects.
-
#target_projects ⇒ Object
readonly
Returns the value of attribute target_projects.
-
#task_mapping ⇒ Object
readonly
Returns the value of attribute task_mapping.
-
#task_match_threshold ⇒ Object
Returns the value of attribute task_match_threshold.
Instance Method Summary collapse
-
#initialize(source_client, target_client, **args) ⇒ Sync
constructor
A new instance of Sync.
-
#sync(&callbacks) ⇒ Object
rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity.
Constructor Details
#initialize(source_client, target_client, **args) ⇒ Sync
Returns a new instance of Sync.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/moco/sync.rb', line 13 def initialize(source_client, target_client, **args) @source = source_client @target = target_client @project_match_threshold = args.fetch(:project_match_threshold, 0.8) @task_match_threshold = args.fetch(:task_match_threshold, 0.45) @filters = args.fetch(:filters, {}) @dry_run = args.fetch(:dry_run, false) @debug = args.fetch(:debug, false) @default_task_name = args.fetch(:default_task_name, nil) @project_mapping = {} @task_mapping = {} @default_task_cache = {} # Cache default tasks per project fetch_assigned_projects build_initial_mappings create_missing_tasks_for_activities end |
Instance Attribute Details
#debug ⇒ Object
Returns the value of attribute debug.
11 12 13 |
# File 'lib/moco/sync.rb', line 11 def debug @debug end |
#dry_run ⇒ Object
Returns the value of attribute dry_run.
11 12 13 |
# File 'lib/moco/sync.rb', line 11 def dry_run @dry_run end |
#project_mapping ⇒ Object (readonly)
Returns the value of attribute project_mapping.
10 11 12 |
# File 'lib/moco/sync.rb', line 10 def project_mapping @project_mapping end |
#project_match_threshold ⇒ Object
Returns the value of attribute project_match_threshold.
11 12 13 |
# File 'lib/moco/sync.rb', line 11 def project_match_threshold @project_match_threshold end |
#source_projects ⇒ Object (readonly)
Returns the value of attribute source_projects.
10 11 12 |
# File 'lib/moco/sync.rb', line 10 def source_projects @source_projects end |
#target_projects ⇒ Object (readonly)
Returns the value of attribute target_projects.
10 11 12 |
# File 'lib/moco/sync.rb', line 10 def target_projects @target_projects end |
#task_mapping ⇒ Object (readonly)
Returns the value of attribute task_mapping.
10 11 12 |
# File 'lib/moco/sync.rb', line 10 def task_mapping @task_mapping end |
#task_match_threshold ⇒ Object
Returns the value of attribute task_match_threshold.
11 12 13 |
# File 'lib/moco/sync.rb', line 11 def task_match_threshold @task_match_threshold end |
Instance Method Details
#sync(&callbacks) ⇒ Object
rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/moco/sync.rb', line 33 def sync(&callbacks) results = [] source_activity_filters = @filters.fetch(:source, {}) source_activities_r = @source.activities.where(source_activity_filters).all debug_log "Fetched #{source_activities_r.size} source activities" # Log source activities for debugging debug_log "Source activities:" source_activities_r.each do |activity| debug_log " Source Activity: #{activity.id}, Date: #{activity.date}, Project: #{activity.project&.id} (#{activity.project&.name}), Task: #{activity.task&.id} (#{activity.task&.name}), Hours: #{activity.hours}, Description: #{activity.description}, Remote ID: #{activity.remote_id}" # Also log the expected target activity for each source activity begin expected = get_expected_target_activity(activity) if expected project_id = expected.project&.id rescue "N/A" task_id = expected.task&.id rescue "N/A" remote_id = expected.instance_variable_get(:@attributes)[:remote_id] rescue "N/A" debug_log " Expected Target: Project: #{project_id}, Task: #{task_id}, Remote ID: #{remote_id}" end rescue => e debug_log " Error getting expected target: #{e.message}" end end target_activity_filters = @filters.fetch(:target, {}) target_activities_r = @target.activities.where(target_activity_filters).all debug_log "Fetched #{target_activities_r.size} target activities" # Log target activities for debugging debug_log "Target activities:" target_activities_r.each do |activity| debug_log " Target Activity: #{activity.id}, Date: #{activity.date}, Project: #{activity.project&.id} (#{activity.project&.name}), Task: #{activity.task&.id} (#{activity.task&.name}), Hours: #{activity.hours}, Description: #{activity.description}, Remote ID: #{activity.remote_id}" end # Group activities by date and then by project_id for consistent lookups source_activities_grouped = source_activities_r.group_by(&:date).transform_values do |activities| activities.group_by { |a| a.project&.id } # Group by project ID end target_activities_grouped = target_activities_r.group_by(&:date).transform_values do |activities| activities.group_by { |a| a.project&.id } # Group by project ID end used_source_activities = [] used_target_activities = [] debug_log "Starting main sync loop..." source_activities_grouped.each do |date, activities_by_project_id| debug_log "Processing date: #{date}" activities_by_project_id.each do |source_project_id, source_activities| debug_log " Processing source project ID: #{source_project_id} (#{source_activities.count} activities)" # Find the corresponding target project ID using the mapping target_project_object = @project_mapping[source_project_id] unless target_project_object debug_log " Skipping - Source project ID #{source_project_id} not mapped." next end target_project_id = target_project_object.id # Fetch target activities using the target project ID target_activities = target_activities_grouped.fetch(date, {}).fetch(target_project_id, []) debug_log " Found #{target_activities.count} target activities for target project ID: #{target_project_id}" if source_activities.empty? || target_activities.empty? debug_log " Skipping - No source or target activities for this date/project pair." next end matches = calculate_matches(source_activities, target_activities) debug_log " Calculated #{matches.count} potential matches." matches.sort_by! { |match| -match[:score] } debug_log " Entering matches loop..." matches.each do |match| source_activity, target_activity = match[:activity] score = match[:score] debug_log " Match Pair: Score=#{score}, Source=#{source_activity.id}, Target=#{target_activity.id}" if used_source_activities.include?(source_activity) || used_target_activities.include?(target_activity) debug_log " Skipping match pair - already used: Source used=#{used_source_activities.include?(source_activity)}, Target used=#{used_target_activities.include?(target_activity)}" next end best_score = score # Since we sorted, this is the best score for this unused pair best_match = target_activity expected_target_activity = get_expected_target_activity(source_activity) debug_log " Processing best score #{best_score} for Source=#{source_activity.id}" case best_score when 100 debug_log " Case 100: Equal" # 100 - perfect match found, nothing needs doing callbacks&.call(:equal, source_activity, expected_target_activity) # Mark both as used debug_log " Marking Source=#{source_activity.id} and Target=#{target_activity.id} as used." used_source_activities << source_activity used_target_activities << target_activity when 60...100 debug_log " Case 60-99: Update" # >=60 <100 - match with some differences expected_target_activity.to_h.except(:id, :user, :customer).each do |k, v| debug_log " Updating attribute #{k} on Target=#{target_activity.id}" best_match.send("#{k}=", v) end callbacks&.call(:update, source_activity, best_match) unless @dry_run debug_log " Executing API update for Target=#{target_activity.id}" results << @target.activities.update(best_match.id, best_match.attributes) # Pass ID and attributes callbacks&.call(:updated, source_activity, best_match, results.last) end # Mark both as used debug_log " Marking Source=#{source_activity.id} and Target=#{target_activity.id} as used." used_source_activities << source_activity used_target_activities << target_activity when 0...60 debug_log " Case 0-59: Low score, doing nothing for this pair." # <60 - Low score for this specific pair. Do nothing here. # Creation is handled later if source_activity remains unused. nil # Explicitly do nothing end # Only mark activities as used if score >= 60 (handled within the case branches above) end debug_log " Finished matches loop." end debug_log " Finished processing project IDs for date #{date}." end debug_log "Finished main sync loop." # Second loop: Create source activities that were never used (i.e., had no match >= 60) debug_log "Starting creation loop..." source_activities_r.each do |source_activity| if used_source_activities.include?(source_activity) debug_log " Skipping creation for Source=#{source_activity.id} - already used." next end # Use safe navigation in case project is nil source_project_id = source_activity.project&.id unless @project_mapping[source_project_id] debug_log " Skipping creation for Source=#{source_activity.id} - project #{source_project_id} not mapped." next end debug_log " Processing creation for Source=#{source_activity.id}" expected_target_activity = get_expected_target_activity(source_activity) callbacks&.call(:create, source_activity, expected_target_activity) next if @dry_run debug_log " Executing API create." # Pass attributes hash to create created_activity = @target.activities.create(expected_target_activity.attributes) results << created_activity # Pass the actual created activity object to the callback callbacks&.call(:created, source_activity, created_activity, results.last) end debug_log "Finished creation loop." results end |