effective_mergery 0.2.0 → 1.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09d729cc7ba2f40740a83f58b09c7244d2f82f323b7b50fd9ac32f43cbd0b009'
4
- data.tar.gz: ee2e412bf88531636840f41817c5b366516f6fa7c1d7abbeca9ef2eb20145bdf
3
+ metadata.gz: d8a7243e93f05391396a3da6a84e7d1716df8cb7e8654ebb3256580305d8b58a
4
+ data.tar.gz: 2a18ad03f692f3a6d114a1ae098f2f09d981f03c4aecf338c7cc90504ecb454a
5
5
  SHA512:
6
- metadata.gz: e7ae5bfc66839df22f1b87fbf6533ae3e8c69aaa1895222c1ee6547222a108c37409c95508e488942470ec3e9f9c3ebb3b972469b4ae31dad749f29bba5e929f
7
- data.tar.gz: 1cbe65cefab6cdb88dd5565a38c5eda4cdd6b9d0ee1221721b3d9a6291a292dec45948a43d72ff6badf5c6536b10208840975fab33c811bab7199a421620fcbe
6
+ metadata.gz: dfd33e2d4c7d72786842be57e0ff29dd516ac08f390f0f2b85f845adb81494edccf6a593d15af8459d167f74433776e7c3fe6d9cee6a75b88f04e1880d055791
7
+ data.tar.gz: d5f1a5868dc219bcc2980ecafa86416cf03949450a4614363fdb917b725a922452afdd17924f58f6ca7f1abdfc3f8acc8301fcf5aaba4554dada6d479066f3e1
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2023 Code and Effect Inc.
1
+ Copyright 2026 Code and Effect Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Effective Mergery
2
2
 
3
- Merge any two Active Record objects, along with all associated objects, into one record.
3
+ Merge any two Active Record Users, along with all associated objects, into one record.
4
4
 
5
5
  ## Getting Started
6
6
 
@@ -38,11 +38,10 @@ Require the stylesheet on the asset pipeline by adding the following to your app
38
38
 
39
39
  ## Usage
40
40
 
41
- Visit `/admin/merge` and select an object type to merge.
41
+ Visit `/admin/merge/new` and select an object type to merge.
42
42
 
43
43
  ```ruby
44
- link_to 'Merge', effective_mergery.admin_merge_index_path
45
- link_to 'Merge: User', effective_mergery.new_admin_merge_path(type: 'User')
44
+ link_to 'Merge', effective_mergery.new_admin_merge_path
46
45
  ```
47
46
 
48
47
  ## Permissions
@@ -51,6 +50,7 @@ Add the following permissions (using CanCan):
51
50
 
52
51
  ```ruby
53
52
  can :admin, :effective_mergery
53
+ can :manage, Effective::Merge
54
54
  ```
55
55
 
56
56
  ## License
@@ -65,4 +65,3 @@ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
65
65
  4. Push to the branch (`git push origin my-new-feature`)
66
66
  5. Bonus points for test coverage
67
67
  6. Create new Pull Request
68
-
File without changes
@@ -0,0 +1,15 @@
1
+ module Admin
2
+ class MergesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_mergery) }
5
+
6
+ include Effective::CrudController
7
+
8
+ page_title "Merge Users"
9
+
10
+ def merge_params
11
+ params.require(:effective_merge).permit!
12
+ end
13
+
14
+ end
15
+ end
@@ -2,83 +2,215 @@ module Effective
2
2
  class Merge
3
3
  include ActiveModel::Model
4
4
 
5
- attr_accessor :type, :source, :source_id, :target, :target_id
5
+ attr_accessor :current_user
6
+ attr_accessor :source_type, :source_id
7
+ attr_accessor :target_type, :target_id
6
8
 
7
- validate(if: -> { source_id.present? }) { @source ||= collection.find_by_id(source_id) }
8
- validate(if: -> { target_id.present? }) { @target ||= collection.find_by_id(target_id) }
9
+ validates :target_id, presence: true
10
+ validates :target_type, presence: true
9
11
 
10
- validates :type, presence: true
11
- validates :source_id, presence: true, unless: -> { source.present? }
12
- validates :target_id, presence: true, unless: -> { target.present? }
12
+ validates :source_id, presence: true
13
+ validates :source_type, presence: true
13
14
 
