draft_punk 0.3.0 → 0.3.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.
- 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
|