Search code examples
ruby-on-railscachingauthorizationpunditfragment-caching

rails leaving out some parts from fragment caching


I have a rails 4 app using pundit gem for authorization. If I do russian-doll fragment caching like the code below, the conditional statement used for authorization will be also cached, which is not good, since edit/delete buttons should only be available for the post.user.

What is the good way to get around this? Should I split the cache into smaller parts or is there a way to exclude some parts of the caching? What's the rails convention in this case?

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

  <div class="row>
    <div class="col-md-2">
      <%= link_to user_path(post.user) do %>
        <%= image_tag post.user.avatar.url(:base_thumb), class: 'post-avatar' %>
      <% end %>
    </div>

    <div class="col-md-8">
      <span class="post-user-name"><%= post.user.full_name %></span>
      <span class="post-updated"><%= local_time_ago(post.updated_at) %></span>
      <div class="post-body">
        <%= post.body %>
      </div>

    <div class="col-md-2" style="text-align:right;">

      <!--############### THIS IS THE PART THAT SHOULD NOT BE CACHED #############-->

      <% if policy(post).edit? && policy(post).delete? %> 
        <li class="dropdown">
          <ul class = "dropdown-menu dropdown-menu-right">
            <li>
              <%= link_to "Edit Post", edit_post_path(post), remote: true, type: "button", 'data-toggle' => "modal", 'data-target' => "#updatepost_#{post.id}" %>
            </li>   
            <li>
              <a href="#" data-toggle="modal" role="button" data-target="#deletepost_<%= post.id %>">Delete Post</a>
            </li>
          </ul>
        </li>
      <% end %>

      <!--########################## UNTIL HERE ############################-->

    </div>
  </div>

  <div class = "row comment-top-row" style="padding-bottom:10px;">
    <div class="col-md-12 post-comment-form">
      <%= render partial: 'posts/post_comments/post_comment_form', locals: { post: post } %>
    </div>
  </div>

  <div class = "row">
    <div class="col-md-12 post-comment-insert-<%= post.id%>">
      <%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>
    </div>
  </div>

  <% if policy(post).edit? %>
    <div class="modal fade updatepost" id="updatepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
       <!-- FORM GETS RENDERED HERE VIA JS -->
    </div>
  <% end %>

  <% if policy(post).delete? %>
    <div class="modal fade" id="deletepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
      ......
    </div>
  <% end %>

<% end %>

Solution

  • Russian Doll Caching is simple but handy way for caching, there's no complex options or convention to exclude part of fragment from it. Above that, It's more relative to cache strategies. Here are two strategies for this user specific situation:

    1. Rearrange individually and Cache fragments manually, which I don't recommend. Because it's more complex and doesn't leverage the advantages of Russian Doll Caching. Not so maintainable as well. Here is an example:

    index.html.erb

    <% # pull out cache %>
    <%= render @posts %>
    

    _post.html.erb

    <% cache post %>
      <%= # first part %>
    <% end %>
    
    <% # without cache %>
    <%= # user specific part %>
    
    <% cache post %>
      <%= # third part %>
    <% end %>
    
    1. Preferred way: Add current_user as part of cache_key, which means you will have as many fragment caches as approximately your users and the fragments will automatically invalidate whenever the post or the user has changed their fingerprint. This is more elegant and maintainable. Here is an example:

    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, current_user ] do %>
      <div class="row>
        <div class="col-md-2">
          <%= link_to user_path(post.user) do %>
            <%= image_tag post.user.avatar.url(:base_thumb), class: 'post-avatar' %>
          <% end %>
        </div>
    
        <div class="col-md-8">
          <span class="post-user-name"><%= post.user.full_name %></span>
          <span class="post-updated"><%= local_time_ago(post.updated_at) %></span>
          <div class="post-body">
            <%= post.body %>
          </div>
    
        <div class="col-md-2" style="text-align:right;">
          <% if policy(post).edit? && policy(post).delete? %> 
            <li class="dropdown">
              <ul class = "dropdown-menu dropdown-menu-right">
                <li>
                  <%= link_to "Edit Post", edit_post_path(post), remote: true, type: "button", 'data-toggle' => "modal", 'data-target' => "#updatepost_#{post.id}" %>
                </li>   
                <li>
                  <a href="#" data-toggle="modal" role="button" data-target="#deletepost_<%= post.id %>">Delete Post</a>
                </li>
              </ul>
            </li>
          <% end %>
        </div>
      </div>
    
      <div class = "row comment-top-row" style="padding-bottom:10px;">
        <div class="col-md-12 post-comment-form">
          <%= render partial: 'posts/post_comments/post_comment_form', locals: { post: post } %>
        </div>
      </div>
    
      <div class = "row">
        <div class="col-md-12 post-comment-insert-<%= post.id%>">
          <%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>
        </div>
      </div>
    
      <% if policy(post).edit? %>
        <div class="modal fade updatepost" id="updatepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
           <!-- FORM GETS RENDERED HERE VIA JS -->
        </div>
      <% end %>
    
      <% if policy(post).delete? %>
        <div class="modal fade" id="deletepost_<%= post.id %>" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
          ......
        </div>
      <% end %>
    <% end %>