14
- validate(if: -> { source_id.present? && target_id.present? }) do
15
- self.errors.add(:target_id, "can't be the same as source") if source_id == target_id
15
+ validate(if: -> { source.present? && target.present? }) do
16
+ errors.add(:base, "must be the same type") unless target.class == source.class
17
+ errors.add(:target_id, "can't be the same record") if target.id == source.id
16
18
  end
17
19
 
18
- validates :source, presence: { message: 'invalid source id' }, if: -> { source_id.present? }
19
- validates :target, presence: { message: 'invalid target id' }, if: -> { target_id.present? }
20
+ # Both records must be fully valid before we start moving anything
21
+ validate(if: -> { source.present? }) do
22
+ errors.add(:source_id, "is invalid: #{source.errors.full_messages.to_sentence}") unless source.valid?
23
+ end
20
24
 
21
- def to_s
22
- return 'New Merge' unless type
23
- type.downcase
25
+ validate(if: -> { target.present? }) do
26
+ errors.add(:target_id, "is invalid: #{target.errors.full_messages.to_sentence}") unless target.valid?
24
27
  end
25
28
 
26
- def save(validate: true)
27
- return false unless valid?
28
- (merge!(validate: validate) rescue false)
29
+ def to_s
30
+ (source.present? && target.present?) ? "Merge of #{source} to #{target}" : "New Merge"
29
31
  end
30
32
 
31
- def save!(validate: true)
32
- raise 'is invalid' unless valid?
33
- merge!(validate: validate)
33
+ def source
34
+ @source ||= source_type.try(:safe_constantize).try(:find_by_id, source_id)
34
35
  end
35
36
 
36
- def collection
37
- @collection ||= (klass.respond_to?(:effective_mergery_collection) ? klass.effective_mergery_collection : klass.all)
37
+ def source=(resource)
38
+ raise('expected an ActiveRecord::Base resource') unless resource.is_a?(ActiveRecord::Base)
39
+ assign_attributes(source_type: resource.class.name, source_id: resource.id)
40
+ @source = resource
38
41
  end
39
42
 
40
- def form_collection
41
- @form_collection ||= (klass.respond_to?(:effective_mergery_form_collection) ? klass.effective_mergery_form_collection : collection)
43
+ def target
44
+ @target ||= target_type.try(:safe_constantize).try(:find_by_id, target_id)
42
45
  end
43
46
 
44
- def klass
45
- @klass ||= type.safe_constantize
47
+ def target=(resource)
48
+ raise('expected an ActiveRecord::Base resource') unless resource.is_a?(ActiveRecord::Base)
49
+ assign_attributes(target_type: resource.class.name, target_id: resource.id)
50
+ @target = resource
46
51
  end
47
52
 
48
- # This is called on Admin::Merges#new
49
- def validate_klass!
50
- raise "type can't be blank" unless type.present?
51
- raise 'type must be a mergable type' unless EffectiveMergery.mergables.map(&:name).include?(type)
52
- raise "invalid ActiveRecord klass" unless klass
53
- raise "invalid ActiveRecord collection" unless collection.kind_of?(ActiveRecord::Relation)
53
+ def new_record?
54
54
  true
55
55
  end
56
56
 
57
- private
57
+ def save!
58
+ merge!
59
+ end
58
60
 
59
61
  def merge!(validate: true)
60
- resource = Effective::Resource.new(source)
61
- success = false
62
+ raise ActiveRecord::RecordInvalid.new(self) unless valid?
63
+
64
+ Rails.application.eager_load! unless Rails.application.config.eager_load
62
65
 
63
- klass.transaction do
64
- # Merge associations
65
- (resource.has_ones + resource.has_manys + resource.nested_resources).compact.each do |association|
66
- next if association.options[:through].present?
66
+ klasses = defined?(Tenant) ? Tenant.klasses : ActiveRecord::Base.descendants.reject(&:abstract_class?)
67
+ klasses = klasses.select { |klass| klass.table_exists? }
67
68
 
