userstamper 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +35 -0
  5. data/CHANGELOG.md +116 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +222 -0
  8. data/LICENSE +22 -0
  9. data/README.md +142 -0
  10. data/lib/userstamper.rb +23 -0
  11. data/lib/userstamper/configuration.rb +40 -0
  12. data/lib/userstamper/controller_concern.rb +44 -0
  13. data/lib/userstamper/migration_concern.rb +9 -0
  14. data/lib/userstamper/model_concern.rb +6 -0
  15. data/lib/userstamper/railtie.rb +15 -0
  16. data/lib/userstamper/stampable.rb +106 -0
  17. data/lib/userstamper/stamper.rb +54 -0
  18. data/lib/userstamper/utilities.rb +57 -0
  19. data/spec/controllers/posts_controller_spec.rb +44 -0
  20. data/spec/controllers/users_controller_spec.rb +50 -0
  21. data/spec/coverage_helper.rb +58 -0
  22. data/spec/dummy/README.rdoc +28 -0
  23. data/spec/dummy/Rakefile +6 -0
  24. data/spec/dummy/app/assets/config/manifest.js +0 -0
  25. data/spec/dummy/app/assets/images/.keep +0 -0
  26. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  27. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  28. data/spec/dummy/app/controllers/application_controller.rb +13 -0
  29. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  30. data/spec/dummy/app/controllers/posts_controller.rb +36 -0
  31. data/spec/dummy/app/controllers/users_controller.rb +22 -0
  32. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  33. data/spec/dummy/app/mailers/.keep +0 -0
  34. data/spec/dummy/app/models/comment.rb +5 -0
  35. data/spec/dummy/app/models/concerns/.keep +0 -0
  36. data/spec/dummy/app/models/person.rb +3 -0
  37. data/spec/dummy/app/models/post.rb +6 -0
  38. data/spec/dummy/app/models/tag.rb +3 -0
  39. data/spec/dummy/app/models/user.rb +3 -0
  40. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  41. data/spec/dummy/bin/bundle +3 -0
  42. data/spec/dummy/bin/rails +4 -0
  43. data/spec/dummy/bin/rake +4 -0
  44. data/spec/dummy/bin/setup +29 -0
  45. data/spec/dummy/config.ru +4 -0
  46. data/spec/dummy/config/application.rb +12 -0
  47. data/spec/dummy/config/boot.rb +5 -0
  48. data/spec/dummy/config/database.yml +23 -0
  49. data/spec/dummy/config/environment.rb +5 -0
  50. data/spec/dummy/config/environments/development.rb +41 -0
  51. data/spec/dummy/config/environments/production.rb +79 -0
  52. data/spec/dummy/config/environments/test.rb +37 -0
  53. data/spec/dummy/config/initializers/assets.rb +11 -0
  54. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  55. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  56. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  57. data/spec/dummy/config/initializers/inflections.rb +16 -0
  58. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  59. data/spec/dummy/config/initializers/session_store.rb +3 -0
  60. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  61. data/spec/dummy/config/locales/en.yml +23 -0
  62. data/spec/dummy/config/routes.rb +56 -0
  63. data/spec/dummy/config/secrets.yml +22 -0
  64. data/spec/dummy/lib/assets/.keep +0 -0
  65. data/spec/dummy/log/.keep +0 -0
  66. data/spec/dummy/public/404.html +67 -0
  67. data/spec/dummy/public/422.html +67 -0
  68. data/spec/dummy/public/500.html +66 -0
  69. data/spec/dummy/public/favicon.ico +0 -0
  70. data/spec/lib/configuration_spec.rb +20 -0
  71. data/spec/lib/migration_spec.rb +60 -0
  72. data/spec/lib/stamper_spec.rb +66 -0
  73. data/spec/lib/stamping_spec.rb +235 -0
  74. data/spec/lib/userstamp_spec.rb +4 -0
  75. data/spec/rails_helper.rb +7 -0
  76. data/spec/spec_helper.rb +98 -0
  77. data/spec/support/database_helpers.rb +73 -0
  78. data/spec/support/with_temporary_table.rb +51 -0
  79. data/userstamper.gemspec +46 -0
  80. metadata +279 -0
