userstamper 4.0.0

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