has_moderated 1.0.rc4 → 1.0.rc5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Add it to your project's Gemfile
7
7
 
8
- gem "has_moderated"
8
+ gem "has_moderated", ">=1.0.rc4"
9
9
 
10
10
  and run
11
11
 
@@ -28,6 +28,8 @@ When upgrading, rerun the generator
28
28
 
29
29
  If there is a new migration file and you have filename conflicts, remove the old one and apply the new one, in case the schema changed. This is especially important if you are upgrading from a previous version to v1.x.
30
30
 
31
+ Before upgrading make sure you have reviewed and accepted/declined all current moderations first! This means the moderations table in database must be empty, otherwise the migration process will fail (on purpose). I suggest you modify the migration generated by has_moderated:install to first accept or discard all existing moderations, based on your preference - or do it in your administrative interface before deploying the upgrade.
32
+
31
33
  == Why use this instead of something like Papertrail?
32
34
 
33
35
  Papertrail and vestal_versions for example, are designed not for moderation, but for tracking changes on models. If you use Papertrail for moderating, you will always have the "newest" version in the database, while previous changes will be recorded in a special table. The problem with this for moderation is that when you are showing records, you have to read from both the model table and the "changes" table in the database, and do additional processing to combine them. This will impact performance quite a bit.
@@ -110,16 +112,33 @@ Do not use moderation.destroy, because discard triggers certain callbacks which
110
112
 
111
113
  == Preview moderation
112
114
 
113
- You can see a preview of what a moderation will do. Right now, you can only preview changes to attributes, not associations. To create a preview of how a record will look after applying a moderation, use
115
+ You can see a preview of what a moderation will do.
114
116
 
115
- record_preview = moderation.preview
116
-
117
- This will return the record with modified attributes, but not saved to database. You can see what changed by looking at (provided by ActiveRecord)
117
+ === Get a read-only preview
118
118
 
