effective_mergery 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []