secretary-rails 1.0.0.beta1

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