119
- record_preview.changes
119
+ This method allows you to retrieve a fake object which behaves like an ActiveRecord object, but is read-only.
120
+ This object allows you to see the value of all attributes, as well as all the associations (including non-moderated).
121
+ It also supports dirty tracking through ActiveModel, so you can use all its functionality (http://api.rubyonrails.org/classes/ActiveModel/Dirty.html).
122
+
123
+ preview = moderation.preview
124
+ preview.some_attr # => "new value"
125
+ preview.some_attr_change # => ["old value", "new value"]
126
+ preview.some_association.first # => #<HasModerated::FakeTask @fake_of_model=Task, @attributes={"id"=>1, "title"=>"Task 1", "desc"=>nil, "created_at"=>"2012-06-08 15:49:56.726646", "updated_at"=>"2012-06-08 15:49:56.726646"}, @changed_attributes={}, reflections.keys => [:assoc1, :assoc2]>
120
127
 
121
- Just be careful not to call record_preview.save, as this will create a new moderation record.
128
+ This preview is cached from applying the moderation in a transaction and then rolling it back (so it does not *really* affect the database).
129
+
130
+ === Get a real ActiveRecord preview
122
131
 
132
+ This way, you will get a real ActiveRecord object that is really in the database. You can do whatever you want on it, however the difference is that you need to use it inside the live_preview block. This is because the moderation is applied in a transaction and after you are done using the preview, the transaction will be rolled back. I would recommend using the read-only fake object (see above) if you can, as it is safer. But sometimes you just need access to the real ActiveRecord object.
133
+
134
+ moderation.live_preview do |preview|
135
+ preview.some_attr # => "new value"
136
+ # note how we need to use .previous_changes when using live_preview,
137
+ # .changes will be blank as the record has already been saved to DB
138
+ preview.previous_changes # => {"attr"=>["old", "new"]}
139
+ preview.some_association.first # => ActiveRecord object
140
+ end
141
+
123
142
  == Which kind of moderation is it?
124
143
 
125
144
  You can call the following convenience methods to determine the type of moderation
@@ -227,6 +246,8 @@ It is also possible you will encounter problems with some heavily customized ass
227
246
 
228
247
  This is just for my personal todo list...
229
248
 
249
+ improve functionality when rerunning has_moderated:install generator
250
+
230
251
  hm maybe has_moderated_association could be just a has_moderated_create on the assoc? just check stuff like if task_id is not nil or something. could be much easier to implement then
231
252
 
232
253
  also maybe use dirty https://github.com/rails/rails/blob/master/activemodel/lib/active_model/dirty.rb
data/Rakefile CHANGED
@@ -23,9 +23,14 @@ end
23
23
  Bundler::GemHelper.install_tasks
24
24
 
25
25
  require 'rspec/core/rake_task'
26
-
27
26
  RSpec::Core::RakeTask.new(:spec) do |spec|
27
+ spec.rspec_opts = %w(--color)
28
28
  spec.pattern = 'test/dummy/spec{,/*/**}/*_spec.rb'
29
29
  end
30
30
 
31
- task :default => :spec
31
+ task :prepare_test_env do
32
+ puts "Preparing test environment..."
33
+ system("cd test/dummy && RAILS_ENV=test rake db:schema:load") # db:test:prepare
34
+ end
35
+
36
+ task :default => [:spec]
@@ -130,12 +130,7 @@ module HasModerated
130
130
  end
131
131
 
132
132
  # add/delete associations to a record
133
- def self.apply(moderation, data)
134
- record = if moderation.kind_of? Moderation
135
- moderation.moderatable
136
- else
137
- moderation
138
- end
133
+ def self.apply(record, data)
139
134
  associations = data[:associations]
140
135
  delete_associations = data[:delete_associations]
141
136
 
@@ -154,6 +149,8 @@ module HasModerated
154
149
  apply_delete_association(record, reflection, attrs) if attrs.present?
155
150
  end
156
151
  end
152
+
153
+ record
157
154
  end
158
155
  end # module
159
156
  end
@@ -51,16 +51,13 @@ module HasModerated
51
51
  end
52
52
 
53
53
  module ApplyModeration
54
- def self.apply(moderation, value, do_save = true)
54
+ def self.apply(rec, value)
55
55
  if value[:attributes].present?
56
- rec = moderation.moderatable
57
56
  rec.without_moderation do
58
57
  value[:attributes].each_pair do |attr_name, attr_value|
59
58
  # bypass attr_accessible protection
60
59
  rec.send(attr_name.to_s+"=", attr_value)
61
60
  end
62
- # don't run validations on save (were already ran when moderation was created)
63
- rec.save(:validate => false) if do_save
64
61
  end
65
62
  end
66
63
  rec
@@ -16,19 +16,20 @@ module HasModerated
16
16
  end
17
17
 
18
18
  module ApplyModeration
19
- def self.apply(moderation, value)
19
+ def self.apply(klass, value)
20
+ rec = nil
20
21
  if value[:create].present?
21
22
  # create the main record
22
- rec = moderation.moderatable_type.constantize.new
23
+ rec = klass.new
23
24
  attrs = value[:create][:attributes]
24
25
  # bypass attr_accessible protection
25
26
  attrs && attrs.each_pair do |key, val|
26
27
  rec.send(key.to_s+"=", val) unless key.to_s == 'id'
27
28
  end
28
29
  rec.without_moderation { rec.save(:validate => false) }
29
- moderation.moderatable = rec # just so associations can be applied in next line
30
- HasModerated::Associations::Base::ApplyModeration::apply(moderation, value[:create])
30
+ HasModerated::Associations::Base::ApplyModeration::apply(rec, value[:create])
31
31
  end
32
+ rec
32
33
  end
33
34
  end
34
35
 
@@ -12,9 +12,12 @@ module HasModerated
12
12
  end
13
13
 
14
14
  module ApplyModeration
15
- def self.apply(moderation, value)
15
+ def self.apply(record, value)
16
16
  if value[:destroy] == true
17
- moderation.moderatable.without_moderation { |m| m.destroy }
17
+ record.without_moderation { |m| m.destroy }
18
+ nil
19
+ else
20
+ record
18
21
  end
19
22
  end
20
23
  end
@@ -4,13 +4,35 @@ module HasModerated
4
4
  @parsed_data ||= YAML::load(data)
5
5
  end
6
6
 
7
+ def apply
8
+ if create?
9
+ record = HasModerated::ModeratedCreate::ApplyModeration::apply(moderatable_type.constantize, parsed_data)
10
+ else
11
+ record = moderatable
12
+ if record
13
+ record = HasModerated::ModeratedAttributes::ApplyModeration::apply(record, parsed_data)
14
+ record = HasModerated::Associations::Base::ApplyModeration::apply(record, parsed_data)
15
+ record = HasModerated::ModeratedDestroy::ApplyModeration::apply(record, parsed_data)
16
+ end
17
+ end
18
+ record
19
+ end
20
+
21
+ def accept_changes(record)
22
+ if record
23
+ HasModerated::Common::try_without_moderation(record) do
24
+ # don't run validations on save (were already ran when moderation was created)
25
+ record.save(:validate => false)
26
+ end
27
+ end
28
+ record
29
+ end
30
+
7
31
  def accept
8
- loaded_val = parsed_data
9
- HasModerated::ModeratedCreate::ApplyModeration::apply(self, loaded_val)
10
- HasModerated::ModeratedAttributes::ApplyModeration::apply(self, loaded_val)
11
- HasModerated::Associations::Base::ApplyModeration::apply(self, loaded_val)
12
- HasModerated::ModeratedDestroy::ApplyModeration::apply(self, loaded_val)
32
+ record = apply
33
+ accept_changes(record)
13
34
  self.destroy
35
+ record
14
36
  end
15
37
 
16
38
  def discard
@@ -21,8 +43,21 @@ module HasModerated
21
43
  self.destroy
22
44
  end
23
45
 
46
+ def live_preview
47
+ ActiveRecord::Base.transaction do
48
+ record = accept
49
+ yield(record)
50
+ raise ActiveRecord::Rollback
51
+ end
52
+ nil
53
+ end
54
+
24
55
  def preview
25
- HasModerated::ModeratedAttributes::ApplyModeration::apply(self, parsed_data, false)
56
+ fake_record = nil
57
+ live_preview do |record|
58
+ fake_record = HasModerated::Preview::from_live(record)
59
+ end
60
+ fake_record
26
61
  end
27
62
 
28
63
  def create?
@@ -0,0 +1,86 @@
1
+ require 'ostruct'
2
+ module HasModerated
3
+ module Preview
4
+ class FakeRecord
5
+ extend ActiveModel::Naming
6
+ include ActiveModel::Conversion
7
+ include ActiveModel::Dirty
8
+
9
+ attr_accessor :attributes
10
+
11
+ def initialize(fake_of_model)
12
+ @fake_of_model = fake_of_model
13
+ end
14
+
15
+ def persisted?
16
+ false
17
+ end
18
+
19
+ def attribute name
20
+ attributes[name]
21
+ end
22
+
23
+ def to_s
24
+ "#<HasModerated::Fake#{@fake_of_model.to_s}>"
25
+ end
26
+
27
+ def inspect
28
+ to_s.chomp(">") +
29
+ instance_variables.map{|name| " #{name}=#{instance_variable_get(name)}"}.join(",") +
30
+ ", reflections.keys => [" + reflections.keys.map{|s| ":#{s}"}.join(", ")+ "]>"
31
+ end
32
+ end
33
+
34
+ def self.create_fake_association(attr_name, value, target)
35
+ target.send(:define_method, attr_name) do
36
+ value
37
+ end
38
+ end
39
+
40
+ def self.resolve_record(record, cache)
41
+ return nil if record.blank?
42
+ cache[record.class] ||= Hash.new
43
+ if cache[record.class][record.id].present?
44
+ cache[record.class][record.id]
45
+ else
46
+ cache[record.class][record.id] = from_live(record, cache)
47
+ end
48
+ end
49
+
50
+ def self.from_live(record, object_cache = nil)
51
+ return nil if record.blank?
52
+ obj = FakeRecord.new(record.class)
53
+ eigenclass = (class << obj ; self ; end)
54
+
55
+ obj.instance_variable_set(:@attributes, record.instance_variable_get(:@attributes))
56
+ changed_attributes = Hash.new
57
+ record.previous_changes.each_pair do |attr_name, values|
58
+ changed_attributes[attr_name] = values[0]
59
+ end
60
+ obj.instance_variable_set(:@changed_attributes, changed_attributes)
61
+
62
+ # associations
63
+ object_cache ||= Hash.new
64
+ object_cache[record.class] ||= Hash.new
65
+ object_cache[record.class][record.id] = obj
66
+ eigenclass.send(:define_method, :reflections) do
67
+ record.class.reflections.reject{|k,v| k.to_sym == :moderations}
68
+ end
69
+ record.class.reflections.values.reject{|s| s.name.to_sym == :moderations}.each do |reflection|
70
+ if reflection.macro == :has_one || reflection.macro == :belongs_to
71
+ create_fake_association(
72
+ reflection.name,
73
+ resolve_record(record.send(reflection.name), object_cache),
74
+ eigenclass)
75
+ elsif reflection.collection?
76
+ create_fake_association(
77
+ reflection.name,
78
+ record.send(reflection.name).map{|r| resolve_record(r, object_cache)},
79
+ eigenclass)
80
+ end
81
+ end
82
+
83
+ obj
84
+ end
85
+ end
86
+ end
@@ -1,3 +1,3 @@
1
1
  module HasModerated
2
- VERSION = "1.0.rc4"
2
+ VERSION = "1.0.rc5"
3
3
  end
data/lib/has_moderated.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'has_moderated/common'
2
2
  require 'has_moderated/user_hooks'
3
3
  require 'has_moderated/moderation_model'
4
+ require 'has_moderated/moderation_preview'
4
5
 
5
6
  require 'has_moderated/active_record/active_record_helpers'
6
7
  require 'has_moderated/associations/base'
Binary file
Binary file
@@ -0,0 +1,72 @@
1
+ Connecting to database specified by database.yml
2
+  (0.2ms) select sqlite_version(*)
3
+  (1.3ms) CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL)
4
+  (0.0ms) PRAGMA index_list("schema_migrations")
5
+  (1.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
6
+  (0.1ms) SELECT "schema_migrations"."version" FROM "schema_migrations" 
7
+ Connecting to database specified by database.yml
8
+  (1.7ms) select sqlite_version(*)
9
+  (1.5ms) CREATE TABLE "moderations" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "moderatable_id" integer, "moderatable_type" varchar(255), "data" text NOT NULL, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
10
+  (1.2ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "avatar" varchar(255), "picture" varchar(255), "parentable_id" integer, "parentable_type" varchar(255), "title" varchar(255), "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) 
11
+  (2.0ms) CREATE TABLE "subtasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "task_id" integer, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime, "parentable_id" integer, "parentable_type" varchar(255))
12
+  (1.2ms) CREATE TABLE "task_connections" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "m1_id" integer, "m2_id" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) 
13
+  (1.3ms) CREATE TABLE "task_photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "photo" varchar(255), "task_id" integer, "created_at" datetime, "updated_at" datetime)
14
+  (1.1ms) CREATE TABLE "tasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime) 
15
+  (1.1ms) CREATE TABLE "tasks_jointable" ("m1_id" integer, "m2_id" integer)
16
+  (0.1ms) SELECT version FROM "schema_migrations"
17
+  (2.4ms) INSERT INTO "schema_migrations" (version) VALUES ('20120520215224')
18
+  (1.0ms) INSERT INTO "schema_migrations" (version) VALUES ('20120520215008')
19
+ Connecting to database specified by database.yml
20
+  (0.1ms) SELECT "schema_migrations"."version" FROM "schema_migrations" 
21
+  (0.4ms) select sqlite_version(*)
22
+  (65.9ms) CREATE TABLE "moderations" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "moderatable_id" integer, "moderatable_type" varchar(255), "data" text NOT NULL, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) 
23
+  (21.1ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "avatar" varchar(255), "picture" varchar(255), "parentable_id" integer, "parentable_type" varchar(255), "title" varchar(255), "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
24
+  (2.0ms) CREATE TABLE "subtasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "task_id" integer, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime, "parentable_id" integer, "parentable_type" varchar(255)) 
25
+  (1.1ms) CREATE TABLE "task_connections" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "m1_id" integer, "m2_id" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
26
+  (1.6ms) CREATE TABLE "task_photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "photo" varchar(255), "task_id" integer, "created_at" datetime, "updated_at" datetime) 
27
+  (1.1ms) CREATE TABLE "tasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime)
28
+  (2.0ms) CREATE TABLE "tasks_jointable" ("m1_id" integer, "m2_id" integer) 
29
+  (1.1ms) CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL)
30
+  (0.0ms) PRAGMA index_list("schema_migrations")
31
+  (1.2ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
32
+  (0.1ms) SELECT version FROM "schema_migrations"
33
+  (1.2ms) INSERT INTO "schema_migrations" (version) VALUES ('20120520215224')
34
+  (2.4ms) INSERT INTO "schema_migrations" (version) VALUES ('20120520215008')
35
+ Connecting to database specified by database.yml
36
+  (1.8ms) select sqlite_version(*)
37
+  (1.4ms) DROP TABLE "moderations"
38
+  (1.2ms) CREATE TABLE "moderations" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "moderatable_id" integer, "moderatable_type" varchar(255), "data" text NOT NULL, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) 
39
+  (1.2ms) DROP TABLE "photos"
40
+  (1.1ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "avatar" varchar(255), "picture" varchar(255), "parentable_id" integer, "parentable_type" varchar(255), "title" varchar(255), "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) 
41
+  (1.1ms) DROP TABLE "subtasks"
42
+  (2.9ms) CREATE TABLE "subtasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "task_id" integer, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime, "parentable_id" integer, "parentable_type" varchar(255)) 
43
+  (1.2ms) DROP TABLE "task_connections"
44
+  (1.2ms) CREATE TABLE "task_connections" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "m1_id" integer, "m2_id" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) 
45
+  (1.1ms) DROP TABLE "task_photos"
46
+  (1.2ms) CREATE TABLE "task_photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "photo" varchar(255), "task_id" integer, "created_at" datetime, "updated_at" datetime) 
47
+  (1.1ms) DROP TABLE "tasks"
48
+  (1.1ms) CREATE TABLE "tasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime) 
49
+  (2.3ms) DROP TABLE "tasks_jointable"
50
+  (1.2ms) CREATE TABLE "tasks_jointable" ("m1_id" integer, "m2_id" integer) 
51
+  (0.1ms) SELECT version FROM "schema_migrations"
52
+ Connecting to database specified by database.yml
53
+  (0.1ms) SELECT "schema_migrations"."version" FROM "schema_migrations" 
54
+  (0.3ms) select sqlite_version(*)
55
+  (1.4ms) CREATE TABLE "moderations" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "moderatable_id" integer, "moderatable_type" varchar(255), "data" text NOT NULL, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL) 
56
+  (1.2ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "avatar" varchar(255), "picture" varchar(255), "parentable_id" integer, "parentable_type" varchar(255), "title" varchar(255), "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
57
+  (1.1ms) CREATE TABLE "subtasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "task_id" integer, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime, "parentable_id" integer, "parentable_type" varchar(255)) 
58
+  (1.1ms) CREATE TABLE "task_connections" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "m1_id" integer, "m2_id" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
59
+  (1.2ms) CREATE TABLE "task_photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "photo" varchar(255), "task_id" integer, "created_at" datetime, "updated_at" datetime) 
60
+  (1.1ms) CREATE TABLE "tasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "desc" varchar(255), "created_at" datetime, "updated_at" datetime)
61
+  (1.1ms) CREATE TABLE "tasks_jointable" ("m1_id" integer, "m2_id" integer) 
62
+  (1.0ms) CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL)
63
+  (0.0ms) PRAGMA index_list("schema_migrations")
64
+  (1.2ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
65
+  (0.1ms) SELECT version FROM "schema_migrations"
66
+  (1.1ms) INSERT INTO "schema_migrations" (version) VALUES ('20120520215224')
67
+  (1.1ms) INSERT INTO "schema_migrations" (version) VALUES ('20120520215008')
68
+ Connecting to database specified by database.yml
69
+ Connecting to database specified by database.yml
70
+ Connecting to database specified by database.yml
71
+ Connecting to database specified by database.yml
72
+ Connecting to database specified by database.yml