All threads / dealing with authorized parts in fragment caching

Ask A Question

Notifications

You’re not receiving notifications from this thread.

dealing with authorized parts in fragment caching

Sean M asked in General

I have a facebook/disqus like news feed in my app. Every action handled via AJAX on the posts index page. I have hard time figuring out how to do the russian-doll-catching when there are authorized parts in the partials. By authorized parts I mean editing/deleting link should only be visible for the post/post_comment/post_comment creator.

At the moment with the following code after creation the edit/delete links also get cached and don't change based on the current_user. What is the good approach here to handle this issue?

Post has many :post_comments, touch: true, post_comment has_many :post_comment_replies, touch: true.

posts index.html.erb

<% cache ["posts-index", @posts.map(&:id), @posts.map(&:updated_at).max, @posts.map {|post| post.user.profile.updated_at}.max] do %>
    <%= render @posts %>
<% end %>

_post.html.erb

<% cache ['post', post, post.user.profile] do %>
  <%= post.body %>
  <% if policy(post).edit? && policy(post).delete? %> #this is the part that should only be visible to the creator
    <%= link_to "Edit Post", edit_post_path(post), remote: true............... %> 
  <% end %>
  <%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.includes(:user, :user_profile), as: :post_comment, locals: {post: post} %>
<% end %>

_post_comment.htmle.erb

<% cache ['post-comment', post_comment, post_comment.user.profile] do %>
  <%= post_comment.body %>
  <% if policy(post_comment).edit? && policy(post_comment).delete? %> #this is the part for comments that should only be visible to the creator
    <%= link_to "Edit Post Comment", edit_post_post_comment_path(@post, post_comment), remote: true............... %>
  <% end %>
  <%= render partial: 'posts/post_comment_replies/post_comment_reply', collection: post_comment.post_comment_replies.ordered.includes(:user, :user_profile), as: :post_comment_reply, locals: { post_comment: post_comment } %>
<% end %>

_post_comment_reply.html.erb

<% cache ['post-comment-reply', post_comment_reply, post_comment_reply.user.profile] do %>
  <%= post_comment_reply.body %>
  <% if policy(post_comment_reply).edit? && policy(post_comment_reply).delete? %> #here the same again
    <%= link_to "Edit Reply", edit_post_comment_post_comment_reply_path(@post_comment, post_comment_reply), remote: true............... %>
  <% end %>
<% end %>

After watching this video https://www.youtube.com/watch?v=ktZLpjCanvg with DHH I ended up using JS that loads the authorized parts like this:

$(document).on("page:change", function() {
  if ($('.post-container').length > 0) {
    collectionPostEditDropdown();  
  };
});

function collectionPostEditDropdown() {
  $('.edit-post-dropdown-button').each(function(index) {
    if ($(this).data('postauthorid') == $('#bodycurrentuser').data('currentuserid')) {
      $(this).removeClass('hidden');
    };
  });
};

The main recommendation is to try to write every cache with good defaults that's generic to the current user. For example, you'd write the cache and always include the edit link, but make it hidden by default. Then you can add some JS to display that link when the user has permissions. Obviously you'll need to write something to let the JS know what permissions the user has, so it changes things a bit.

The primary reason for having generic caches is that you end up not storing N number of copies of the cache because you have N number of users on your site. It's not good to have 1 million copies of a cache because you have 1 million users (of course). :)

DHH also talked about a similar situation in another blog or video where he mentioned handling the "share" form for Basecamp. Basically it's a list of all the users an object is shared with. They actually cache the form that includes yourself in it and then use JS to hide your name from the list because it would be weird to share something with yourself. Having a single cache really improves this and a small sprinkling of JS on top cleans it up nicely.

I'll have to do the next episode on this!

Thanks Chris, I guess I'm just doing what you are talking about. I load all the links and if the current_user is the post author (what I check with data-attrs) then I remove the hidden class.

Can't wait to see that episode! Could you also include some complex cache keys in that episode? I mean I struggled a bit when I had to include other models in the cache key like this:

On the tasks index page I display the task.executor.profile.name and the task.assigner.profile.name. The other day I realized when a profile gets updated the profile.name on the tasks index page doesn't change. I ended up using the following, but still not sure if this is the preferred way to do it.

<% cache ['tasks-index', @tasks.map(&:id), @tasks.map(&:updated_at).max, @tasks.map{|task| task.assigner.profile.updated_at}.max, @tasks.map{|task| task.executor.profile.updated_at}.max] do %>
  <%= render @tasks %>
<% end %>

Just did an episode on this! https://gorails.com/episodes/advanced-caching-user-permissions-and-authorization?autoplay=1

Cache keys are definitely tough because you have to make sure that no matter what when something changes, you get that a new cache key. Setting up extra "touch" methods so that you make sure all the parents and related models get updated_at changes is pretty important, but it always ends up coming down to the individual app you're building on some level.

Join the discussion

Want to stay up-to-date with Ruby on Rails?

Join 35,095+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.

    logo Created with Sketch.

    Ruby on Rails tutorials, guides, and screencasts for web developers learning Ruby, Rails, Javascript, Turbolinks, Stimulus.js, Vue.js, and more. Icons by Icons8

    © 2020 GoRails, LLC. All rights reserved.