68
- Array(source.send(association.name)).each do |obj|
69
- obj.assign_attributes(association.foreign_key => target.id)
70
- obj.save!(validate: validate)
69
+ success = false
70
+
71
+ EffectiveResources.transaction do
72
+ # Re-point every record that belongs to the source onto the target, treating the target as the
73
+ # authoritative account. We walk from the belongs_to side so we catch foreign keys no has_many/has_one
74
+ # is declared for - polymorphic owners (Effective::Address, Effective::EventRegistration) and named
75
+ # self-refs alike (advisor_id, endorser_id, reviewer_id) - and move them with update_all. No per-record
76
+ # validations or callbacks run, so historical data and business rules can't block the merge. Any source
77
+ # record that would duplicate one the target already owns (per a uniqueness validator OR a unique index)
78
+ # is deleted instead of moved, so the merge never creates a duplicate or trips a unique constraint.
79
+ klasses.each do |klass|
80
+ source_foreign_keys(klass).each do |foreign_key, foreign_type|
81
+ source_records = records_for(klass, foreign_key, foreign_type, source)
82
+ next unless source_records.exists?
83
+
84
+ attributes = { foreign_key => target.id }
85
+ attributes[foreign_type] = target.class.name if foreign_type
86
+
87
+ # Addresses are kept as a whole set, not merged record by record: if the target already has any
88
+ # addresses, keep the target's and drop the source's; only copy the source's over when the target
89
+ # has none.
90
+ if klass.name == 'Effective::Address'
91
+ records_for(klass, foreign_key, foreign_type, target).exists? ? source_records.delete_all : source_records.update_all(attributes)
92
+ next
93
+ end
94
+
95
+ duplicate_ids = duplicate_record_ids(klass, foreign_key, foreign_type)
96
+ source_records.where(id: duplicate_ids).delete_all if duplicate_ids.present?
97
+ source_records.where.not(id: duplicate_ids).update_all(attributes)
71
98
  end
72
99
  end
73
100
 
74
- source.destroy!
101
+ # Prove the merge is complete before we destroy the source: nothing may still reference it.
102
+ assert_no_references_to_source!(klasses)
75
103
 
104
+ # Everything the source owned now points at the target; whatever is left dies with the source.
105
+ # Reload first so dependent: callbacks only fire for what STILL points at the source (update_all
106
+ # bypasses the in-memory association cache).
107
+ source.reload.destroy!
76
108
  target.save!(validate: validate)
109
+
110
+ log_merged!
111
+
77
112
  success = true
78
113
  end
79
114
 
80
115
  success
81
116
  end
82
117
 
