I run a site similar to StackOverflow where users can publish content (Posts). There also is a frontpage showcasing featured Posts picked by our editors.
I want to allow our users to edit their Posts anytime, while still making sure nothing appears on our frontpage without it first being reviewed by our editors.
To solve this, I figured we could implement a versioning system like paper_trail
and store the current version_id
whenever we feature a Post. This way we can simply fetch the editor-reviewed content on the frontpage, while still showing the most recent version on less critical parts of the site such as the user's profile. Our editors can periodically review any changes and approve those.
I'm wondering what the cleanest approach is that allows me to select reviewed versions in some controllers and the latest versions in others while keeping a shared interface and minimally duplicated code.
The ideal solution would involve a reviewed
scope so I could simply do Post.reviewed
and get back the reviewed versions, and Post.all
to get the latest versions.
I'm not sure how to go about this though, as getting the reviewed version requires de-serializing the object PaperTrail stores (version.reify
) and that doesn't seem possible within a scope.
def self.reviewed
all.map do |post|
Version.find(post.version_id).reify
end.compact! || Post.none
end
However, this is less than ideal as it's not a real scope and thus not chainable, etc.
def reviewed_version
Version.find(version_id).reify
end
This works in theory, but this means I now have to call this method all over my views, whereas it's the responsibility of the controller to fetch the right data. Let's say I have a render collection: @posts
on both the frontpage and user profile, how does my _post.html.erb
partial know whether to call reviewed_version
or not.
I'm not tied to PaperTrail, but it has a lot of niceties I prefer not having to duplicate. I'm open to exploring other directions if PaperTrail proves too inflexible however.
FWIW, I have looked at Draftsman as well, but it considers the 'draft' as the exception (i.e. in most cases you wouldn't show the draft), whereas in my case I want to show the most recent version on most pages, except for a particular few like the frontpage.
This sounds over complicated for doing something that is quite simple by itself:
Post
model, which contains common attributes (like author and creation date)Post has_many :revisions
Revision
has a reviewed
boolean attributeIf you want to find the last reviewed revision for a Post
, you can have:
class Post < ActiveRecord::Base
has_many :revisions
delegate :content, to: :current_revision # you can even have those to transparently delegate
def current_revision
@current_revision ||= revisions.where( reviewed: true ).order( "created_at desc" ).first
end
end
if you want to retrieve only Post
s which have reviewed revisions:
class Post < ActiveRecord::Base
has_many :revisions
scope :reviewed, ->{ group( 'posts.id' ) ).having( 'count(revisions.id) > 0' ).joins( :revisions ).where( "revisions.reviewed = ?", true ) }
end
This is how I would have done it, let people more familiar with paper_trail
tell you about its way :)
EDIT
As discussed in comments, a decorator may also help to differentiate the revision and delegate methods to it, while still being able to pass an activerecord relation. Which give something in that way:
class Post < ActiveRecord::Base
has_many :revisions
scope :reviewed, ->{ group( 'posts.id' ) ).having( 'count(revisions.id) > 0' ).joins( :revisions ).where( "revisions.reviewed = ?", true ) }
def reviewed_revision
@reviewed_revision ||= revisions.where( reviewed: true ).order( "created_at desc" ).first
end
def latest_revision
@latest_revision ||= revisions.order( "created_at desc" ).first
end
end
class PostDecorator
attr_reader :post, :revision
delegate :content, :updated_at, to: :revision
def self.items( scope, revision_method )
revision_method = :reviewed_revision unless %i[ reviewed_revision latest_revision ].include?( revision_method )
scope.map do |post|
PostDecorator.new( post, post.send( revision_method ) )
end
end
def initalize( post, revision )
@post, @revision = post, revision
end
def method_missing( action_name, *args, &block )
post.send( action_name, *args, &block )
end
end
# view
#
# <% PostDecorator.items( Post.reviewed.limit(10), :reviewed_revision ).each do |post| %>
# <p><%= post.author.name %></p>
# <p><%= simple_format post.content %></p>
# <% end %>