secretary-rails 1.0.0.beta1

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 (38) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +186 -0
  3. data/Rakefile +9 -0
  4. data/app/models/secretary/version.rb +93 -0
  5. data/lib/generators/secretary/install_generator.rb +23 -0
  6. data/lib/generators/secretary/templates/versions_migration.rb +18 -0
  7. data/lib/secretary/config.rb +19 -0
  8. data/lib/secretary/engine.rb +4 -0
  9. data/lib/secretary/errors.rb +10 -0
  10. data/lib/secretary/gem_version.rb +3 -0
  11. data/lib/secretary/has_secretary.rb +75 -0
  12. data/lib/secretary/tracks_association.rb +146 -0
  13. data/lib/secretary/versioned_attributes.rb +58 -0
  14. data/lib/secretary-rails.rb +3 -0
  15. data/lib/secretary.rb +40 -0
  16. data/lib/tasks/secretary_tasks.rake +6 -0
  17. data/spec/factories.rb +32 -0
  18. data/spec/internal/app/models/animal.rb +7 -0
  19. data/spec/internal/app/models/hobby.rb +3 -0
  20. data/spec/internal/app/models/location.rb +6 -0
  21. data/spec/internal/app/models/person.rb +14 -0
  22. data/spec/internal/app/models/story.rb +3 -0
  23. data/spec/internal/app/models/user.rb +3 -0
  24. data/spec/internal/config/database.yml +3 -0
  25. data/spec/internal/config/routes.rb +2 -0
  26. data/spec/internal/db/combustion_test.sqlite +0 -0
  27. data/spec/internal/db/schema.rb +50 -0
  28. data/spec/internal/log/test.log +23177 -0
  29. data/spec/lib/generators/secretary/install_generator_spec.rb +17 -0
  30. data/spec/lib/secretary/config_spec.rb +9 -0
  31. data/spec/lib/secretary/has_secretary_spec.rb +116 -0
  32. data/spec/lib/secretary/tracks_association_spec.rb +214 -0
  33. data/spec/lib/secretary/versioned_attributes_spec.rb +63 -0
  34. data/spec/lib/secretary_spec.rb +44 -0
  35. data/spec/models/secretary/version_spec.rb +68 -0
  36. data/spec/spec_helper.rb +20 -0
  37. data/spec/tmp/db/migrate/20131105082639_create_versions.rb +18 -0
  38. metadata +181 -0
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'generator_spec/test_case'
3
+ require 'generators/secretary/install_generator'
4
+
5
+ describe Secretary::InstallGenerator do
6
+ include GeneratorSpec::TestCase
7
+ destination File.expand_path("../../../../tmp", __FILE__)
8
+
9
+ before do
10
+ prepare_destination
11
+ run_generator
12
+ end
13
+
14
+ it 'copies the migration file' do
15
+ assert_migration "db/migrate/create_versions"
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Secretary::Config do
4
+ let(:config) { Secretary::Config.new }
5
+ subject { config }
6
+
7
+ its(:user_class) { should eq "::User" }
8
+ its(:ignored_attributes) { should eq ['id', 'created_at', 'updated_at'] }
9
+ end
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+
3
+ describe Secretary::HasSecretary do
4
+ let(:user) { create :user }
5
+
6
+ let(:new_story) {
7
+ build :story,
8
+ :headline => "Cool Story, Bro",
9
+ :body => "Some cool text.",
10
+ :logged_user_id => user.id
11
+ }
12
+
13
+ let(:other_story) {
14
+ create :story,
15
+ :headline => "Cooler Story, Bro",
16
+ :body => "Some cooler text.",
17
+ :logged_user_id => user.id
18
+ }
19
+
20
+
21
+ describe "::has_secretary?" do
22
+ it "returns false if no has_secretary declared" do
23
+ User.has_secretary?.should eq false
24
+ end
25
+
26
+ it "returns true if @_has_secretary is true" do
27
+ Story.has_secretary?.should eq true
28
+ end
29
+ end
30
+
31
+
32
+ describe "::has_secretary" do
33
+ it "sets @_has_secretary to true" do
34
+ Story.has_secretary?.should eq true
35
+ end
36
+
37
+ it 'adds the model to Secretary.versioned_models' do
38
+ Story # Load the class
39
+ Secretary.versioned_models.should include "Story"
40
+ end
41
+
42
+ it "adds the has_many association for versions" do
43
+ new_story.versions.to_a.should eq Array.new
44
+ end
45
+
46
+ it "has logged_user_id" do
47
+ new_story.should respond_to :logged_user_id
48
+ new_story.should respond_to :logged_user_id=
49
+ end
50
+
51
+ it "generates a version on create" do
52
+ Secretary::Version.count.should eq 0
53
+ new_story.save!
54
+ Secretary::Version.count.should eq 1
55
+ new_story.versions.count.should eq 1
56
+ end
57
+
58
+ it "generates a version when a record is changed" do
59
+ other_story.update_attributes(headline: "Some Cool Headline?!")
60
+ Secretary::Version.count.should eq 2
61
+ other_story.versions.size.should eq 2
62
+ end
63
+
64
+ it "doesn't generate a version if no attributes were changed" do
65
+ other_story.save!
66
+ other_story.versions.size.should eq 1
67
+ other_story.save!
68
+ other_story.versions.size.should eq 1
69
+ end
70
+
71
+ it "destroys all versions when the object is destroyed" do
72
+ other_story.update_attributes!(headline: "Changed the headline")
73
+ other_story.versions.size.should eq 2
74
+ Secretary::Version.count.should eq 2
75
+ other_story.destroy
76
+ Secretary::Version.count.should eq 0
77
+ end
78
+ end
79
+
80
+
81
+ describe '#changes' do
82
+ it 'is the built-in changes reverse-merged with custom changes' do
83
+ story = create :story, headline: "Original Headline"
84
+ story.headline = "Updated Headline!"
85
+ story.custom_changes['assets'] = [[], { a: 1, b: 2 }]
86
+
87
+ story.changes.should eq Hash[{
88
+ 'headline' => ['Original Headline', "Updated Headline!"],
89
+ 'assets' => [[], { 'a' => 1, 'b' => 2 }]
90
+ }]
91
+ end
92
+ end
93
+
94
+
95
+ describe '#changed?' do
96
+ it 'checks if custom changes are present as well' do
97
+ other_story.changed?.should eq false
98
+ other_story.custom_changes['assets'] = [[], { a: 1, b: 2 }]
99
+ other_story.changed?.should eq true
100
+ end
101
+ end
102
+
103
+
104
+ describe '#custom_changes' do
105
+ it 'is a hash into which you can put things' do
106
+ other_story.custom_changes['something'] = ['old', 'new']
107
+ end
108
+
109
+ it 'gets cleared after saved' do
110
+ other_story.custom_changes["something"] = ["old", "new"]
111
+ other_story.custom_changes.should eq Hash[{"something" => ["old", "new"]}]
112
+ other_story.save!
113
+ other_story.custom_changes.should eq Hash[]
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,214 @@
1
+ require 'spec_helper'
2
+
3
+ describe Secretary::TracksAssociation do
4
+ describe '::tracks_association' do
5
+ it "raises an error if the model isn't versioned" do
6
+ -> {
7
+ User.tracks_association
8
+ }.should raise_error Secretary::NotVersionedError
9
+ end
10
+
11
+ it 'adds the associations to the versioned attributes' do
12
+ Person.versioned_attributes.should include "animals"
13
+ Person.versioned_attributes.should include "hobbies"
14
+ end
15
+ end
16
+
17
+
18
+ describe 'dirty association' do
19
+ let(:person) { create :person }
20
+ let(:animal) { create :animal }
21
+
22
+ it 'sets associations_were' do
23
+ person.animals << animal
24
+ person.animals_were.should eq []
25
+ end
26
+
27
+ it 'marks the association as changed when the association is changed' do
28
+ person.animals << animal
29
+ person.animals_changed?.should eq true
30
+ end
31
+
32
+ it 'clears out the dirty association after commit' do
33
+ person.animals << animal
34
+ person.animals_changed?.should eq true
35
+ person.save!
36
+ person.animals_changed?.should eq false
37
+ end
38
+ end
39
+
40
+
41
+ describe "adding to association collections" do
42
+ let(:person) { create :person }
43
+ let(:animal) { build :animal, name: "Bob", color: "dog" }
44
+
45
+ it "creates a new version when adding" do
46
+ person.animals = [animal]
47
+ person.save!
48
+
49
+ person.versions.count.should eq 2
50
+ version = person.versions.last
51
+ version.object_changes["animals"][0].should eq []
52
+
53
+ version.object_changes["animals"][1].should eq [
54
+ {"name" => "Bob", "color" => "dog"}
55
+ ]
56
+ end
57
+
58
+ it 'makes a version when adding' do
59
+ person = create :person
60
+ animal = build :animal, name: "Bryan", color: 'lame'
61
+
62
+ person.animals << animal
63
+ person.save!
64
+ person.versions.count.should eq 2
65
+
66
+ versions = person.versions.order('version_number').to_a
67
+ versions.last.object_changes["animals"][0].should eq []
68
+ versions.last.object_changes["animals"][1].should eq [{
69
+ "name" => "Bryan",
70
+ "color" => "lame"
71
+ }]
72
+ end
73
+
74
+ it 'makes a version when removing' do
75
+ person = build :person
76
+ animal = build :animal, name: "Bryan", color: 'lame'
77
+ person.animals = [animal]
78
+ person.save!
79
+ person.versions.count.should eq 1
80
+
81
+ person.animals = []
82
+ person.save!
83
+ person.versions.count.should eq 2
84
+
85
+ versions = person.versions.order('version_number').to_a
86
+ versions.last.object_changes["animals"][0].should eq [{
87
+ "name" => "Bryan",
88
+ "color" => "lame"
89
+ }]
90
+ versions.last.object_changes["animals"][1].should eq []
91
+ end
92
+ end
93
+
94
+
95
+ describe 'with accepts_nested_attributes_for' do
96
+ let(:animal) { create :animal, name: "Henry", color: "blind" }
97
+ let(:person) { create :person }
98
+
99
+ it 'adds a new version when adding to collection' do
100
+ animals_attributes = [
101
+ {
102
+ "name" => "George",
103
+ "color" => "yes"
104
+ }
105
+ ]
106
+
107
+ person.animals_attributes = animals_attributes
108
+ person.save!
109
+ person.versions.count.should eq 2
110
+
111
+ version = person.versions.order('version_number').last
112
+ version.object_changes["animals"][0].should eq []
113
+ version.object_changes["animals"][1].should eq [{
114
+ "name" => "George",
115
+ "color" => "yes"
116
+ }]
117
+ end
118
+
119
+ it 'adds a new version when changing something in collection' do
120
+ animals_attributes = [
121
+ {
122
+ "id" => animal.id,
123
+ "name" => "Lemon"
124
+ }
125
+ ]
126
+
127
+ person.animals << animal
128
+ person.save!
129
+ person.versions.count.should eq 2
130
+ person.animals_attributes = animals_attributes
131
+ person.save!
132
+ person.versions.count.should eq 3
133
+
134
+ version = person.versions.order('version_number').last
135
+ version.object_changes["animals"][0].should eq [{
136
+ "name" => "Henry",
137
+ "color" => "blind"
138
+ }]
139
+ version.object_changes["animals"][1].should eq [{
140
+ "name" => "Lemon",
141
+ "color" => "blind"
142
+ }]
143
+ end
144
+
145
+ it 'adds a new version when removing something from collection' do
146
+ animals_attributes = [
147
+ {
148
+ "id" => animal.id,
149
+ "_destroy" => "1"
150
+ }
151
+ ]
152
+
153
+ person.animals << animal
154
+ person.save!
155
+ person.versions.count.should eq 2
156
+ person.animals_attributes = animals_attributes
157
+ person.save!
158
+ person.versions.count.should eq 3
159
+
160
+ version = person.versions.order('version_number').last
161
+ version.object_changes["animals"][0].should eq [{
162
+ "name" => "Henry",
163
+ "color" => "blind"
164
+ }]
165
+ version.object_changes["animals"][1].should eq []
166
+ end
167
+
168
+ it 'does not add a new version if nothing has changed' do
169
+ animals_attributes = [
170
+ {
171
+ "id" => animal.id,
172
+ "name" => "Henry"
173
+ }
174
+ ]
175
+
176
+ person.animals << animal
177
+ person.save!
178
+ person.versions.count.should eq 2
179
+ # this doesn't call before_add/remove callbacks
180
+ person.animals_attributes = animals_attributes
181
+ person.save!
182
+ person.versions.count.should eq 2
183
+ end
184
+ end
185
+
186
+
187
+ describe '#association_changed?' do
188
+ let(:person) { create :person }
189
+ let(:animal) { create :animal }
190
+
191
+ it 'is true if the association has changed' do
192
+ person.animals_changed?.should eq false
193
+ person.animals << animal
194
+ person.animals_changed?.should eq true
195
+ end
196
+
197
+ it 'is false after the parent object has been saved' do
198
+ person.animals << animal
199
+ person.animals_changed?.should eq true
200
+ person.save!
201
+ person.animals_changed?.should eq false
202
+ end
203
+
204
+ it 'is false if the association has not changed' do
205
+ person.animals << animal
206
+ person.animals_changed?.should eq true
207
+ person.save!
208
+ person.animals_changed?.should eq false
209
+
210
+ person.animals = [animal]
211
+ person.animals_changed?.should eq false
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe Secretary::VersionedAttributes do
4
+ describe '::versioned_attributes' do
5
+ it 'uses the attributes set in the model' do
6
+ Animal.versioned_attributes.should eq ["name", "color"]
7
+ end
8
+
9
+ it 'uses the column names minus the global ignores if nothing is set' do
10
+ Location.versioned_attributes.should eq ["title", "address", "people"]
11
+ end
12
+
13
+ it 'subtracts unversioned attributes if they are set' do
14
+ Person.versioned_attributes.should_not include "name"
15
+ Person.versioned_attributes.should_not include "ethnicity"
16
+ end
17
+ end
18
+
19
+
20
+ describe '#versioned_changes' do
21
+ let(:person) { create :person }
22
+
23
+ it 'is empty for non-dirty objects' do
24
+ person.versioned_changes.should eq Hash[]
25
+ end
26
+
27
+ it "return a hash of changes for just the attributes we want" do
28
+ person.age = 120
29
+ person.name = "Freddie"
30
+
31
+ person.versioned_changes.should eq Hash[{
32
+ "age" => [100, 120]
33
+ }]
34
+ end
35
+ end
36
+
37
+
38
+ describe '#versioned_attributes' do
39
+ let(:animal) { create :animal, name: "Henry", color: "henry" }
40
+
41
+ it 'is only the attributes we want' do
42
+ animal.versioned_attributes.should eq Hash[{
43
+ "name" => "Henry",
44
+ "color" => "henry"
45
+ }]
46
+ end
47
+ end
48
+
49
+
50
+ describe '#versioned_attribute?' do
51
+ let(:person) { create :person }
52
+
53
+ it 'is true if the attribute should be versioned' do
54
+ person.versioned_attribute?("age").should eq true
55
+ person.versioned_attribute?(:age).should eq true
56
+ end
57
+
58
+ it 'is false if the attribute should not be versioned' do
59
+ person.versioned_attribute?("name").should eq false
60
+ person.versioned_attribute?("id").should eq false
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe Secretary do
4
+ describe "::configure" do
5
+ it "accepts a block with the config object" do
6
+ Secretary.configure do |config|
7
+ config.should be_a Secretary::Config
8
+ end
9
+ end
10
+
11
+ it "sets Secretary.config to the new Config object" do
12
+ config = Secretary.configure
13
+ Secretary.config.should eq config
14
+ end
15
+ end
16
+
17
+
18
+ describe '::config' do
19
+ it 'creates a new configuration if none is set' do
20
+ Secretary.config.should be_a Secretary::Config
21
+ end
22
+
23
+ it 'uses the set configuration if available' do
24
+ id = Secretary.configure.object_id
25
+ Secretary.config.object_id.should eq id
26
+ end
27
+ end
28
+
29
+
30
+ describe '::versioned_models' do
31
+ it 'lists the name of all the versioned models' do
32
+ Story # load the class
33
+ Secretary.versioned_models.should include "Story"
34
+
35
+ class Something < ActiveRecord::Base
36
+ self.table_name = "stories"
37
+ has_secretary
38
+ end
39
+
40
+ Secretary.versioned_models.should include "Story"
41
+ Secretary.versioned_models.should include "Something"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe Secretary::Version do
4
+ describe "::generate" do
5
+ it "generates a new version for passed-in object" do
6
+ story = create :story
7
+ version = Secretary::Version.generate(story)
8
+
9
+ Secretary::Version.count.should eq 2
10
+ story.versions.last.should eq version
11
+ end
12
+ end
13
+
14
+
15
+ describe "generating description" do
16
+ let(:story) {
17
+ build :story, headline: "Cool story, bro", body: "Cool text, bro."
18
+ }
19
+
20
+ it "generates a description with object name on create" do
21
+ story.save!
22
+ story.versions.last.description.should eq "Created Story ##{story.id}"
23
+ end
24
+
25
+ it "generates a description with the changed attributes on update" do
26
+ story.save!
27
+ story.update_attributes(headline: "Another Headline", body: "New Body")
28
+ story.versions.last.description.should eq "Changed headline and body"
29
+ end
30
+ end
31
+
32
+
33
+ describe "incrementing version number" do
34
+ it "sets version_number to 1 if no other versions exist for this object" do
35
+ story = create :story
36
+ story.versions.last.version_number.should eq 1
37
+ end
38
+
39
+ it "increments version number if versions already exist" do
40
+ story = create :story, headline: "Some Headline"
41
+ story.versions.last.version_number.should eq 1
42
+ story.update_attributes(headline: "Cooler story, bro.")
43
+ story.versions.last.version_number.should eq 2
44
+ story.update_attributes(headline: "Coolest story, bro!")
45
+ story.versions.last.version_number.should eq 3
46
+ end
47
+ end
48
+
49
+
50
+ describe '#attribute_diffs' do
51
+ it 'is a hash of attribute keys, and Diffy::Diff objects' do
52
+ story = create :story, headline: "Cool story, bro"
53
+ story.update_attributes!(headline: "Updated Headline")
54
+
55
+ version = story.versions.last
56
+ version.attribute_diffs.keys.should eq ["headline"]
57
+ version.attribute_diffs["headline"].should be_a Diffy::Diff
58
+ end
59
+ end
60
+
61
+
62
+ describe '#title' do
63
+ it "is the simple title for the version" do
64
+ story = create :story
65
+ story.versions.last.title.should eq "Story ##{story.id} v1"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,20 @@
1
+ ENV["RAILS_ENV"] ||= 'test'
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ require 'combustion'
7
+ Combustion.initialize! :active_record
8
+
9
+ require 'rspec/rails'
10
+ require 'factory_girl'
11
+ load 'factories.rb'
12
+
13
+ RSpec.configure do |config|
14
+ config.use_transactional_fixtures = true
15
+ config.include FactoryGirl::Syntax::Methods
16
+ config.mock_with :rspec
17
+ config.order = "random"
18
+ config.filter_run focus: true
19
+ config.run_all_when_everything_filtered = true
20
+ end
@@ -0,0 +1,18 @@
1
+ class SecretaryCreateVersions < ActiveRecord::Migration
2
+ def change
3
+ create_table "versions" do |t|
4
+ t.integer "version_number"
5
+ t.string "versioned_type"
6
+ t.integer "versioned_id"
7
+ t.string "user_id"
8
+ t.text "description"
9
+ t.text "object_changes"
10
+ t.datetime "created_at"
11
+ end
12
+
13
+ add_index "versions", ["created_at"]
14
+ add_index "versions", ["user_id"]
15
+ add_index "versions", ["version_number"]
16
+ add_index "versions", ["versioned_type", "versioned_id"]
17
+ end
18
+ end