118
+ private
119
+
120
+ # [[foreign_key, foreign_type], ...] for every belongs_to on klass that could point at the source:
121
+ # polymorphic, or one whose target IS the source's class (belongs_to :user, self-refs like advisor_id).
122
+ # foreign_type is nil for non-polymorphic associations; associations missing their column are skipped.
123
+ def source_foreign_keys(klass)
124
+ klass.reflect_on_all_associations(:belongs_to).filter_map do |reflection|
125
+ next unless reflection.polymorphic? || (reflection.klass == source.class rescue false)
126
+
127
+ foreign_key = reflection.foreign_key.to_s
128
+ next unless klass.column_names.include?(foreign_key)
129
+
130
+ [foreign_key, (reflection.foreign_type.to_s if reflection.polymorphic?)]
131
+ end
132
+ end
133
+
134
+ # The klass records owned by `owner` through foreign_key (scoped by *_type for polymorphics).
135
+ def records_for(klass, foreign_key, foreign_type, owner)
136
+ scope = klass.where(foreign_key => owner.id)
137
+ foreign_type ? scope.where(foreign_type => owner.class.name) : scope
138
+ end
139
+
140
+ # Safety net run before we destroy the source: after the move, none of the models we moved may still point
141
+ # at the source - otherwise destroying it would orphan or cascade-delete that record. Re-checks the same
142
+ # klasses the move walked, so a bug there (an STI type mismatch, a row written mid-merge) fails the merge
143
+ # loudly and rolls it back instead of losing data.
144
+ def assert_no_references_to_source!(klasses)
145
+ klasses.uniq(&:table_name).each do |klass|
146
+ source_foreign_keys(klass).each do |foreign_key, foreign_type|
147
+ next unless records_for(klass, foreign_key, foreign_type, source).exists?
148
+
149
+ raise "Merge incomplete: #{klass.table_name}.#{foreign_key} still references #{source_type} ##{source_id}"
150
+ end
151
+ end
152
+ end
153
+
154
+ # Ids of the source's `klass` records the (authoritative) target already has an equivalent of - judged by
155
+ # klass's uniqueness validators AND unique indexes that involve the foreign key we're repointing. These get
156
+ # deleted instead of moved, so the merge can neither create a duplicate nor trip a unique index.
157
+ def duplicate_record_ids(klass, foreign_key, foreign_type)
158
+ identifying_columns = dedupe_key_columns(klass, foreign_key, foreign_type)
159
+ return [] if identifying_columns.blank?
160
+
161
+ source_records = records_for(klass, foreign_key, foreign_type, source)
162
+ target_records = records_for(klass, foreign_key, foreign_type, target)
163
+
164
+ identifying_columns.flat_map do |columns|
165
+ if columns.empty?
166
+ # One-per-owner (e.g. a membership, unique on the owner alone): if the target already owns one,
167
+ # every source record is a duplicate and is dropped rather than moved into the unique constraint.
168
+ target_records.exists? ? source_records.pluck(:id) : []
169
+ else
170
+ existing = target_records.pluck(*columns).map { |row| Array(row) }.to_set
171
+ source_records.pluck(:id, *columns).filter_map { |id, *values| id if existing.include?(values) }
172
+ end
173
+ end.uniq
174
+ end
175
+
176
+ # The column sets that - together with the foreign key - make a record unique for its owner, drawn from
177
+ # both uniqueness validators and unique indexes. The foreign key (and its *_type) is dropped from each set
178
+ # since every moved record shares the target's value for those; an empty set means the record is unique on
179
+ # the owner alone (one-per-owner). Partial indexes are skipped - their WHERE can't be judged from columns.
180
+ def dedupe_key_columns(klass, foreign_key, foreign_type)
181
+ removable = [foreign_key, foreign_type].compact
182
+
183
+ from_validators = klass.validators.select { |validator| validator.kind == :uniqueness }.filter_map do |validator|
184
+ columns = (Array(validator.attributes) + Array(validator.options[:scope])).map(&:to_s)
185
+ (columns - removable) if columns.include?(foreign_key)
186
+ end
187
+
188
+ from_indexes =
189
+ begin
190
+ klass.connection.indexes(klass.table_name).filter_map do |index|
191
+ next unless index.unique && index.where.blank?
192
+ columns = Array(index.columns).map(&:to_s)
193
+ (columns - removable) if columns.include?(foreign_key)
194
+ end
195
+ rescue StandardError
196
+ []
197
+ end
198
+
199
+ (from_validators + from_indexes).uniq
200
+ end
201
+
202
+ def log_merged!
203
+ return unless defined?(EffectiveLogger)
204
+
205
+ EffectiveLogger.success(
206
+ "Merged #{source} into #{target}",
207
+ user: current_user,
208
+ associated: target,
209
+ source_id: source_id,
210
+ target_id: target_id,
211
+ source_email: source.try(:email),
212
+ source_name: source.to_s
213
+ )
214
+ end
83
215
  end
84
216
  end
