draft_punk 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/activerecord/activerecord_class_methods.rb +52 -0
- data/lib/{activerecord_instance_methods.rb → activerecord/activerecord_instance_methods.rb} +16 -7
- data/lib/{draft_diff_instance_methods.rb → activerecord/draft_diff_instance_methods.rb} +0 -0
- data/lib/activerecord/macros.rb +70 -0
- data/lib/{previous_version_instance_methods.rb → activerecord/previous_version_instance_methods.rb} +0 -0
- data/lib/activerecord/setup_model.rb +116 -0
- data/lib/draft_punk.rb +3 -10
- data/lib/draft_punk/version.rb +1 -1
- data/lib/{helper_methods.rb → helpers/helper_methods.rb} +0 -0
- metadata +9 -7
- data/lib/activerecord_class_methods.rb +0 -212
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d72097293299128db20d9aefa7a69a4722eb1a10
|
4
|
+
data.tar.gz: abd611675c762f6ac56e51b908489fb11260cc26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b42ee587d685ad6a454e8aea9b42b014fad40b9aeecfa0b9fa92500c1639bf1bde49a73ebe03bf27bf506fa60bd09d0116faa3ee00f29e986794a5a5ff191bb
|
7
|
+
data.tar.gz: 303f073323eb193f5e5a87aed1546abe3251ca09e7456123ac6c98efdc847c30466a5abb180c8228c463743197ee57de1b5fbbd35ffb197b4f9ad24e2f0dd33a
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module DraftPunk
|
2
|
+
module Model
|
3
|
+
module ActiveRecordClassMethods
|
4
|
+
# List of association names this model is configured to have drafts for
|
5
|
+
# @return [Array]
|
6
|
+
def draft_target_associations
|
7
|
+
targets = if const_defined?(:CREATES_NESTED_DRAFTS_FOR) && const_get(:CREATES_NESTED_DRAFTS_FOR).is_a?(Array)
|
8
|
+
const_get(:CREATES_NESTED_DRAFTS_FOR).compact
|
9
|
+
else
|
10
|
+
default_draft_target_associations
|
11
|
+
end.map(&:to_sym)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Whether this model is configured to track the approved version of a draft object.
|
15
|
+
# This will be true if the model has an approved_version_id column
|
16
|
+
#
|
17
|
+
# @return (Boolean)
|
18
|
+
def tracks_approved_version?
|
19
|
+
column_names.include? 'approved_version_id'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Whether this model is configured to store previously-approved versions of the model.
|
23
|
+
# This will be true if the model has an current_approved_version_id column
|
24
|
+
#
|
25
|
+
# @return (Boolean)
|
26
|
+
def tracks_approved_version_history?
|
27
|
+
column_names.include?('current_approved_version_id')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Callback runs after save to set the approved version id on the draft object.
|
31
|
+
#
|
32
|
+
# @return (true)
|
33
|
+
def set_approved_version_id_callback
|
34
|
+
lambda do |live_obj, draft_obj|
|
35
|
+
draft_obj.approved_version_id = live_obj.id if draft_obj.respond_to?(:approved_version_id)
|
36
|
+
draft_obj.temporary_approved_object = live_obj
|
37
|
+
true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected #################################################################
|
42
|
+
|
43
|
+
def default_draft_target_associations
|
44
|
+
reflect_on_all_associations.select do |reflection|
|
45
|
+
DraftPunk.is_relevant_association_type?(reflection) &&
|
46
|
+
!reflection.name.in?(%i(draft approved_version)) &&
|
47
|
+
reflection.class_name != name # Self referential associations!!! Don't do them!
|
48
|
+
end.map{|r| r.name.downcase.to_sym }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -59,13 +59,18 @@ module DraftPunk
|
|
59
59
|
@live_version = get_approved_version
|
60
60
|
@draft_version = editable_version
|
61
61
|
return unless changes_require_approval? && @draft_version.is_draft? # No-op. ie. the live version is in a state that doesn't require approval.
|
62
|
+
@live_version.instance_variable_set :@publishing_draft, true
|
62
63
|
transaction do
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
64
|
+
begin
|
65
|
+
create_historic_version_of_approved_object if tracks_approved_version_history?
|
66
|
+
save_attribute_changes_and_belongs_to_assocations_from_draft
|
67
|
+
update_has_many_and_has_one_associations_from_draft
|
68
|
+
# We have to destroy the draft this since we moved all the draft's has_many associations to @live_version. If you call "editable_version" later, it'll build the draft.
|
69
|
+
# We destroy_all in case extra drafts are in the database. Extra drafts can potentially be created due to race conditions in the application.
|
70
|
+
self.class.unscoped.where(approved_version_id: @live_version.id).destroy_all
|
71
|
+
ensure
|
72
|
+
@live_version.remove_instance_variable :@publishing_draft
|
73
|
+
end
|
69
74
|
end
|
70
75
|
@live_version = self.class.find(@live_version.id)
|
71
76
|
end
|
@@ -90,6 +95,10 @@ module DraftPunk
|
|
90
95
|
self.class.tracks_approved_version_history?
|
91
96
|
end
|
92
97
|
|
98
|
+
def publishing_draft?
|
99
|
+
!!@publishing_draft
|
100
|
+
end
|
101
|
+
|
93
102
|
protected #################################################################
|
94
103
|
|
95
104
|
def get_draft
|
@@ -190,7 +199,7 @@ module DraftPunk
|
|
190
199
|
# @return [Boolean] whether the current ActiveRecord object has a draft version
|
191
200
|
def has_draft?
|
192
201
|
raise DraftPunk::ApprovedVersionIdError unless respond_to?(:approved_version_id)
|
193
|
-
draft.present?
|
202
|
+
draft.present? && !publishing_draft?
|
194
203
|
end
|
195
204
|
|
196
205
|
# @return [Boolean] whether the current ActiveRecord object is a previously-approved
|
File without changes
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'setup_model'
|
2
|
+
|
3
|
+
module DraftPunk
|
4
|
+
module Model
|
5
|
+
module Macros
|
6
|
+
# Call this method in your model to setup approval. It will recursively apply to its associations,
|
7
|
+
# thus does not need to be explicity called on its associated models (and will error if you try).
|
8
|
+
#
|
9
|
+
# This model must have an approved_version_id column (Integer), which will be used to track its draft
|
10
|
+
#
|
11
|
+
# For instance, your Business model:
|
12
|
+
# class Business << ActiveRecord::Base
|
13
|
+
# has_many :employees
|
14
|
+
# has_many :images
|
15
|
+
# has_one :address
|
16
|
+
# has_many :vending_machines
|
17
|
+
#
|
18
|
+
# requires_approval # When creating a business's draft, :employees, :vending_machines, :images, and :address will all have drafts created
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Optionally, specify which associations which the user will edit - associations which should have a draft created
|
22
|
+
# by defining a CREATES_NESTED_DRAFTS_FOR constant for this model.
|
23
|
+
#
|
24
|
+
# CREATES_NESTED_DRAFTS_FOR = [:address] # When creating a business's draft, only :address will have drafts created
|
25
|
+
#
|
26
|
+
# To disable drafts for all assocations for this model, simply pass an empty array:
|
27
|
+
# by defining a CREATES_NESTED_DRAFTS_FOR constant for this model.
|
28
|
+
# CREATES_NESTED_DRAFTS_FOR = [] # When creating a business's draft, no associations will have drafts created
|
29
|
+
#
|
30
|
+
# WARNING: If you are setting associations via accepts_nested_attributes all changes to the draft, including associations, get set on the
|
31
|
+
# draft object (as expected). If your form includes associated objects which weren't defined in requires_approval, your save will fail since
|
32
|
+
# the draft object doesn't HAVE those associations to update! In this case, you should probably add that association to the
|
33
|
+
# +associations+ param here.
|
34
|
+
#
|
35
|
+
# If you want your draft associations to track their live version, add an :approved_version_id column
|
36
|
+
# to each association's table. You'll be able to access that associated object's live version, just
|
37
|
+
# like you can with the original model which called requires_approval.
|
38
|
+
#
|
39
|
+
# @param nullify [Array] A list of attributes on this model to set to null on the draft when it is created. For
|
40
|
+
# instance, the _id_ and _created_at_ columns are nullified by default, since you don't want Rails to try to
|
41
|
+
# persist those on the draft.
|
42
|
+
# @param set_default_scope [Boolean] If true, set a default scope on this model for the approved scope; only approved objects
|
43
|
+
# will be returned in ActiveRecord queries, unless you call Model.unscoped
|
44
|
+
# @param allow_previous_versions_to_be_changed [Boolean] If the model tracks approved version history, and this
|
45
|
+
# param is false, previously-approved versions of the object cannot be saved (via a before_save callback )
|
46
|
+
# @param associations [Array] Use internally; set associations to create drafts for in the CREATES_NESTED_DRAFTS_FOR constant
|
47
|
+
# @return true
|
48
|
+
def requires_approval(associations: [], nullify: [], set_default_scope: false, allow_previous_versions_to_be_changed: true)
|
49
|
+
DraftPunk::SetupModel.new(self,
|
50
|
+
associations: associations,
|
51
|
+
nullify: nullify,
|
52
|
+
set_default_scope: set_default_scope,
|
53
|
+
allow_previous_versions_to_be_changed: allow_previous_versions_to_be_changed
|
54
|
+
).make_approvable!
|
55
|
+
end
|
56
|
+
|
57
|
+
# This will generally be only used in testing scenarios, in cases when requires_approval need to be
|
58
|
+
# called multiple times. Only the usage for that use case is supported. Use at your own risk for other
|
59
|
+
# use cases.
|
60
|
+
def disable_approval!
|
61
|
+
send(:remove_const, :DRAFT_PUNK_IS_SETUP) if const_defined? :DRAFT_PUNK_IS_SETUP
|
62
|
+
send(:remove_const, :DRAFT_NULLIFY_ATTRIBUTES) if const_defined? :DRAFT_NULLIFY_ATTRIBUTES
|
63
|
+
send(:remove_const, :ALLOW_PREVIOUS_VERSIONS_TO_BE_CHANGED) if const_defined? :ALLOW_PREVIOUS_VERSIONS_TO_BE_CHANGED
|
64
|
+
fresh_amoeba do
|
65
|
+
disable
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/{previous_version_instance_methods.rb → activerecord/previous_version_instance_methods.rb}
RENAMED
File without changes
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module DraftPunk
|
2
|
+
|
3
|
+
def self.is_relevant_association_type?(activerecord_reflection)
|
4
|
+
activerecord_reflection.macro.in? Amoeba::Config::DEFAULTS[:known_macros]
|
5
|
+
end
|
6
|
+
|
7
|
+
class SetupModel
|
8
|
+
require_relative 'activerecord_class_methods'
|
9
|
+
require_relative 'activerecord_instance_methods'
|
10
|
+
require_relative 'draft_diff_instance_methods'
|
11
|
+
require_relative 'previous_version_instance_methods'
|
12
|
+
|
13
|
+
def initialize(top_level_model, associations: [], nullify: [], set_default_scope: false, allow_previous_versions_to_be_changed: true)
|
14
|
+
@top_level_model, @associations, @nullify = top_level_model, associations, nullify
|
15
|
+
@set_default_scope, @allow_previous_versions_to_be_changed = set_default_scope, allow_previous_versions_to_be_changed
|
16
|
+
end
|
17
|
+
|
18
|
+
def make_approvable!
|
19
|
+
return unless model_table_exists? # Short circuits if you're migrating
|
20
|
+
@top_level_model.extend Model::ActiveRecordClassMethods
|
21
|
+
|
22
|
+
@associations = @top_level_model.draft_target_associations if @associations.empty?
|
23
|
+
set_valid_associations @top_level_model, @associations
|
24
|
+
|
25
|
+
raise DraftPunk::ConfigurationError, "Cannot call requires_approval multiple times for #{@top_level_model.name}" if @top_level_model.const_defined?(:DRAFT_PUNK_IS_SETUP)
|
26
|
+
@top_level_model.const_set :DRAFT_NULLIFY_ATTRIBUTES, Array(@nullify).flatten.freeze
|
27
|
+
@top_level_model.amoeba do
|
28
|
+
nullify @nullify
|
29
|
+
# Note that the amoeba associations and customize options are being set in setup_associations_and_scopes_for
|
30
|
+
end
|
31
|
+
setup_amoeba_for @top_level_model, set_default_scope: @set_default_scope, allow_previous_versions_to_be_changed: @allow_previous_versions_to_be_changed
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
private #####################################################################
|
36
|
+
|
37
|
+
def model_table_exists?(table_name=@top_level_model.table_name)
|
38
|
+
if ActiveRecord::Base.connection.respond_to? :data_source_exists?
|
39
|
+
ActiveRecord::Base.connection.data_source_exists?(table_name)
|
40
|
+
else
|
41
|
+
ActiveRecord::Base.connection.table_exists?(table_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def setup_amoeba_for(target_model, options={})
|
46
|
+
return if target_model.const_defined?(:DRAFT_PUNK_IS_SETUP)
|
47
|
+
target_model.extend Model::ActiveRecordClassMethods
|
48
|
+
target_associations = target_model.draft_target_associations
|
49
|
+
target_associations = set_valid_associations(target_model, target_associations)
|
50
|
+
target_model.amoeba do
|
51
|
+
enable
|
52
|
+
include_associations target_model.const_get(:DRAFT_VALID_ASSOCIATIONS) unless target_model.const_get(:DRAFT_VALID_ASSOCIATIONS).empty?
|
53
|
+
customize target_model.set_approved_version_id_callback
|
54
|
+
end
|
55
|
+
target_model.const_set :DRAFT_PUNK_IS_SETUP, true.freeze
|
56
|
+
|
57
|
+
setup_associations_and_scopes_for target_model, **options
|
58
|
+
setup_draft_association_persistance_for_children_of target_model, target_associations
|
59
|
+
end
|
60
|
+
|
61
|
+
def setup_associations_and_scopes_for(target_model, set_default_scope: false, allow_previous_versions_to_be_changed: true)
|
62
|
+
target_model.send :include, Model::InstanceInterrogators unless target_model.method_defined?(:has_draft?)
|
63
|
+
target_model.send :attr_accessor, :temporary_approved_object
|
64
|
+
target_model.send :before_create, :before_create_draft if target_model.method_defined?(:before_create_draft)
|
65
|
+
|
66
|
+
target_model.const_set :ALLOW_PREVIOUS_VERSIONS_TO_BE_CHANGED, allow_previous_versions_to_be_changed.freeze
|
67
|
+
if target_model.tracks_approved_version_history?
|
68
|
+
target_model.belongs_to :current_approved_version, class_name: target_model.name, optional: true
|
69
|
+
target_model.has_many :previous_versions, -> { order(id: :desc) }, class_name: target_model.name, foreign_key: :current_approved_version_id
|
70
|
+
target_model.before_update :prevent_previous_versions_from_saving
|
71
|
+
target_model.send :include, Model::PreviousVersionInstanceMethods
|
72
|
+
end
|
73
|
+
|
74
|
+
if set_default_scope
|
75
|
+
target_model.default_scope -> { approved }
|
76
|
+
end
|
77
|
+
|
78
|
+
return if target_model.reflect_on_association(:approved_version) || !target_model.column_names.include?('approved_version_id')
|
79
|
+
target_model.send :include, Model::ActiveRecordInstanceMethods
|
80
|
+
target_model.send :include, Model::DraftDiffInstanceMethods
|
81
|
+
target_model.belongs_to :approved_version, class_name: target_model.name, optional: true
|
82
|
+
target_model.scope :approved, -> { where("#{target_model.quoted_table_name}.approved_version_id IS NULL") }
|
83
|
+
target_model.has_one :draft, -> { unscope(where: :approved) }, class_name: target_model.name, foreign_key: :approved_version_id
|
84
|
+
target_model.scope :draft, -> { unscoped.where("#{target_model.quoted_table_name}.approved_version_id IS NOT NULL") }
|
85
|
+
end
|
86
|
+
|
87
|
+
def setup_draft_association_persistance_for_children_of(target_model, target_associations=nil)
|
88
|
+
target_associations = target_model.draft_target_associations unless target_associations
|
89
|
+
target_reflections = target_associations.map do |assoc|
|
90
|
+
reflection = target_model.reflect_on_association(assoc.to_sym)
|
91
|
+
reflection.presence || (raise DraftPunk::ConfigurationError, "#{name} includes invalid association (#{assoc})")
|
92
|
+
end
|
93
|
+
target_reflections.select{|r| DraftPunk.is_relevant_association_type?(r) }.each do |assoc|
|
94
|
+
setup_amoeba_for assoc.klass
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Rejects the associations if the table hasn't been defined yet. This happens when
|
99
|
+
# running migrations which add that association's table.
|
100
|
+
def set_valid_associations(target_model, associations)
|
101
|
+
return target_model.const_get(:DRAFT_VALID_ASSOCIATIONS) if target_model.const_defined?(:DRAFT_VALID_ASSOCIATIONS)
|
102
|
+
associations = associations.map(&:to_sym)
|
103
|
+
valid_assocations = associations.select do |assoc|
|
104
|
+
reflection = target_model.reflect_on_association(assoc)
|
105
|
+
if reflection
|
106
|
+
table_name = reflection.klass.table_name
|
107
|
+
model_table_exists?(table_name)
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
target_model.const_set :DRAFT_VALID_ASSOCIATIONS, valid_assocations.freeze
|
113
|
+
valid_assocations
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/draft_punk.rb
CHANGED
@@ -1,15 +1,9 @@
|
|
1
1
|
require 'draft_punk/version'
|
2
2
|
require 'amoeba'
|
3
|
-
require '
|
4
|
-
require 'helper_methods'
|
3
|
+
require 'activerecord/macros'
|
4
|
+
require 'helpers/helper_methods'
|
5
5
|
|
6
6
|
module DraftPunk
|
7
|
-
module Model
|
8
|
-
def self.included(base)
|
9
|
-
base.send :extend, ActiveRecordClassMethods
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
7
|
class ConfigurationError < RuntimeError
|
14
8
|
def initialize(message)
|
15
9
|
@caller = caller[0]
|
@@ -46,9 +40,8 @@ module DraftPunk
|
|
46
40
|
"the draft failed to be created: #{@message}"
|
47
41
|
end
|
48
42
|
end
|
49
|
-
|
50
43
|
end
|
51
44
|
|
52
45
|
ActiveSupport.on_load(:active_record) do
|
53
|
-
|
46
|
+
extend DraftPunk::Model::Macros
|
54
47
|
end
|
data/lib/draft_punk/version.rb
CHANGED
File without changes
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: draft_punk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steve Hodges
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-12-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: amoeba
|
@@ -185,13 +185,15 @@ files:
|
|
185
185
|
- draft_punk-0.2.0.gem
|
186
186
|
- draft_punk-0.2.1.gem
|
187
187
|
- draft_punk.gemspec
|
188
|
-
- lib/activerecord_class_methods.rb
|
189
|
-
- lib/activerecord_instance_methods.rb
|
190
|
-
- lib/draft_diff_instance_methods.rb
|
188
|
+
- lib/activerecord/activerecord_class_methods.rb
|
189
|
+
- lib/activerecord/activerecord_instance_methods.rb
|
190
|
+
- lib/activerecord/draft_diff_instance_methods.rb
|
191
|
+
- lib/activerecord/macros.rb
|
192
|
+
- lib/activerecord/previous_version_instance_methods.rb
|
193
|
+
- lib/activerecord/setup_model.rb
|
191
194
|
- lib/draft_punk.rb
|
192
195
|
- lib/draft_punk/version.rb
|
193
|
-
- lib/helper_methods.rb
|
194
|
-
- lib/previous_version_instance_methods.rb
|
196
|
+
- lib/helpers/helper_methods.rb
|
195
197
|
homepage: https://github.com/stevehodges/draftpunk
|
196
198
|
licenses:
|
197
199
|
- MIT
|
@@ -1,212 +0,0 @@
|
|
1
|
-
require 'activerecord_instance_methods'
|
2
|
-
require 'draft_diff_instance_methods'
|
3
|
-
require 'previous_version_instance_methods'
|
4
|
-
|
5
|
-
module DraftPunk
|
6
|
-
module Model
|
7
|
-
module ActiveRecordClassMethods
|
8
|
-
# Call this method in your model to setup approval. It will recursively apply to its associations,
|
9
|
-
# thus does not need to be explicity called on its associated models (and will error if you try).
|
10
|
-
#
|
11
|
-
# This model must have an approved_version_id column (Integer), which will be used to track its draft
|
12
|
-
#
|
13
|
-
# For instance, your Business model:
|
14
|
-
# class Business << ActiveRecord::Base
|
15
|
-
# has_many :employees
|
16
|
-
# has_many :images
|
17
|
-
# has_one :address
|
18
|
-
# has_many :vending_machines
|
19
|
-
#
|
20
|
-
# requires_approval # When creating a business's draft, :employees, :vending_machines, :images, and :address will all have drafts created
|
21
|
-
# end
|
22
|
-
#
|
23
|
-
# Optionally, specify which associations which the user will edit - associations which should have a draft created
|
24
|
-
# by defining a CREATES_NESTED_DRAFTS_FOR constant for this model.
|
25
|
-
#
|
26
|
-
# CREATES_NESTED_DRAFTS_FOR = [:address] # When creating a business's draft, only :address will have drafts created
|
27
|
-
#
|
28
|
-
# To disable drafts for all assocations for this model, simply pass an empty array:
|
29
|
-
# by defining a CREATES_NESTED_DRAFTS_FOR constant for this model.
|
30
|
-
# CREATES_NESTED_DRAFTS_FOR = [] # When creating a business's draft, no associations will have drafts created
|
31
|
-
#
|
32
|
-
# WARNING: If you are setting associations via accepts_nested_attributes all changes to the draft, including associations, get set on the
|
33
|
-
# draft object (as expected). If your form includes associated objects which weren't defined in requires_approval, your save will fail since
|
34
|
-
# the draft object doesn't HAVE those associations to update! In this case, you should probably add that association to the
|
35
|
-
# +associations+ param here.
|
36
|
-
#
|
37
|
-
# If you want your draft associations to track their live version, add an :approved_version_id column
|
38
|
-
# to each association's table. You'll be able to access that associated object's live version, just
|
39
|
-
# like you can with the original model which called requires_approval.
|
40
|
-
#
|
41
|
-
# @param nullify [Array] A list of attributes on this model to set to null on the draft when it is created. For
|
42
|
-
# instance, the _id_ and _created_at_ columns are nullified by default, since you don't want Rails to try to
|
43
|
-
# persist those on the draft.
|
44
|
-
# @param set_default_scope [Boolean] If true, set a default scope on this model for the approved scope; only approved objects
|
45
|
-
# will be returned in ActiveRecord queries, unless you call Model.unscoped
|
46
|
-
# @param allow_previous_versions_to_be_changed [Boolean] If the model tracks approved version history, and this
|
47
|
-
# param is false, previously-approved versions of the object cannot be saved (via a before_save callback )
|
48
|
-
# @param associations [Array] Use internally; set associations to create drafts for in the CREATES_NESTED_DRAFTS_FOR constant
|
49
|
-
# @return true
|
50
|
-
def requires_approval(associations: [], nullify: [], set_default_scope: false, allow_previous_versions_to_be_changed: true)
|
51
|
-
return unless draft_punk_table_exists?(table_name) # Short circuits if you're migrating
|
52
|
-
|
53
|
-
associations = draft_target_associations if associations.empty?
|
54
|
-
set_valid_associations(associations)
|
55
|
-
|
56
|
-
raise DraftPunk::ConfigurationError, "Cannot call requires_approval multiple times for #{name}" if const_defined?(:DRAFT_PUNK_IS_SETUP)
|
57
|
-
self.const_set :DRAFT_NULLIFY_ATTRIBUTES, Array(nullify).flatten.freeze
|
58
|
-
|
59
|
-
amoeba do
|
60
|
-
nullify nullify
|
61
|
-
# Note that the amoeba associations and customize options are being set in setup_associations_and_scopes_for
|
62
|
-
end
|
63
|
-
setup_amoeba_for self, set_default_scope: set_default_scope, allow_previous_versions_to_be_changed: allow_previous_versions_to_be_changed
|
64
|
-
true
|
65
|
-
end
|
66
|
-
|
67
|
-
# This will generally be only used in testing scenarios, in cases when requires_approval need to be
|
68
|
-
# called multiple times. Only the usage for that use case is supported. Use at your own risk for other
|
69
|
-
# use cases.
|
70
|
-
def disable_approval!
|
71
|
-
send(:remove_const, :DRAFT_PUNK_IS_SETUP) if const_defined? :DRAFT_PUNK_IS_SETUP
|
72
|
-
send(:remove_const, :DRAFT_NULLIFY_ATTRIBUTES) if const_defined? :DRAFT_NULLIFY_ATTRIBUTES
|
73
|
-
send(:remove_const, :ALLOW_PREVIOUS_VERSIONS_TO_BE_CHANGED) if const_defined? :ALLOW_PREVIOUS_VERSIONS_TO_BE_CHANGED
|
74
|
-
fresh_amoeba do
|
75
|
-
disable
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# List of association names this model is configured to have drafts for
|
80
|
-
# @return [Array]
|
81
|
-
def draft_target_associations
|
82
|
-
targets = if const_defined?(:CREATES_NESTED_DRAFTS_FOR) && const_get(:CREATES_NESTED_DRAFTS_FOR).is_a?(Array)
|
83
|
-
const_get(:CREATES_NESTED_DRAFTS_FOR).compact
|
84
|
-
else
|
85
|
-
default_draft_target_associations
|
86
|
-
end.map(&:to_sym)
|
87
|
-
end
|
88
|
-
|
89
|
-
# Whether this model is configured to track the approved version of a draft object.
|
90
|
-
# This will be true if the model has an approved_version_id column
|
91
|
-
#
|
92
|
-
# @return (Boolean)
|
93
|
-
def tracks_approved_version?
|
94
|
-
column_names.include? 'approved_version_id'
|
95
|
-
end
|
96
|
-
|
97
|
-
# Whether this model is configured to store previously-approved versions of the model.
|
98
|
-
# This will be true if the model has an current_approved_version_id column
|
99
|
-
#
|
100
|
-
# @return (Boolean)
|
101
|
-
def tracks_approved_version_history?
|
102
|
-
column_names.include?('current_approved_version_id')
|
103
|
-
end
|
104
|
-
|
105
|
-
protected #################################################################
|
106
|
-
|
107
|
-
def default_draft_target_associations
|
108
|
-
reflect_on_all_associations.select do |reflection|
|
109
|
-
is_relevant_association_type?(reflection) &&
|
110
|
-
!reflection.name.in?(%i(draft approved_version)) &&
|
111
|
-
reflection.class_name != name # Self referential associations!!! Don't do them!
|
112
|
-
end.map{|r| r.name.downcase.to_sym }
|
113
|
-
end
|
114
|
-
|
115
|
-
# Rejects the associations if the table hasn't been defined yet. This happens when
|
116
|
-
# running migrations which add that association's table.
|
117
|
-
def set_valid_associations(associations)
|
118
|
-
return const_get(:DRAFT_VALID_ASSOCIATIONS) if const_defined?(:DRAFT_VALID_ASSOCIATIONS)
|
119
|
-
associations = associations.map(&:to_sym)
|
120
|
-
valid_assocations = associations.select do |assoc|
|
121
|
-
reflection = reflect_on_association(assoc)
|
122
|
-
if reflection
|
123
|
-
table_name = reflection.klass.table_name
|
124
|
-
draft_punk_table_exists?(table_name)
|
125
|
-
else
|
126
|
-
false
|
127
|
-
end
|
128
|
-
end
|
129
|
-
self.const_set :DRAFT_VALID_ASSOCIATIONS, valid_assocations.freeze
|
130
|
-
valid_assocations
|
131
|
-
end
|
132
|
-
|
133
|
-
private ###################################################################
|
134
|
-
|
135
|
-
def setup_amoeba_for(target_class, options={})
|
136
|
-
return if target_class.const_defined?(:DRAFT_PUNK_IS_SETUP)
|
137
|
-
associations = target_class.draft_target_associations
|
138
|
-
associations = target_class.set_valid_associations(associations)
|
139
|
-
target_class.amoeba do
|
140
|
-
enable
|
141
|
-
include_associations target_class.const_get(:DRAFT_VALID_ASSOCIATIONS) unless target_class.const_get(:DRAFT_VALID_ASSOCIATIONS).empty?
|
142
|
-
customize target_class.set_approved_version_id_callback
|
143
|
-
end
|
144
|
-
target_class.const_set :DRAFT_PUNK_IS_SETUP, true.freeze
|
145
|
-
|
146
|
-
setup_associations_and_scopes_for target_class, **options
|
147
|
-
setup_draft_association_persistance_for_children_of target_class, associations
|
148
|
-
end
|
149
|
-
|
150
|
-
public ###################################################################
|
151
|
-
|
152
|
-
def set_approved_version_id_callback
|
153
|
-
lambda do |live_obj, draft_obj|
|
154
|
-
draft_obj.approved_version_id = live_obj.id if draft_obj.respond_to?(:approved_version_id)
|
155
|
-
draft_obj.temporary_approved_object = live_obj
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
private ###################################################################
|
160
|
-
|
161
|
-
def setup_draft_association_persistance_for_children_of(target_class, associations=nil)
|
162
|
-
associations = target_class.draft_target_associations unless associations
|
163
|
-
target_reflections = associations.map do |assoc|
|
164
|
-
reflection = target_class.reflect_on_association(assoc.to_sym)
|
165
|
-
reflection.presence || (raise DraftPunk::ConfigurationError, "#{name} includes invalid association (#{assoc})")
|
166
|
-
end
|
167
|
-
target_reflections.select{|r| is_relevant_association_type?(r) }.each do |assoc|
|
168
|
-
setup_amoeba_for assoc.klass
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
def setup_associations_and_scopes_for(target_class, set_default_scope: false, allow_previous_versions_to_be_changed: true)
|
173
|
-
target_class.send :include, InstanceInterrogators unless target_class.method_defined?(:has_draft?)
|
174
|
-
target_class.send :attr_accessor, :temporary_approved_object
|
175
|
-
target_class.send :before_create, :before_create_draft if target_class.method_defined?(:before_create_draft)
|
176
|
-
|
177
|
-
target_class.const_set :ALLOW_PREVIOUS_VERSIONS_TO_BE_CHANGED, allow_previous_versions_to_be_changed.freeze
|
178
|
-
if target_class.tracks_approved_version_history?
|
179
|
-
target_class.belongs_to :current_approved_version, class_name: target_class.name, optional: true
|
180
|
-
target_class.has_many :previous_versions, -> { order(id: :desc) }, class_name: target_class.name, foreign_key: :current_approved_version_id
|
181
|
-
target_class.before_update :prevent_previous_versions_from_saving
|
182
|
-
target_class.send :include, PreviousVersionInstanceMethods
|
183
|
-
end
|
184
|
-
|
185
|
-
if set_default_scope
|
186
|
-
target_class.default_scope -> { approved }
|
187
|
-
end
|
188
|
-
|
189
|
-
return if target_class.reflect_on_association(:approved_version) || !target_class.column_names.include?('approved_version_id')
|
190
|
-
target_class.send :include, ActiveRecordInstanceMethods
|
191
|
-
target_class.send :include, DraftDiffInstanceMethods
|
192
|
-
target_class.belongs_to :approved_version, class_name: target_class.name, optional: true
|
193
|
-
target_class.scope :approved, -> { where("#{target_class.quoted_table_name}.approved_version_id IS NULL") }
|
194
|
-
target_class.has_one :draft, -> { unscope(where: :approved) }, class_name: target_class.name, foreign_key: :approved_version_id
|
195
|
-
target_class.scope :draft, -> { unscoped.where("#{target_class.quoted_table_name}.approved_version_id IS NOT NULL") }
|
196
|
-
end
|
197
|
-
|
198
|
-
def is_relevant_association_type?(activerecord_reflection)
|
199
|
-
activerecord_reflection.macro.in? Amoeba::Config::DEFAULTS[:known_macros]
|
200
|
-
end
|
201
|
-
|
202
|
-
def draft_punk_table_exists?(table_name)
|
203
|
-
if ActiveRecord::Base.connection.respond_to? :data_source_exists?
|
204
|
-
ActiveRecord::Base.connection.data_source_exists?(table_name)
|
205
|
-
else
|
206
|
-
ActiveRecord::Base.connection.table_exists?(table_name)
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
end
|
212
|
-
end
|