Building an Active Record scope of 'editor approved' versions of user-generated content

179 Views Asked by At

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.

I could use a class method instead like this:

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.

Alternatively I could use a Post instance method like:

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.

1

There are 1 best solutions below

7
kik On BEST ANSWER

This sounds over complicated for doing something that is quite simple by itself:

  • you have a Post model, which contains common attributes (like author and creation date)
  • Post has_many :revisions
  • a Revision has a reviewed boolean attribute

If 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 Posts 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 %>