@@ -0,0 +1,23 @@
1
+ - effective_resource = Effective::Resource.new(resource)
2
+ - associations = (effective_resource.has_ones + effective_resource.has_manys).reject { |association| association.options[:through].present? }
3
+ - limit = 10
4
+
5
+ %table.table.table-sm.table-striped.mt-4
6
+ %tbody
7
+ - associations.each do |association|
8
+ - collection = resource.send(association.name)
9
+ - records = (collection.respond_to?(:limit) ? collection.limit(limit).to_a : Array(collection))
10
+ - next if records.blank?
11
+
12
+ - total = (collection.respond_to?(:count) ? collection.count : records.size)
13
+ - admin_resource = (Effective::Resource.new(association.klass, namespace: :admin) rescue nil)
14
+
15
+ %tr
16
+ %th.text-nowrap= association.name.to_s.humanize
17
+ %td
18
+ - records.each do |record|
19
+ - path = (admin_resource&.action_path(:edit, record) || admin_resource&.action_path(:show, record))
20
+ %div= path.present? ? link_to(record.to_s, path) : record.to_s
21
+
22
+ - if total > records.size
23
+ %small.text-muted= "and #{total - records.size} more…"
@@ -0,0 +1,35 @@
1
+ = effective_form_with(model: [:admin, merge], engine: true) do |f|
2
+ -# User
3
+ - klass = (current_user.class)
4
+ - ajax_url = (effective_resources.users_effective_ajax_index_path unless Rails.env.test?)
5
+
6
+ .row
7
+ .col
8
+ = f.hidden_field :source_type, value: klass.name
9
+ = f.select :source_id, klass.all, ajax_url: ajax_url, hint: 'This record will be destroyed',
10
+ 'data-load-ajax-url': effective_mergery.new_admin_merge_path,
11
+ 'data-load-ajax-div': '#effective-mergery-ajax',
12
+ 'data-load-ajax-all': true
13
+ .col
14
+ = f.hidden_field :target_type, value: klass.name
15
+ = f.select :target_id, klass.all, ajax_url: ajax_url, hint: 'This record will be kept',
16
+ 'data-load-ajax-url': effective_mergery.new_admin_merge_path,
17
+ 'data-load-ajax-div': '#effective-mergery-ajax',
18
+ 'data-load-ajax-all': true
19
+
20
+ #effective-mergery-ajax
21
+ - source = f.object.source
22
+ - target = f.object.target
23
+
24
+ .row
25
+ .col-6
26
+ - if source.present?
27
+ = card(source.to_s) do
28
+ = render('admin/merges/resource', resource: source)
29
+ .col-6
30
+ - if target.present?
31
+ = card(target.to_s) do
32
+ = render('admin/merges/resource', resource: target)
33
+
34
+ - if source.present? && target.present?
35
+ = f.submit "Merge", center: true, 'data-confirm': "Are you sure you want to merge these records and destroy #{source}?"
@@ -0,0 +1,6 @@
1
+ - if resource.class.name.ends_with?("::User")
2
+ = render('admin/merges/user', user: resource)
3
+ - else
4
+ - raise("Unsupported mergable class: #{resource.class.name}")
5
+
6
+ = render('admin/merges/associations', resource: resource)
@@ -0,0 +1,3 @@
1
+ = effective_table_with(user) do |f|
2
+ = render_if_exists('users/fields_account', f: f)
3
+ = render_if_exists('users/fields_demographics', f: f)
@@ -0,0 +1 @@
1
+ = render('admin/merges/form', merge: Effective::Merge.new)
@@ -1,24 +1,3 @@
1
1
  EffectiveMergery.setup do |config|
2
-
3
- # Admin Screens Layout Settings
4
- config.layout = 'application' # All EffectiveMergery controllers will use this layout
5
-
6
- # config.layout = {
7
- # merge: 'application',
8
- # admin_merge: 'admin',
9
- # }
10
-
11
- config.admin_simple_form_options = {} # For the /admin/merge/new form
12
- # config.admin_simple_form_options = {
13
- # :html => {:class => ['form-horizontal']},
14
- # :wrapper => :horizontal_form,
15
- # :wrapper_mappings => {
16
- # :boolean => :horizontal_boolean,
17
- # :check_boxes => :horizontal_radio_and_checkboxes,
18
- # :radio_buttons => :horizontal_radio_and_checkboxes
19
- # }
20
- # }
21
-
22
- # The class names that can be merged
23
- # config.class_names = ['User']
2
+ # config.layout = { admin_merge: 'admin' }
24
3
  end
data/config/routes.rb CHANGED
@@ -1,11 +1,9 @@
1
+ Rails.application.routes.draw do
2
+ mount EffectiveMergery::Engine => '/', :as => 'effective_mergery'
3
+ end
4
+
1
5
  EffectiveMergery::Engine.routes.draw do
2
6
  namespace :admin do
3
- resources :merge, only: [:index, :new, :create] do
4
- get :attributes, on: :collection
5
- end
7
+ resources :merges, only: [:index, :new, :create]
6
8
  end
7
9
  end
8
-
9
- Rails.application.routes.draw do
10
- mount EffectiveMergery::Engine => '/', :as => 'effective_mergery'
11
- end
@@ -1,3 +1,3 @@
1
1
  module EffectiveMergery
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -5,14 +5,8 @@ require 'effective_mergery/version'
5
5
  module EffectiveMergery
6
6
 
7
7
  def self.config_keys
8
- [:layout, :admin_simple_form_options, :class_names]
8
+ [:layout]
9
9
  end
10
10
 
11
11
  include EffectiveGem
