paper_trail 4.0.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,10 +7,10 @@ module PaperTrail
7
7
  included do
8
8
  belongs_to :item, :polymorphic => true
9
9
 
10
- # Since the test suite has test coverage for this, we want to declare the
11
- # association when the test suite is running. This makes it pass
12
- # when DB is not initialized prior to test runs such as when we run on
13
- # Travis CI (there won't be a db in `test/dummy/db/`)
10
+ # Since the test suite has test coverage for this, we want to declare
11
+ # the association when the test suite is running. This makes it pass when
12
+ # DB is not initialized prior to test runs such as when we run on Travis
13
+ # CI (there won't be a db in `test/dummy/db/`).
14
14
  if PaperTrail.config.track_associations?
15
15
  has_many :version_associations, :dependent => :destroy
16
16
  end
@@ -47,8 +47,8 @@ module PaperTrail
47
47
  where 'event <> ?', 'create'
48
48
  end
49
49
 
50
- # Expects `obj` to be an instance of `PaperTrail::Version` by default, but can accept a timestamp if
51
- # `timestamp_arg` receives `true`
50
+ # Expects `obj` to be an instance of `PaperTrail::Version` by default,
51
+ # but can accept a timestamp if `timestamp_arg` receives `true`
52
52
  def subsequent(obj, timestamp_arg = false)
53
53
  if timestamp_arg != true && self.primary_key_is_int?
54
54
  return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
@@ -75,7 +75,8 @@ module PaperTrail
75
75
  ).order(self.timestamp_sort_order)
76
76
  end
77
77
 
78
- # defaults to using the primary key as the secondary sort order if possible
78
+ # Defaults to using the primary key as the secondary sort order if
79
+ # possible.
79
80
  def timestamp_sort_order(direction = 'asc')
80
81
  [arel_table[PaperTrail.timestamp_field].send(direction.downcase)].tap do |array|
81
82
  array << arel_table[primary_key].send(direction.downcase) if self.primary_key_is_int?
@@ -137,12 +138,14 @@ module PaperTrail
137
138
  true
138
139
  end
139
140
 
140
- # Returns whether the `object` column is using the `json` type supported by PostgreSQL
141
+ # Returns whether the `object` column is using the `json` type supported
142
+ # by PostgreSQL.
141
143
  def object_col_is_json?
142
144
  [:json, :jsonb].include?(columns_hash['object'].type)
143
145
  end
144
146
 
145
- # Returns whether the `object_changes` column is using the `json` type supported by PostgreSQL
147
+ # Returns whether the `object_changes` column is using the `json` type
148
+ # supported by PostgreSQL.
146
149
  def object_changes_col_is_json?
147
150
  [:json, :jsonb].include?(columns_hash['object_changes'].try(:type))
148
151
  end
@@ -150,18 +153,31 @@ module PaperTrail
150
153
 
151
154
  # Restore the item from this version.
152
155
  #
153
- # Optionally this can also restore all :has_one and :has_many (including has_many :through) associations as
154
- # they were "at the time", if they are also being versioned by PaperTrail.
156
+ # Optionally this can also restore all :has_one and :has_many (including
157
+ # has_many :through) associations as they were "at the time", if they are
158
+ # also being versioned by PaperTrail.
155
159
  #
156
160
  # Options:
157
- # :has_one set to `true` to also reify has_one associations. Default is `false`.
158
- # :has_many set to `true` to also reify has_many and has_many :through associations.
159
- # Default is `false`.
160
- # :mark_for_destruction set to `true` to mark the has_one/has_many associations that did not exist in the
161
- # reified version for destruction, instead of remove them. Default is `false`.
162
- # This option is handy for people who want to persist the reified version.
163
- # :dup `false` default behavior
164
- # `true` it always create a new object instance. It is useful for comparing two versions of the same object
161
+ #
162
+ # - :has_one
163
+ # - `true` - Also reify has_one associations.
164
+ # - `false - Default.
165
+ # - :has_many
166
+ # - `true` - Also reify has_many and has_many :through associations.
167
+ # - `false` - Default.
168
+ # - :mark_for_destruction
169
+ # - `true` - Mark the has_one/has_many associations that did not exist in
170
+ # the reified version for destruction, instead of removing them.
171
+ # - `false` - Default. Useful for persisting the reified version.
172
+ # - :dup
173
+ # - `false` - Default.
174
+ # - `true` - Always create a new object instance. Useful for
175
+ # comparing two versions of the same object.
176
+ # - :unversioned_attributes
177
+ # - `:nil` - Default. Attributes undefined in version record are set to
178
+ # nil in reified record.
179
+ # - `:preserve` - Attributes undefined in version record are not modified.
180
+ #
165
181
  def reify(options = {})