@@ -0,0 +1,23 @@
1
+ require "active_support"
2
+ require "active_support/rails"
3
+ require "active_support/concern"
4
+
5
+ require "zeitwerk"
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
8
+
9
+ module Userstamper
10
+ # Retrieves the configuration for the Userstamper gem.
11
+ # @return [Userstamper::Configuration]
12
+ def self.config
13
+ Configuration
14
+ end
15
+
16
+ # Configures the gem.
17
+ # @yield [Userstamper::Configuration] The configuration for the gem.
18
+ def self.configure
19
+ yield config
20
+ end
21
+ end
22
+
23
+ require "userstamper/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,40 @@
1
+ module Userstamper::Configuration
2
+ # !@attribute [r] default_stamper
3
+ # Determines the default model used to stamp other models.
4
+ #
5
+ # By default, this is set to +'User'+.
6
+ def self.default_stamper
7
+ ActiveRecord::Base.stamper_class_name
8
+ end
9
+
10
+ # !@attribute [rw] default_stamper
11
+ # @see {.default_stamper}
12
+ def self.default_stamper=(stamper)
13
+ ActiveRecord::Base.stamper_class_name = stamper
14
+ end
15
+ self.default_stamper = 'User'.freeze
16
+
17
+ # @!attribute [r] default_stamper_class
18
+ # Determines the default model used to stamp other models.
19
+ def self.default_stamper_class
20
+ ActiveRecord::Base.stamper_class
21
+ end
22
+
23
+ # !@attribute [rw] creator_attribute
24
+ # Determines the name of the column in the database which stores the name of the creator.
25
+ #
26
+ # Override the attribute by using the stampable class method within a model.
27
+ #
28
+ # By default, this is set to +:creator_id+.
29
+ mattr_accessor :creator_attribute
30
+ self.creator_attribute = :creator_id
31
+
32
+ # !@attribute [rw] updater_attribute
33
+ # Determines the name of the column in the database which stores the name of the updater.
34
+ #
35
+ # Override the attribute by using the stampable class method within a model.
36
+ #
37
+ # By default, this is set to +:updater_id+.
38
+ mattr_accessor :updater_attribute
39
+ self.updater_attribute = :updater_id
40
+ end
@@ -0,0 +1,44 @@
1
+ # The +Userstamper::Controller+ module, when included into a controller, adds an +before_action+
2
+ # callback (named +set_stamper+) and an +after_action+ callback (named +reset_stamper+). These
3
+ # methods assume a couple of things, but can be re-implemented in your controller to better suit
4
+ # your application.
5
+ #
6
+ # See the documentation for `set_stamper` and `reset_stamper` for specific implementation details.
7
+ module Userstamper::ControllerConcern
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ around_action :with_stamper
12
+ end
13
+
14
+ private
15
+
16
+ # This {#with_stamper} method sets the stamper for the duration of the action. This ensures
17
+ # that exceptions raised within the controller action would properly restore the previous stamper.
18
+ #
19
+ # TODO: Remove set_stamper/reset_stamper
20
+ def with_stamper
21
+ set_stamper
22
+ yield
23
+ ensure
24
+ reset_stamper
25
+ end
26
+
27
+ # The {#set_stamper} method as implemented here assumes a couple of things. First, that you are
28
+ # using a +User+ model as the stamper and second that your controller has a +current_user+
29
+ # method that contains the currently logged in stamper. If either of these are not the case in
30
+ # your application you will want to manually add your own implementation of this method to the
31
+ # private section of your +ApplicationController+
32
+ def set_stamper
33
+ @_userstamp_stamper = Userstamper.config.default_stamper_class.stamper
34
+ Userstamper.config.default_stamper_class.stamper = current_user
35
+ end
36
+
37
+ # The {#reset_stamper} method as implemented here assumes that a +User+ model is being used as
38
+ # the stamper. If this is not the case then you will need to manually add your own
39
+ # implementation of this method to the private section of your +ApplicationController+
40
+ def reset_stamper
41
+ Userstamper.config.default_stamper_class.stamper = @_userstamp_stamper
42
+ @_userstamp_stamper = nil
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ module Userstamper::MigrationConcern
2
+ extend ActiveSupport::Concern
3
+
4
+ def userstamps(*args)
5
+ config = Userstamper.config
6
+ column(config.creator_attribute, :integer, *args)
7
+ column(config.updater_attribute, :integer, *args)
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Userstamper::ModelConcern
2
+ extend ActiveSupport::Concern
3
+
4
+ include Userstamper::Stampable
5
+ include Userstamper::Stamper
6
+ end
@@ -0,0 +1,15 @@
1
+ module Userstamper
2
+ class Railtie < Rails::Railtie
3
+ initializer "userstamper.action_controller" do
4
+ ActiveSupport.on_load(:action_controller_base) do
5
+ include Userstamper::ControllerConcern
6
+ end
7
+
8
+ ActiveSupport.on_load(:active_record) do
9
+ include Userstamper::ModelConcern
10
+
11
+ ActiveRecord::ConnectionAdapters::TableDefinition.send(:include, Userstamper::MigrationConcern)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,106 @@
1
+ # Extends the stamping functionality of ActiveRecord by automatically recording the model
2
+ # responsible for creating, updating the current object. See the +Stamper+ and
3
+ # +ControllerAdditions+ modules for further documentation on how the entire process works.
4
+ module Userstamper::Stampable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Should ActiveRecord record userstamps? Defaults to true.
9
+ class_attribute :record_userstamp
10
+ self.record_userstamp = true
11
+
12
+ class_attribute :stamper_class_name
13
+
14
+ before_validation :set_updater_attribute, if: :record_userstamp
15
+ before_validation :set_creator_attribute, on: :create, if: :record_userstamp
16
+ before_save :set_updater_attribute, if: :record_userstamp
17
+ before_create :set_creator_attribute, if: :record_userstamp
18
+ end
19
+
20
+ module ClassMethods
21
+ def columns(*)
22
+ columns = super
23
+ return columns if defined?(@stamper_initialized) && @stamper_initialized
24
+
25
+ add_userstamp_associations({})
26
+ columns
27
+ end
28
+
29
+ # This method customizes how the gem functions. For example:
30
+ #
31
+ # class Post < ActiveRecord::Base
32
+ # stampable stamper_class_name: Person.name
33
+ # end
34
+ #
35
+ # the gem configuration.
36
+ def stampable(options = {})
37
+ self.stamper_class_name = options.delete(:stamper_class_name) if options.key?(:stamper_class_name)
38
+
39
+ add_userstamp_associations(options)
40
+ end
41
+
42
+ # Temporarily allows you to turn stamping off. For example:
43
+ #
44
+ # Post.without_stamps do
45
+ # post = Post.find(params[:id])
46
+ # post.update_attributes(params[:post])
47
+ # post.save
48
+ # end
49
+ def without_stamps
50
+ original_value = self.record_userstamp
51
+ self.record_userstamp = false
52
+ yield
53
+ ensure
54
+ self.record_userstamp = original_value
55
+ end
56
+
57
+ def stamper_class #:nodoc:
58
+ stamper_class_name.to_s.camelize.constantize rescue nil
59
+ end
60
+
61
+ private
62
+
63
+ # Defines the associations for Userstamper.
64
+ def add_userstamp_associations(options)
65
+ @stamper_initialized = true
66
+ Userstamper::Utilities.remove_association(self, :creator)
67
+ Userstamper::Utilities.remove_association(self, :updater)
68
+
69
+ associations = Userstamper::Utilities.available_association_columns(self)
70
+ return if associations.nil?
71
+
72
+ config = Userstamper.config
73
+ klass = stamper_class.try(:name)
74
+ relation_options = options.reverse_merge(class_name: klass)
75
+
76
+ belongs_to :creator, relation_options.reverse_merge(foreign_key: config.creator_attribute, required: false) if associations.first
77
+ belongs_to :updater, relation_options.reverse_merge(foreign_key: config.updater_attribute, required: false) if associations.second
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def has_stamper?
84
+ !self.class.stamper_class.nil? && !self.class.stamper_class.stamper.nil?
85
+ end
86
+
87
+ def set_creator_attribute
88
+ return unless has_stamper?
89
+
90
+ creator_association = self.class.reflect_on_association(:creator)
91
+ return unless creator_association
92
+ return if creator.present?
93
+
94
+ Userstamper::Utilities.assign_stamper(self, creator_association)
95
+ end
96
+
97
+ def set_updater_attribute
98
+ return unless has_stamper?
99
+
100
+ updater_association = self.class.reflect_on_association(:updater)
101
+ return unless updater_association
102
+ return if !new_record? && !changed?
103
+
104
+ Userstamper::Utilities.assign_stamper(self, updater_association)
105
+ end
106
+ end
@@ -0,0 +1,54 @@
1
+ module Userstamper::Stamper
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def model_stamper
6
+ # don't allow multiple calls
7
+ return if singleton_class.included_modules.include?(InstanceMethods)
8
+ extend Userstamper::Stamper::InstanceMethods
9
+ end
10
+ end
11
+
12
+ module InstanceMethods
13
+ # Used to set the current stamper for this model.
14
+ #
15
+ # @overload stamper=(object_id)
16
+ # @param [Fixnum] object_id The ID of the stamper.
17
+ # @return [Fixnum]
18
+ # @overload stamper=(object)
19
+ # @param [ActiveRecord::Base] object The stamper object.
20
+ # @return [ActiveRecord::Base]
21
+ def stamper=(object)
22
+ Thread.current[stamper_identifier] = object
23
+ end
24
+
25
+ # Retrieves the existing stamper.
26
+ def stamper
27
+ Thread.current[stamper_identifier]
28
+ end
29
+
30
+ # Sets the stamper back to +nil+ to prepare for the next request.
31
+ def reset_stamper
32
+ self.stamper = nil
33
+ end
34
+
35
+ # For the duration that execution is within the provided block, the stamper for this class
36
+ # would be the specified value.
37
+ #
38
+ # This replaces the {#stamper=} and {#reset_stamper} pair because this guarantees exception
39
+ # safety.
40
+ def with_stamper(stamper)
41
+ old_stamper = self.stamper
42
+ self.stamper = stamper
43
+ yield
44
+ ensure
45
+ self.stamper = old_stamper
46
+ end
47
+
48
+ private
49
+
50
+ def stamper_identifier
51
+ "#{self.to_s.downcase}_#{self.object_id}_stamper"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ module Userstamper::Utilities
2
+ # Removes the association methods from the model.
3
+ #
4
+ # @param [Class] model The model to remove methods from.
5
+ # @param [Symbol] association The name of the association to remove.
6
+ # @return [void]
7
+ def self.remove_association(model, association)
8
+ methods = [
9
+ association,
10
+ "#{association}=",
11
+ "build_#{association}",
12
+ "create_#{association}",
13
+ "create_#{association}!"
14
+ ]
15
+
16
+ model.generated_association_methods.module_eval do
17
+ methods.each do |method|
18
+ remove_possible_method(method)
19
+ end
20
+ end
21
+ end
22
+
23
+ # Obtains the creator/updater columns which are present in the model.
24
+ #
25
+ # @param [Class] model The model to query.
26
+ # @return [nil|Array<(bool, bool, bool)>] Nil if the model does not have a table defined.
27
+ # Otherwise, a tuple of booleans indicating the presence of the created, updated columns.
28
+ def self.available_association_columns(model)
29
+ return nil if model.name.nil? || model.table_name.empty?
30
+ columns = Set[*model.column_names]
31
+ config = Userstamper.config
32
+
33
+ [config.creator_attribute.present? && columns.include?(config.creator_attribute.to_s),
34
+ config.updater_attribute.present? && columns.include?(config.updater_attribute.to_s)]
35
+ rescue ActiveRecord::StatementInvalid => _
36
+ nil
37
+ end
38
+
39
+ # Assigns the stamper to the given association reflection in the record.
40
+ #
41
+ # If the stamper is a record, then it is assigned to the association; if it is a number, then it
42
+ # is assigned to the foreign key.
43
+ #
44
+ # @param [ActiveRecord::Base] record The record to assign.
45
+ # @param [ActiveRecord::Reflection] association The association to assign
46
+ def self.assign_stamper(record, association)
47
+ stamp_value = record.class.stamper_class.stamper
48
+ attribute =
49
+ if stamp_value.is_a?(ActiveRecord::Base)
50
+ association.name
51
+ else
52
+ association.foreign_key
53
+ end
54
+
55
+ record.send("#{attribute}=", stamp_value)
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe PostsController, type: :controller do
4
+ controller do
5
+ end
6
+
7
+ before(:each) { define_first_post }
8
+
9
+ context 'when updating a Post' do
10
+ it 'sets the correct updater' do
11
+ request.session = { person_id: @delynn.id }
12
+ post :update, params: { id: @first_post.id, post: { title: 'Different' } }
13
+
14
+ expect(response.status).to eq(200)
15
+ expect(controller.instance_variable_get(:@post).title).to eq('Different')
16
+ expect(controller.instance_variable_get(:@post).updater).to eq(@delynn)
17
+ end
18
+ end
19
+
20
+ context 'when handling multiple requests' do
21
+ def simulate_second_request
22
+ old_request_session = request.session
23
+ request.session = { person_id: @nicole.id }
24
+
25
+ post :update, params: { id: @first_post.id, post: { title: 'Different Second'} }
26
+ expect(controller.instance_variable_get(:@post).updater).to eq(@nicole)
27
+ ensure
28
+ request.session = old_request_session
29
+ end
30
+
31
+ it 'sets the correct updater' do
32
+ request.session = { person_id: @delynn.id }
33
+ get :edit, params: { id: @first_post.id }
34
+ expect(response.status).to eq(200)
35
+
36
+ simulate_second_request
37
+
38
+ post :update, params: { id: @first_post.id, post: { title: 'Different' } }
39
+ expect(response.status).to eq(200)
40
+ expect(controller.instance_variable_get(:@post).title).to eq('Different')
41
+ expect(controller.instance_variable_get(:@post).updater).to eq(@delynn)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,50 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe UsersController, type: :controller do
4
+ controller do
5
+ end
6
+
7
+ context 'when updating a User' do
8
+ it 'sets the correct updater' do
9
+ request.session = { user_id: @hera.id }
10
+ patch :update, params: { id: @hera.id, user: { name: 'Different'} }
11
+
12
+ expect(response.status).to eq(200)
13
+ expect(controller.instance_variable_get(:@user).name).to eq('Different')
14
+ expect(controller.instance_variable_get(:@user).updater).to eq(@hera)
15
+ end
16
+ end
17
+
18
+ context 'when handling multiple requests' do
19
+ def simulate_second_request
20
+ old_request_session = request.session
21
+ request.session = { user_id: @zeus.id }
22
+
23
+ post :update, params: { id: @hera.id, user: { name: 'Different Second' } }
24
+ expect(controller.instance_variable_get(:@user).updater).to eq(@zeus)
25
+ ensure
26
+ request.session = old_request_session
27
+ end
28
+
29
+ it 'sets the correct updater' do
30
+ request.session = { user_id: @hera.id }
31
+ get :edit, params: { id: @hera.id }
32
+ expect(response.status).to eq(200)
33
+
34
+ simulate_second_request
35
+ end
36
+ end
37
+
38
+ context 'when the handler raises an exception' do
39
+ before { @stamper = User.stamper }
40
+ it 'restores the correct stamper' do
41
+ begin
42
+ request.session = { user_id: @zeus.id }
43
+ post :create
44
+ rescue
45
+ end
46
+
47
+ expect(User.stamper).to be(@stamper)
48
+ end
49
+ end
50
+ end