12
-
13
- # Just consider the onlies right now. sorry future matt.
14
- def self.mergables
15
- Array(class_names).map { |name| name.constantize }
16
- end
17
-
18
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_mergery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-26 00:00:00.000000000 Z
11
+ date: 2026-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,16 +16,16 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.2.0
19
+ version: 6.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 3.2.0
26
+ version: 6.0.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: effective_resources
28
+ name: effective_bootstrap
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,7 +39,21 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: coffee-rails
42
+ name: effective_datatables
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 4.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 4.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: effective_resources
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
@@ -53,13 +67,13 @@ dependencies:
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
- name: sassc
70
+ name: sqlite3
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - ">="
60
74
  - !ruby/object:Gem::Version
61
75
  version: '0'
62
- type: :runtime
76
+ type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
@@ -67,13 +81,83 @@ dependencies:
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
- name: simple_form
84
+ name: devise
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - ">="
74
88
  - !ruby/object:Gem::Version
75
89
  version: '0'
76
- type: :runtime
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: haml-rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: effective_logging
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: effective_test_bot
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: effective_developer
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
77
161
  prerelease: false
78
162
  version_requirements: !ruby/object:Gem::Requirement
79
163
  requirements:
@@ -91,15 +175,16 @@ files:
91
175
  - README.md
92
176
  - app/assets/config/effective_mergery_manifest.js
93
177
  - app/assets/javascripts/effective_mergery.js
94
- - app/assets/javascripts/effective_mergery/merge.js.coffee
178
+ - app/assets/javascripts/effective_mergery/base.js
95
179
  - app/assets/stylesheets/effective_mergery.scss
96
180
  - app/assets/stylesheets/effective_mergery/_merge.scss
97
- - app/controllers/admin/merge_controller.rb
181
+ - app/controllers/admin/merges_controller.rb
98
182
  - app/models/effective/merge.rb
99
- - app/views/admin/merge/_attributes.html.haml
100
- - app/views/admin/merge/create.html.haml
101
- - app/views/admin/merge/index.html.haml
102
- - app/views/admin/merge/new.html.haml
183
+ - app/views/admin/merges/_associations.html.haml
184
+ - app/views/admin/merges/_form.html.haml
185
+ - app/views/admin/merges/_resource.html.haml
186
+ - app/views/admin/merges/_user.html.haml
187
+ - app/views/admin/merges/index.html.haml
103
188
  - config/effective_mergery.rb
104
189
  - config/routes.rb
105
190
  - lib/effective_mergery.rb
@@ -125,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
210
  - !ruby/object:Gem::Version
126
211
  version: '0'
127
212
  requirements: []
128
- rubygems_version: 3.3.7
213
+ rubygems_version: 3.5.9
129
214
  signing_key:
130
215
  specification_version: 4
131
216
  summary: Deep merge any two Active Record objects.