166
182
  return nil if object.nil?
167
183
 
@@ -170,39 +186,43 @@ module PaperTrail
170
186
  :version_at => created_at,
171
187
  :mark_for_destruction => false,
172
188
  :has_one => false,
173
- :has_many => false
189
+ :has_many => false,
190
+ :unversioned_attributes => :nil
174
191
  )
175
192
 
176
193
  attrs = self.class.object_col_is_json? ? object : PaperTrail.serializer.load(object)
177
194
 
178
- # Normally a polymorphic belongs_to relationship allows us
179
- # to get the object we belong to by calling, in this case,
180
- # `item`. However this returns nil if `item` has been
181
- # destroyed, and we need to be able to retrieve destroyed
182
- # objects.
195
+ # Normally a polymorphic belongs_to relationship allows us to get the
196
+ # object we belong to by calling, in this case, `item`. However this
197
+ # returns nil if `item` has been destroyed, and we need to be able to
198
+ # retrieve destroyed objects.
183
199
  #
184
- # In this situation we constantize the `item_type` to get hold of
185
- # the class...except when the stored object's attributes
186
- # include a `type` key. If this is the case, the object
187
- # we belong to is using single table inheritance and the
188
- # `item_type` will be the base class, not the actual subclass.
189
- # If `type` is present but empty, the class is the base class.
200
+ # In this situation we constantize the `item_type` to get hold of the
201
+ # class...except when the stored object's attributes include a `type`
202
+ # key. If this is the case, the object we belong to is using single
203
+ # table inheritance and the `item_type` will be the base class, not the
204
+ # actual subclass. If `type` is present but empty, the class is the base
205
+ # class.
190
206
 
191
207
  if options[:dup] != true && item
192
208
  model = item
