effective_mergery 0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 185eb0a320784bf5e9f6cc24b1db3296761469d8
4
+ data.tar.gz: 79ed4931f61a2c803e50e8b132d9bae2d0060c97
5
+ SHA512:
6
+ metadata.gz: 29c4448f5df36346b5567795a9548ed8f1bcef9d03d7e83142c479653a26027b3f940cc234b603ec54278988deb14ef1fdfeaf24caf654a3856d1998e04ffab1
7
+ data.tar.gz: bd52345540fcfd32a37751d8d25e70857158a9ccacd64332850f827988475aa28573dcbc9753b318a8c3eea8451f4c2630f47185baec4b958c049bf07cdea0e8
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Code and Effect Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,68 @@
1
+ # Effective Mergery
2
+
3
+ Merge any two Active Record objects, along with all associated objects, into one record.
4
+
5
+ ## Getting Started
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'effective_mergery'
11
+ ```
12
+
13
+ Run the bundle command to install it:
14
+
15
+ ```console
16
+ bundle install
17
+ ```
18
+
19
+ Then run the generator:
20
+
21
+ ```ruby
22
+ rails generate effective_mergery:install
23
+ ```
24
+
25
+ The generator will install an initializer which describes all configuration options.
26
+
27
+ Require the javascript on the asset pipeline by adding the following to your application.js:
28
+
29
+ ```ruby
30
+ //= require effective_mergery
31
+ ```
32
+
33
+ Require the stylesheet on the asset pipeline by adding the following to your application.css:
34
+
35
+ ```ruby
36
+ *= require effective_mergery
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ Visit `/admin/merge` and select an object type to merge.
42
+
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')
46
+ ```
47
+
48
+ ## Permissions
49
+
50
+ Add the following permissions (using CanCan):
51
+
52
+ ```ruby
53
+ can :admin, :effective_mergery
54
+ ```
55
+
56
+ ## License
57
+
58
+ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
59
+
60
+ ## Contributing
61
+
62
+ 1. Fork it
63
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
64
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
65
+ 4. Push to the branch (`git push origin my-new-feature`)
66
+ 5. Bonus points for test coverage
67
+ 6. Create new Pull Request
68
+
@@ -0,0 +1,2 @@
1
+ //= require_tree ./effective_mergery
2
+
@@ -0,0 +1,20 @@
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)
@@ -0,0 +1 @@
1
+ @import 'effective_mergery/merge';
@@ -0,0 +1,5 @@
1
+ .effective-merge {
2
+ .source {
3
+ .table-attributes { opacity: 0.4; }
4
+ }
5
+ }
@@ -0,0 +1,63 @@
1
+ module Admin
2
+ class MergeController < ApplicationController
3
+ before_action :authenticate_user! if defined?(Devise)
4
+
5
+ layout (EffectiveMergery.layout.kind_of?(Hash) ? EffectiveMergery.layout[:admin_merge] : EffectiveMergery.layout)
6
+
7
+ def index
8
+ @page_title = 'Merges'
9
+ EffectiveMergery.authorized?(self, :admin, :effective_mergery)
10
+ end
11
+
12
+ def new
13
+ @page_title = 'New Merge'
14
+ EffectiveMergery.authorized?(self, :admin, :effective_mergery)
15
+
16
+ begin
17
+ @merge = Effective::Merge.new(type: params[:type])
18
+ @merge.validate_klass!
19
+ rescue => e
20
+ flash[:danger] = "An error occurred while loading #{@merge}: #{e.message}"
21
+ redirect_to effective_mergery.admin_merge_index_path
22
+ end
23
+ end
24
+
25
+ def create
26
+ EffectiveMergery.authorized?(self, :admin, :effective_mergery)
27
+
28
+ @merge = Effective::Merge.new(merge_params)
29
+
30
+ if @merge.save
31
+ @page_title = 'Successful Merge'
32
+ flash[:success] = "Successfully merged #{@merge}"
33
+
34
+ @merge.target = @merge.collection.find(@merge.target_id)
35
+ else
36
+ @page_title = 'New Merge'
37
+ flash.now[:danger] = "Unable to merge #{@merge}: #{@merge.errors.full_messages.to_sentence}"
38
+
39
+ render :new
40
+ end
41
+ end
42
+
43
+ # This is the AJAX request for the object's attributes
44
+ def attributes
45
+ EffectiveMergery.authorized?(self, :admin, :effective_mergery)
46
+
47
+ object = Effective::Merge.new(type: params[:type]).collection.find(params[:id])
48
+
49
+ if object.present?
50
+ render partial: '/admin/merge/attributes', locals: { resource: object }
51
+ else
52
+ render body: '<p>None Available</p>'
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def merge_params
59
+ params.require(:effective_merge).permit(:type, :source_id, :target_id)
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ unless defined?(Effective::AccessDenied)
2
+ module Effective
3
+ class AccessDenied < StandardError
4
+ attr_reader :action, :subject
5
+
6
+ def initialize(message = nil, action = nil, subject = nil)
7
+ @message = message
8
+ @action = action
9
+ @subject = subject
10
+ end
11
+
12
+ def to_s
13
+ @message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,91 @@
1
+ module Effective
2
+ class Merge
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :type, :source, :source_id, :target, :target_id
6
+
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
+
10
+ validates :type, presence: true, inclusion: { in: EffectiveMergery.mergables, message: 'must be a mergable type' }
11
+ validates :source_id, presence: true, unless: -> { source.present? }
12
+ validates :target_id, presence: true, unless: -> { target.present? }
13
+
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
16
+ end
17
+
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
+
21
+ def to_s
22
+ return 'New Merge' unless type
23
+ type.downcase
24
+ end
25
+
26
+ def save(validate: true)
27
+ return false unless valid?
28
+ (merge!(validate: validate) rescue false)
29
+ end
30
+
31
+ def save!(validate: true)
32
+ raise 'is invalid' unless valid?
33
+ merge!(validate: validate)
34
+ end
35
+
36
+ def collection
37
+ @collection ||= (klass.respond_to?(:sorted) ? klass.sorted : klass.all)
38
+ end
39
+
40
+ def klass
41
+ @klass ||= type.safe_constantize
42
+ end
43
+
44
+ # This is called on Admin::Merges#new
45
+ def validate_klass!
46
+ raise "type can't be blank" unless type.present?
47
+ raise 'type must be a mergable type' unless EffectiveMergery.mergables.include?(type)
48
+ raise "invalid ActiveRecord klass" unless klass
49
+ raise "invalid ActiveRecord collection" unless collection.kind_of?(ActiveRecord::Relation)
50
+ true
51
+ end
52
+
53
+ private
54
+
55
+ def merge!(validate: true)
56
+ resource = Effective::Resource.new(source)
57
+
58
+ klass.transaction do
59
+ begin
60
+
61
+ # Merge associations
62
+ (resource.has_ones + resource.has_manys + resource.nested_resources).compact.each do |association|
63
+ next if association.options[:through].present?
64
+
65
+ Array(source.send(association.name)).each do |obj|
66
+ obj.assign_attributes(association.foreign_key => target.id)
67
+
68
+ unless obj.save(validate: validate)
69
+ raise "associated #{obj.class.name}<#{obj.to_param}> is invalid: #{obj.errors.full_messages.to_sentence}"
70
+ end
71
+ end
72
+ end
73
+
74
+ source.destroy!
75
+
76
+ unless target.save(validate: validate)
77
+ raise "merged #{target.class.name} is invalid: #{target.errors.full_messages.to_sentence}"
78
+ end
79
+
80
+ return true
81
+ rescue => e
82
+ self.errors.add(:base, e.message)
83
+ raise ActiveRecord::Rollback
84
+ end
85
+ end
86
+
87
+ false
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ = tableize_hash(resource.attributes, table: 'table table-attributes')
2
+ %hr
3
+ = tableize_hash(Effective::Resource.new(resource).instance_attributes.except(:attributes))
@@ -0,0 +1,11 @@
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'
@@ -0,0 +1,10 @@
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)
@@ -0,0 +1,29 @@
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], (EffectiveOrders.admin_simple_form_options || {}).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.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.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
+ .text-center
29
+ = f.button :submit, "Merge #{@merge}", data: { disable_with: 'Merging...', confirm: "Really merge? The source record will be destroyed."}
@@ -0,0 +1,48 @@
1
+ EffectiveMergery.setup do |config|
2
+ # Authorization Method
3
+ #
4
+ # This method is called by all controller actions with the appropriate action and resource
5
+ # If the method returns false, an Effective::AccessDenied Error will be raised (see README.md for complete info)
6
+ #
7
+ # Use via Proc (and with CanCan):
8
+ # config.authorization_method = Proc.new { |controller, action, resource| can?(action, resource) }
9
+ #
10
+ # Use via custom method:
11
+ # config.authorization_method = :my_authorization_method
12
+ #
13
+ # And then in your application_controller.rb:
14
+ #
15
+ # def my_authorization_method(action, resource)
16
+ # current_user.is?(:admin)
17
+ # end
18
+ #
19
+ # Or disable the check completely:
20
+ # config.authorization_method = false
21
+ config.authorization_method = Proc.new { |controller, action, resource| authorize!(action, resource) } # CanCanCan
22
+
23
+ # Admin Screens Layout Settings
24
+ config.layout = 'application' # All EffectiveMergery controllers will use this layout
25
+
26
+ # config.layout = {
27
+ # merge: 'application',
28
+ # admin_merge: 'admin',
29
+ # }
30
+
31
+ config.admin_simple_form_options = {} # For the /admin/merge/new form
32
+ # config.admin_simple_form_options = {
33
+ # :html => {:class => ['form-horizontal']},
34
+ # :wrapper => :horizontal_form,
35
+ # :wrapper_mappings => {
36
+ # :boolean => :horizontal_boolean,
37
+ # :check_boxes => :horizontal_radio_and_checkboxes,
38
+ # :radio_buttons => :horizontal_radio_and_checkboxes
39
+ # }
40
+ # }
41
+
42
+ # Allow merges for only the following ActiveRecord class names:
43
+ # config.only = ['User']
44
+
45
+ # Allow merges on all ActiveRecord classes, except the following:
46
+ # config.except = []
47
+
48
+ end
@@ -0,0 +1,11 @@
1
+ EffectiveMergery::Engine.routes.draw do
2
+ namespace :admin do
3
+ resources :merge, only: [:index, :new, :create] do
4
+ get :attributes, on: :collection
5
+ end
6
+ end
7
+ end
8
+
9
+ Rails.application.routes.draw do
10
+ mount EffectiveMergery::Engine => '/', :as => 'effective_mergery'
11
+ end
@@ -0,0 +1,44 @@
1
+ require 'effective_resources'
2
+ require 'effective_mergery/engine'
3
+ require 'effective_mergery/version'
4
+
5
+ module EffectiveMergery
6
+
7
+ # The following are all valid config keys
8
+ mattr_accessor :authorization_method
9
+ mattr_accessor :layout
10
+ mattr_accessor :admin_simple_form_options
11
+
12
+ mattr_accessor :only
13
+ mattr_accessor :except
14
+
15
+ def self.setup
16
+ yield self
17
+ end
18
+
19
+ def self.authorized?(controller, action, resource)
20
+ if authorization_method.respond_to?(:call) || authorization_method.kind_of?(Symbol)
21
+ raise Effective::AccessDenied.new() unless (controller || self).instance_exec(controller, action, resource, &authorization_method)
22
+ end
23
+ true
24
+ end
25
+
26
+ def self.mergables
27
+ @mergables ||= (
28
+ Rails.application.eager_load! unless Rails.configuration.cache_classes
29
+
30
+ blacklist = ['Delayed::', 'Effective::', 'ActiveRecord::', 'ApplicationRecord']
31
+
32
+ ActiveRecord::Base.descendants.map { |obj| obj.name }.tap do |names|
33
+ names.reject! { |name| blacklist.any? { |b| name.start_with?(b) } }
34
+
35
+ if (onlies = Array(only)).present?
36
+ names.select! { |name| onlies.include?(name) }
37
+ elsif (excepts = Array(except)).present?
38
+ names.reject! { |name| excepts.include?(name) }
39
+ end
40
+ end.sort
41
+ )
42
+ end
43
+
44
+ end
@@ -0,0 +1,13 @@
1
+ module EffectiveMergery
2
+ class Engine < ::Rails::Engine
3
+ engine_name 'effective_mergery'
4
+
5
+ config.autoload_paths += Dir["#{config.root}/lib/"]
6
+
7
+ # Set up our default configuration options.
8
+ initializer "effective_mergery.defaults", before: :load_config_initializers do |app|
9
+ eval File.read("#{config.root}/config/effective_mergery.rb")
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module EffectiveMergery
2
+ VERSION = '0.1'.freeze
3
+ end
@@ -0,0 +1,16 @@
1
+ module EffectiveMergery
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ desc 'Creates an EffectiveMergery initializer in your application.'
7
+
8
+ source_root File.expand_path('../../templates', __FILE__)
9
+
10
+ def copy_initializer
11
+ template ('../' * 3) + 'config/effective_mergery.rb', 'config/initializers/effective_mergery.rb'
12
+ end
13
+
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: effective_mergery
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Code and Effect
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: effective_resources
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: coffee-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sass-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simple_form
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Deep merge any two Active Record objects.
84
+ email:
85
+ - info@codeandeffect.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - MIT-LICENSE
91
+ - README.md
92
+ - app/assets/javascripts/effective_mergery.js
93
+ - app/assets/javascripts/effective_mergery/merge.js.coffee
94
+ - app/assets/stylesheets/effective_mergery.scss
95
+ - app/assets/stylesheets/effective_mergery/_merge.scss
96
+ - app/controllers/admin/merge_controller.rb
97
+ - app/models/effective/access_denied.rb
98
+ - 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
103
+ - config/effective_mergery.rb
104
+ - config/routes.rb
105
+ - lib/effective_mergery.rb
106
+ - lib/effective_mergery/engine.rb
107
+ - lib/effective_mergery/version.rb
108
+ - lib/generators/effective_mergery/install_generator.rb
109
+ homepage: https://github.com/code-and-effect/effective_mergery
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 2.4.5.1
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Deep merge any two Active Record objects.
133
+ test_files: []