@@ -1,20 +0,0 @@
1
- loadAttributes = (event) ->
2
- $obj = $(event.currentTarget)
3
-
4
- id = parseInt($obj.val())
5
- type = $obj.closest('form').find("input[name='effective_merge[type]']").val()
6
-
7
- selector = if ($obj.attr('name') == 'effective_merge[source_id]') then '.source' else '.target'
8
- content = $obj.closest('form').find(selector).first()
9
-
10
- url = "/admin/merge/attributes?id=#{id}&type=#{type}"
11
-
12
- if id != undefined && id != NaN && id > 0 && type.length > 0
13
- content.load(url, (response, status, xhr) =>
14
- content.html('<p>This item is unavailable (ajax error)</p>') if status == 'error'
15
- )
16
- else
17
- content.html('')
18
-
19
- $(document).on 'change', "select[name='effective_merge[source_id]']", (event) -> loadAttributes(event)
20
- $(document).on 'change', "select[name='effective_merge[target_id]']", (event) -> loadAttributes(event)
@@ -1,75 +0,0 @@
1
- module Admin
2
- class MergeController < ApplicationController
3
- before_action(:authenticate_user!) if defined?(Devise)
4
- before_action { EffectiveResources.authorize!(self, :admin, :effective_mergery) }
5
-
6
- include Effective::CrudController
7
-
8
- if (config = EffectiveMergery.layout)
9
- layout(config.kind_of?(Hash) ? config[:admin] : config)
10
- end
11
-
12
- def index
13
- @page_title = 'Merges'
14
- end
15
-
16
- def new
17
- @page_title = 'New Merge'
18
-
19
- begin
20
- @merge = Effective::Merge.new(type: params[:type])
21
- @merge.validate_klass!
22
- rescue => e
23
- flash[:danger] = "An error occurred while loading #{@merge}: #{e.message}"
24
- redirect_to effective_mergery.admin_merge_index_path
25
- end
26
- end
27
-
28
- def create
29
- @merge = Effective::Merge.new(merge_params)
30
-
31
- if @merge.save
32
- @page_title = 'Successful Merge'
33
- flash[:success] = "Successfully merged #{@merge}"
34
-
35
- if defined?(EffectiveLogging)
36
- EffectiveLogger.success(
37
- "Merged #{@merge} - #{@merge.source.respond_to?(:to_s_verbose) ? @merge.source.to_s_verbose : @merge.source.to_s}",
38
- user: (current_user rescue false),
39
- associated: @merge.target,
40
- source_id: @merge.source_id,
41
- target_id: @merge.target_id,
42
- mergable_type: @merge.type,
43
- source_email: (@merge.source.email if @merge.source.respond_to?(:email)),
44
- source_name: (@merge.source.full_name if @merge.source.respond_to?(:full_name))
45
- )
46
- end
47
-
48
- @merge.target = @merge.collection.find(@merge.target_id)
49
- else
50
- @page_title = 'New Merge'
51
- flash.now[:danger] = "Unable to merge #{@merge}: #{@merge.errors.full_messages.to_sentence}"
52
-
53
- render :new
54
- end
55
- end
56
-
57
- # This is the AJAX request for the object's attributes
58
- def attributes
59
- object = Effective::Merge.new(type: params[:type]).collection.find(params[:id])
60
-
61
- if object.present?
62
- render partial: '/admin/merge/attributes', locals: { resource: object }
63
- else
64
- render body: '<p>None Available</p>'
65
- end
66
- end
67
-
68
- private
69
-
70
- def merge_params
71
- params.require(:effective_merge).permit!
72
- end
73
-
74
- end
75
- end
@@ -1,3 +0,0 @@
1
- = tableize_hash(resource.attributes, table: 'table table-merge table-attributes')
2
- %hr
3
- = tableize_hash(Effective::Resource.new(resource).instance_attributes.except(:attributes), table: 'table table-merge', sub_table: 'table table-merge table-subtable')
@@ -1,11 +0,0 @@
1
- %h1.effective-admin-heading= @page_title
2
-
3
- .row
4
- .col-sm-2
5
- .col-sm-8
6
- %p= @merge.target
7
- = render partial: '/admin/merge/attributes', locals: { resource: @merge.target }
8
- .col-sm-2
9
-
10
- .text-center
11
- %p= link_to "Merge Another #{@merge}", effective_mergery.new_admin_merge_path(type: @merge.type), class: 'btn btn-primary'
@@ -1,10 +0,0 @@
1
- %h1.effective-admin-heading= @page_title
2
-
3
- %p Please select one of the following to merge:
4
-
5
- - if EffectiveMergery.mergables.blank?
6
- %p There is nothing available to merge.
7
- - else
8
- %ul
9
- - EffectiveMergery.mergables.each do |name|
10
- %li= link_to name, effective_mergery.new_admin_merge_path(type: name)
@@ -1,29 +0,0 @@
1
- %h1.effective-admin-heading= @page_title
2
-
3
- %p Please select a source and target. The source record's associated data, but not its attributes, will be merged into the target. The source record will then be destroyed.
4
-
5
- .effective-merge
6
- = simple_form_for([:admin, @merge], url: effective_mergery.admin_merge_index_path) do |f|
7
- = f.input :type, as: :hidden
8
-
9
- .row
10
- .col-sm-6
11
- = f.input :source_id, as: (defined?(EffectiveFormInputs) ? :effective_select : :select),
12
- collection: f.object.form_collection,
13
- hint: 'This record will be destroyed'
14
- .col-sm-6
15
- = f.input :target_id, as: (defined?(EffectiveFormInputs) ? :effective_select : :select),
16
- collection: f.object.form_collection,
17
- hint: 'This record will be kept'
18
-
19
- .row
20
- .col-sm-6.source
21
- - if f.object.source.present?
22
- = render partial: '/admin/merge/attributes', locals: { resource: f.object.source }
23
-
24
- .col-sm-6.target
25
- - if f.object.target.present?
26
- = render partial: '/admin/merge/attributes', locals: { resource: f.object.target }
27
-
28
- .form-actions
29
- = f.button :submit, "Merge #{@merge}", data: { disable_with: 'Merging...', confirm: "Really merge? The source record will be destroyed."}