193
- # Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
194
- (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
209
+ # Look for attributes that exist in the model and not in this
210
+ # version. These attributes should be set to nil.
211
+ if options[:unversioned_attributes] == :nil
212
+ (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
213
+ end
195
214
  else
196
215
  inheritance_column_name = item_type.constantize.inheritance_column
197
216
  class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
198
217
  klass = class_name.constantize
199
- # the `dup` option always returns a new object, otherwise we should attempt
200
- # to look for the item outside of default scope(s)
218
+ # The `dup` option always returns a new object, otherwise we should
219
+ # attempt to look for the item outside of default scope(s).
201
220
  if options[:dup] || (_item = klass.unscoped.find_by_id(item_id)).nil?
202
221
  model = klass.new
203
- else
222
+ elsif options[:unversioned_attributes] == :nil
204
223
  model = _item
205
- # Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
224
+ # Look for attributes that exist in the model and not in this
225
+ # version. These attributes should be set to nil.
206
226
  (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
207
227
  end
208
228
  end
@@ -211,7 +231,7 @@ module PaperTrail
211
231
  model.class.unserialize_attributes_for_paper_trail! attrs
212
232
  end
213
233
 
214
- # Set all the attributes in this version on the model
234
+ # Set all the attributes in this version on the model.
215
235
  attrs.each do |k, v|
216
236
  if model.has_attribute?(k)
217
237
  model[k.to_sym] = v
@@ -236,8 +256,9 @@ module PaperTrail
236
256
  end
237
257
  end
238
258
 
239
- # Returns what changed in this version of the item. `ActiveModel::Dirty#changes`.
240
- # returns `nil` if your `versions` table does not have an `object_changes` text column.
259
+ # Returns what changed in this version of the item.
260
+ # `ActiveModel::Dirty#changes`. returns `nil` if your `versions` table does
261
+ # not have an `object_changes` text column.
241
262
  def changeset
242
263
  return nil unless self.class.column_names.include? 'object_changes'
243
264
 
@@ -261,8 +282,8 @@ module PaperTrail
261
282
  self.paper_trail_originator
262
283
  end
263
284
 
264
- # Returns who changed the item from the state it had in this version.
265
- # This is an alias for `whodunnit`.
285
+ # Returns who changed the item from the state it had in this version. This
286
+ # is an alias for `whodunnit`.
266
287
  def terminator
267
288
  @terminator ||= whodunnit
268
289
  end
@@ -304,9 +325,9 @@ module PaperTrail
304
325
  end
305
326
  end
306
327
 
307
- # Restore the `model`'s has_one associations as they were when this version was
308
- # superseded by the next (because that's what the user was looking at when they
309
- # made the change).
328
+ # Restore the `model`'s has_one associations as they were when this
329
+ # version was superseded by the next (because that's what the user was
330
+ # looking at when they made the change).
310
331
  def reify_has_ones(model, options = {})
311
332
  version_table_name = model.class.paper_trail_version_class.table_name
312
333
  model.class.reflect_on_all_associations(:has_one).each do |assoc|
@@ -337,8 +358,9 @@ module PaperTrail
337
358
  end
338
359
  end
339
360
 
340
- # Restore the `model`'s has_many associations as they were at version_at timestamp
341
- # We lookup the first child versions after version_at timestamp or in same transaction.
361
+ # Restore the `model`'s has_many associations as they were at version_at
362
+ # timestamp We lookup the first child versions after version_at timestamp or
363
+ # in same transaction.
342
364
  def reify_has_manys(model, options = {})
343
365
  assoc_has_many_through, assoc_has_many_directly =
344
366
  model.class.reflect_on_all_associations(:has_many).
@@ -347,7 +369,8 @@ module PaperTrail
347
369
  reify_has_many_through(assoc_has_many_through, model, options)
348
370
  end
349
371
 
350
- # Restore the `model`'s has_many associations not associated through another association
372
+ # Restore the `model`'s has_many associations not associated through
373
+ # another association.
351
374
  def reify_has_many_directly(associations, model, options = {})
352
375
  version_table_name = model.class.paper_trail_version_class.table_name
353
376
  associations.each do |assoc|
@@ -363,10 +386,11 @@ module PaperTrail
363
386
  acc.merge!(v.item_id => v)
364
387
  end
365
388
 
366
- # Pass true to force the model to load
389
+ # Pass true to force the model to load.
367
390
  collection = Array.new model.send(assoc.name, true)
368
391
 
369
- # Iterate each child to replace it with the previous value if there is a version after the timestamp
392
+ # Iterate each child to replace it with the previous value if there is
393
+ # a version after the timestamp.
370
394
  collection.map! do |c|
371
395
  if (version = versions.delete(c.id)).nil?
372
396
  c
@@ -377,16 +401,18 @@ module PaperTrail
377
401
  end
378
402
  end
379
403
 
380
- # Reify the rest of the versions and add them to the collection, these versions are for those that
381
- # have been removed from the live associations
404
+ # Reify the rest of the versions and add them to the collection, these
405
+ # versions are for those that have been removed from the live
406
+ # associations.
382
407
  collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
383
408
 
384
409
  model.send(assoc.name).proxy_association.target = collection.compact
385
410
  end
386
411
  end
387
412
 
388
- # Restore the `model`'s has_many associations through another association
389
- # This must be called after the direct has_manys have been reified (reify_has_many_directly)
413
+ # Restore the `model`'s has_many associations through another association.
414
+ # This must be called after the direct has_manys have been reified
415
+ # (reify_has_many_directly).
390
416
  def reify_has_many_through(associations, model, options = {})
391
417
  associations.each do |assoc|
392
418
  next unless assoc.klass.paper_trail_enabled_for_model?
@@ -405,7 +431,8 @@ module PaperTrail
405
431
 
406
432
  collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
407
433
 
408
- # Iterate each child to replace it with the previous value if there is a version after the timestamp
434
+ # Iterate each child to replace it with the previous value if there is
435
+ # a version after the timestamp.
409
436
  collection.map! do |c|
410
437
  if (version = versions.delete(c.id)).nil?
411
438
  c
@@ -416,15 +443,17 @@ module PaperTrail
416
443
  end
417
444
  end
418
445
 
419
- # Reify the rest of the versions and add them to the collection, these versions are for those that
420
- # have been removed from the live associations
446
+ # Reify the rest of the versions and add them to the collection, these
447
+ # versions are for those that have been removed from the live
448
+ # associations.
421
449
  collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
422
450
 
423
451
  model.send(assoc.name).proxy_association.target = collection.compact
424
452
  end
425
453
  end
426
454
 
427
- # checks to see if a value has been set for the `version_limit` config option, and if so enforces it
455
+ # Checks that a value has been set for the `version_limit` config
456
+ # option, and if so enforces it.
428
457
  def enforce_version_limit!
429
458
  return unless PaperTrail.config.version_limit.is_a? Numeric
430
459
  previous_versions = sibling_versions.not_creates
@@ -2,7 +2,7 @@ module PaperTrail
2
2
  module VERSION
3
3
  MAJOR = 4
4
4
  MINOR = 0
5
- TINY = 0
5
+ TINY = 1
6
6
  PRE = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -1,16 +1,45 @@
1
1
  require 'rails_helper'
2
2
 
3
3
  describe Skipper, :type => :model do
4
- describe "#update_attributes!", :versioning => true do
5
- context "updating a skipped attribute" do
6
- let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
7
- let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
4
+ with_versioning do
5
+ it { is_expected.to be_versioned }
8
6
 
9
- it "should not create a version" do
10
- skipper = Skipper.create!(:another_timestamp => t1)
11
- expect {
12
- skipper.update_attributes!(:another_timestamp => t2)
13
- }.to_not change { skipper.versions.length }
7
+ describe "#update_attributes!", :versioning => true do
8
+ context "updating a skipped attribute" do
9
+ let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
10
+ let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
11
+
12
+ it "should not create a version" do
13
+ skipper = Skipper.create!(:another_timestamp => t1)
14
+ expect {
15
+ skipper.update_attributes!(:another_timestamp => t2)
16
+ }.to_not change { skipper.versions.length }
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "reify" do
22
+ context "reifying a with a skipped attribute" do
23
+ let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
24
+ let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
25
+
26
+ context "without preserve (default)" do
27
+ it "should have no timestamp" do
28
+ skipper = Skipper.create!(:another_timestamp => t1)
29
+ skipper.update_attributes!(:another_timestamp => t2, :name => "Foobar")
30
+ skipper = skipper.versions.last.reify
31
+ expect(skipper.another_timestamp).to be(nil)
32
+ end
33
+ end
34
+
35
+ context "with preserve" do
36
+ it "should preserve its timestamp" do
37
+ skipper = Skipper.create!(:another_timestamp => t1)
38
+ skipper.update_attributes!(:another_timestamp => t2, :name => "Foobar")
39
+ skipper = skipper.versions.last.reify(:unversioned_attributes => :preserve)
40
+ expect(skipper.another_timestamp).to eq(t2)
41
+ end
42
+ end
14
43
  end
15
44
  end
16
45
  end
@@ -51,8 +51,8 @@ describe Widget, :type => :model do
51
51
  describe :after_create do
52
52
  let(:widget) { Widget.create!(:name => 'Foobar', :created_at => Time.now - 1.week) }
53
53
 
54
- it "corresponding version should use the widget's `created_at`" do
55
- expect(widget.versions.last.created_at.to_i).to eq(widget.created_at.to_i)
54
+ it "corresponding version should use the widget's `updated_at`" do
55
+ expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i)
56
56
  end
57
57
  end
58
58
 
@@ -1,7 +1,8 @@
1
1
  class SetUpTestTables < ActiveRecord::Migration
2
2
  def self.up
3
3
  create_table :skippers, :force => true do |t|
4
- t.datetime :another_timestamp
4
+ t.string :name
5
+ t.datetime :another_timestamp
5
6
  t.timestamps :null => true
6
7
  end
7
8
 
@@ -13,77 +13,108 @@
13
13
 
14
14
  ActiveRecord::Schema.define(version: 20110208155312) do
15
15
 
16
- create_table "animals", force: true do |t|
16
+ create_table "animals", force: :cascade do |t|
17
17
  t.string "name"
18
18
  t.string "species"
19
19
  end
20
20
 
21
- create_table "articles", force: true do |t|
21
+ create_table "articles", force: :cascade do |t|
22
22
  t.string "title"
23
23
  t.string "content"
24
24
  t.string "abstract"
25
25
  t.string "file_upload"
26
26
  end
27
27
 
28
- create_table "authorships", force: true do |t|
28
+ create_table "authorships", force: :cascade do |t|
29
29
  t.integer "book_id"
30
30
  t.integer "person_id"
31
31
  end
32
32
 
33
- create_table "books", force: true do |t|
33
+ create_table "banana_versions", force: :cascade do |t|
34
+ t.string "item_type", null: false
35
+ t.integer "item_id", null: false
36
+ t.string "event", null: false
37
+ t.string "whodunnit"
38
+ t.text "object"
39
+ t.datetime "created_at"
40
+ end
41
+
42
+ add_index "banana_versions", ["item_type", "item_id"], name: "index_banana_versions_on_item_type_and_item_id"
43
+
44
+ create_table "bananas", force: :cascade do |t|
45
+ t.datetime "created_at"
46
+ t.datetime "updated_at"
47
+ end
48
+
49
+ create_table "books", force: :cascade do |t|
34
50
  t.string "title"
35
51
  end
36
52
 
37
- create_table "customers", force: true do |t|
53
+ create_table "boolits", force: :cascade do |t|
54
+ t.string "name"
55
+ t.boolean "scoped", default: true
56
+ end
57
+
58
+ create_table "customers", force: :cascade do |t|
38
59
  t.string "name"
39
60
  end
40
61
 
41
- create_table "documents", force: true do |t|
62
+ create_table "documents", force: :cascade do |t|
42
63
  t.string "name"
43
64
  end
44
65
 
45
- create_table "editors", force: true do |t|
66
+ create_table "editors", force: :cascade do |t|
46
67
  t.string "name"
47
68
  end
48
69
 
49
- create_table "editorships", force: true do |t|
70
+ create_table "editorships", force: :cascade do |t|
50
71
  t.integer "book_id"
51
72
  t.integer "editor_id"
52
73
  end
53
74
 
54
- create_table "fluxors", force: true do |t|
75
+ create_table "fluxors", force: :cascade do |t|
55
76
  t.integer "widget_id"
56
77
  t.string "name"
57
78
  end
58
79
 
59
- create_table "gadgets", force: true do |t|
80
+ create_table "fruits", force: :cascade do |t|
81
+ t.string "name"
82
+ t.string "color"
83
+ end
84
+
85
+ create_table "gadgets", force: :cascade do |t|
60
86
  t.string "name"
61
87
  t.string "brand"
62
88
  t.datetime "created_at"
63
89
  t.datetime "updated_at"
64
90
  end
65
91
 
66
- create_table "legacy_widgets", force: true do |t|
92
+ create_table "legacy_widgets", force: :cascade do |t|
67
93
  t.string "name"
68
94
  t.integer "version"
69
95
  end
70
96
 
71
- create_table "line_items", force: true do |t|
97
+ create_table "line_items", force: :cascade do |t|
72
98
  t.integer "order_id"
73
99
  t.string "product"
74
100
  end
75
101
 
76
- create_table "orders", force: true do |t|
102
+ create_table "not_on_updates", force: :cascade do |t|
103
+ t.datetime "created_at"
104
+ t.datetime "updated_at"
105
+ end
106
+
107
+ create_table "orders", force: :cascade do |t|
77
108
  t.integer "customer_id"
78
109
  t.string "order_date"
79
110
  end
80
111
 
81
- create_table "people", force: true do |t|
112
+ create_table "people", force: :cascade do |t|
82
113
  t.string "name"
83
114
  t.string "time_zone"
84
115
  end
85
116
 
86
- create_table "post_versions", force: true do |t|
117
+ create_table "post_versions", force: :cascade do |t|
87
118
  t.string "item_type", null: false
88
119
  t.integer "item_id", null: false
89
120
  t.string "event", null: false
@@ -96,31 +127,38 @@ ActiveRecord::Schema.define(version: 20110208155312) do
96
127
 
97
128
  add_index "post_versions", ["item_type", "item_id"], name: "index_post_versions_on_item_type_and_item_id"
98
129
 
99
- create_table "post_with_statuses", force: true do |t|
130
+ create_table "post_with_statuses", force: :cascade do |t|
100
131
  t.integer "status"
101
132
  end
102
133
 
103
- create_table "posts", force: true do |t|
134
+ create_table "posts", force: :cascade do |t|
104
135
  t.string "title"
105
136
  t.string "content"
106
137
  end
107
138
 
108
- create_table "songs", force: true do |t|
139
+ create_table "skippers", force: :cascade do |t|
140
+ t.string "name"
141
+ t.datetime "another_timestamp"
142
+ t.datetime "created_at"
143
+ t.datetime "updated_at"
144
+ end
145
+
146
+ create_table "songs", force: :cascade do |t|
109
147
  t.integer "length"
110
148
  end
111
149
 
112
- create_table "things", force: true do |t|
150
+ create_table "things", force: :cascade do |t|
113
151
  t.string "name"
114
152
  end
115
153
 
116
- create_table "translations", force: true do |t|
154
+ create_table "translations", force: :cascade do |t|
117
155
  t.string "headline"
118
156
  t.string "content"
119
157
  t.string "language_code"
120
158
  t.string "type"
121
159
  end
122
160
 
123
- create_table "version_associations", force: true do |t|
161
+ create_table "version_associations", force: :cascade do |t|
124
162
  t.integer "version_id"
125
163
  t.string "foreign_key_name", null: false
126
164
  t.integer "foreign_key_id"
@@ -129,7 +167,7 @@ ActiveRecord::Schema.define(version: 20110208155312) do
129
167
  add_index "version_associations", ["foreign_key_name", "foreign_key_id"], name: "index_version_associations_on_foreign_key"
130
168
  add_index "version_associations", ["version_id"], name: "index_version_associations_on_version_id"
131
169
 
132
- create_table "versions", force: true do |t|
170
+ create_table "versions", force: :cascade do |t|
133
171
  t.string "item_type", null: false
134
172
  t.integer "item_id", null: false
135
173
  t.string "event", null: false
@@ -149,13 +187,13 @@ ActiveRecord::Schema.define(version: 20110208155312) do
149
187
 
150
188
  add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
151
189
 
152
- create_table "whatchamajiggers", force: true do |t|
190
+ create_table "whatchamajiggers", force: :cascade do |t|
153
191
  t.string "owner_type"
154
192
  t.integer "owner_id"
155
193
  t.string "name"
156
194
  end
157
195
 
158
- create_table "widgets", force: true do |t|
196
+ create_table "widgets", force: :cascade do |t|
159
197
  t.string "name"
160
198
  t.text "a_text"
161
199
  t.integer "an_integer"
@@ -171,7 +209,7 @@ ActiveRecord::Schema.define(version: 20110208155312) do
171
209
  t.datetime "updated_at"
172
210
  end
173
211
 
174
- create_table "wotsits", force: true do |t|
212
+ create_table "wotsits", force: :cascade do |t|
175
213
  t.integer "widget_id"
176
214
  t.string "name"
177
215
  t.datetime "created_at"