Russian doll caching and permission-based links in view fragment - ruby-on-rails

I've got a view that utilizes Russian Doll caching, where the whole collection of items is cached, and each item in the collection is cached individually within that cache.
However, each item in the collection should show edit/delete links based on the current user's permissions granted through CanCan. So, User A would only see the edit/delete links next to her own posts, but not next to User B's posts.
Well, whenever a post is created by User A, it's cached with the appropriate edit/delete links since she should have them visible based on her permissions. But when User B views the collection, he's served User A's cached post, along with the edit/delete links that he shouldn't see. Granted, CanCan prevents these edit/delete actions from occurring, but the links are still present.
Is there anyway around creating individual caches based on current_user.id and prevent having gobs of versions of (almost) identical cached content?

Is there anyway around creating individual caches based on current_user.id and prevent having gobs of versions of (almost) identical cached content?
Instead of including the user's ID in the cache key, you could include the users' permissions. This would still have duplicate copies of the content, but scales as per your permission model, not the number of users. So instead of the typical:
<% cache("posts/all-#{Post.maximum(:updated_at).try(:to_i)}") do %>
...
<% end %>
you can create a cache key like (assuming current_user returns the authenticated user) and you care about only editing vs. reading:
<% cache("posts/all-#{Post.maximum(:updated_at).try(:to_i)}-#{current_user.can?(:edit, Post) ? :edit : :read}") do %>
...
<% end %>
Note that the cache key generation should probably be extracted to a separate class/helper method.

Related

How to use ahoy to track views of posts?

I have multiple posts and I just want to track all the views (including people without accounts) of the posts (so the page itself). I have two methods that I have tried:
1st method
I have added this ahoy.track "Viewed Post", title: #post.id in my controller and <%= Ahoy::Event.joins(:visit).where(name: "Viewed Post").uniq.count("visits.visitor_id") %> to my view.```. The only problem is that it is displaying 0 and not changing.
2nd method
Added visitable to my post model. Ran a migration to add visit id to posts. Also added <%= Post.joins(:visit).distinct.count(:visit_id) %> to my view. The only problem is that the view count is stuck at 1 and it is the same for all the posts.
What am I doing wrong?
I struggled with this for a while aswell. I was able to mostly get it to work.
Your second method won't do what you want because it is tracking something else. Instead of tracking how many views the post has, it tracks 1 single view. It tracks the view that was used to create the post. It does this by attaching the view to the model.
Your first method is close to what you want. However all the events you store arent actually storing the visit id in them, as events do not do this by default. You need to add the visit_id yourself, usually into the properties variable of the Event.
Here is you would need to do:
First you would place the tracking code in the controller(most likely in the "show" portion):
if not Ahoy::Event.where(name: "Post:#{#post.id}", properties: current_visit.visit_token).exists?
ahoy.track "Post:#{#post.id}", current_visit.visit_token
end
By placing the post id in the name, as well as the text "Post:" it will let it track only views to the Post controller, with the specific id. visit_tokens are unique to a user, and the expire after a given configured time(default 8 hours) so it will only track repeat users if they view the page after the configured time
Next to read the view count you can place something like this in the controller wherever you want to see views(in show, edit, etc):
#views = Ahoy::Event.where(name: "Post:#{#post.id}").count
And then in your views you can just use #views
Note: You aren't supposed to set :properties as a single value, but rather its supposed to hold a hash. However I was not able to figure out how to get it to work.

ActiveAdmin - how to delete ALL objects (not only those on current list page) (Rails ')

On my ruby on Rails app using ActiveAdmin, I wish to delete not only the 30 Users displayed but all the 456 users (that's an example of course).
When I select "select all' and then confirm deletion, it only deletes the 30 users visible on the current screen page.
I want to select ALL users (across all view pages, not only the one I currently see), and then manually deselect the first 4 users (or any I would manually pick on the current view page). So not really deleting ALL users. that's my problem.
How to customize ActiveAdmin to be able to do this ?
Maybe something like this would work:
https://github.com/activeadmin-plugins/active_admin_scoped_collection_actions
Plugin for ActiveAdmin. Provides batch Update and Delete for scoped_collection (Filters + Scope) across all pages.
If you want to delete some users from a list of all of them, I suggest you to write a custom active admin action. Minimize your markup, make it easy to render for browser and prepare for the worst. If you have 1 million records, there is no way it will work properly, there is no solution for that.
I suggest you to accept the fact that user will delete records by using search, probably and if you literally want to be able to delete all you can provide a custom button delete all that will do that for you.
The alternative is write a custom active admin action with a lot of javascript to provide pagination. It's still a lot of custom code, no generic solution provided.
Last alternative, you can disable pagination for that active admin page, but you may have a lot of problems loading the entire table every time
You can override the default batch action to destroy/delete all the users like this:
ActiveAdmin.register User do
batch_action :destroy do |ids|
User.delete_all
redirect_to collection_path, alert: "Deleted all the Users!"
end
end
See this for more information.

Pass XML value from one page other page

I want to pass an XML value from one page to another in a better way.
I am getting this XML value from API:
<hotelist>
<hotel>
<hotelId>109</hotelId>
<hotelName>Hotel Sara</hotelName>
<location>UK,london</location>
</hotel>
<hotel>
<hotelId>110</hotelId>
<hotelName>Radha park</hotelName>
<location>UK,london</location>
</hotel>
<hotel>
<hotelId>111</hotelId>
<hotelName>Hotel Green park</hotelName>
<location>chennai,India</location>
</hotel>
<hotel>
<hotelId>112</hotelId>
<hotelName>Hotel Mauria</hotelName>
<location>paris,France</location>
</hotel>
</hotelist>
I want to pass one hotel:
<hotel>
<hotelId>112</hotelId>
<hotelName>Hotel Mauria</hotelName>
<location>paris,France</location>
</hotel>
to next page.
I am using the Nokogiri gem for parsing XML. For the API next call I have to pass the one hotel to the next page. Which is the best method?
Note: This is just a sample. There are a lot of information bound with the hotel including available room, discount and so on.
So as far as I'm getting this, you are searching for some hotels through a third party service, and then displaying a list. After the user clicks on an item you displaying the detail info
for the hotel.
The easiest way would be having another API endpoint, which can provide the detail information for a specific Hotel id. I guess you're dealing with some really bad API and that's not the case.
There are couple of other options (ordered by complexity level):
There is really not much data and it should fit an simple GET request, so you can just encode the respective hotel information into the URL parameter for the detail
page. Assuming you have set up resourcefull routing and have already parsed the XML into #hotels array of some Hotel models/structs or the like:
<% #hotels.each |hotel| do %>
<% # generates <a href=/hotels/112?hotelName=Hotel+Mauria&location=paris%2C+France'>Hotel Mauria</a>
<%= link_to hotel.hotelName hotel_path(hotel.hotelId, hotelName: hotel.hotelName, location: hotel.location) %>
<% end %>
Encode the info into the respective Hotel DOM elements as data-attributes:
<div class="hotel" data-id="112" data-hotel-name="Mauria" ... />
Then render the detail page on the client side without the server entirely by subscribing to a click event, reading the info stored in the respective data attributes and replace the list with the detail div.
If the third party API is public you could even move the search problem entirely to the client.
Introduce caching of search requests on the server. Then just pick a hotel from the cache
by its id. This would be saving you from doing to much third party requests from your Rails app, which is a weak spot of Rails if deployed on a typical multi-process server.
The simplest way of doing this, would be storing the last search result in a user session, but that
would be probably too memory heavy. If you can expect the hotel information not to change frequently, you could cache it by the query parameters. You could also use some smart caching store like redis and index the entire hotel information, than performing the search on the cache and only in case of the cache miss hit the third party API. But always remember, caching is easy, expiring is hard.
"Everyone should be using low level caching in Rails" could be interesting for implementing a cache.
If you don't mind passing all that information in query parameters:
links = doc.xpath('//hotel').map do |hotel|
hash = Hash.from_xml(hotel.to_xml)
url_for({controller: 'x', action: 'y'}.merge(hash))
# or if you have your link as a string
# "#{link_string}?#{hash.to_param}"
end
If you want to create a link for just one hotel, extract the relevant XML (e.g., using the process described in Uri's answer), and then generate the link as above.
Assuming you have the API XML ready before you render the current page, you could render the relevant hotel data into form fields so that you could post to the next page, something like:
<%= fields_for :hotel do |hf| %>
<% hf.hidden_field :hotelId, value: hash['hotel']['hotelId'] %>
# ...
<% end %>
One optimum way to achieve this is as suggested by Mark Thomas.
However if you still need to pass data between pages you can put all the xml information as a string in a session variable and use it on next page.

Fragment Caching and User avatars/images

We use Gravatar on our website, but we want to let users upload their profile images directly in an effort to improve user experience similarly to what Stackexchange has been doing.
On our website users can follow each other, comment, 'like' and interact in ways that cause content to be generated directly and indirectly.
This means that cache-fragments with user avatars are scattered all over the place, and we can't find a reasonable way to invalidate it without negatively affecting render performance.
Here are the possible solutions we've looked at:
Option 1) Take the Gravatar approach and set very short Expires/Cache-Control max-age headers and recycle the same image filename.
Option 2) Use a placeholder image for all users with a data attribute containing the user ids that are read by JavaScript and used to make a second Ajax request asking the server for up-to-data avatars.
Is there any better way to solve this problem? Are we overlooking something?
I think I understand your question, but Im not sure I understand option 2 as a solution, which may indicate that its not a great solution. If it was me I would just cache the html surrounding the user gravatar, which is being reused, in a top level cache key, keyed by the users id. I.E.
<% cache("user_#{user.id}_profile_image") do %>
<img src="blahblahblah or gravatar helper code or whatev">
<% end %>
If you're concerned about a user uploading a gravatar, and subsequently having a cached default gravatar image, I would say your best options are:
1) In the users profile area where you send them to upload a gravatar, have a refresh link which points to a refresh action which actually invalidates that users cache fragment. i.e.
expire_fragment "user_#{user.id}_profile_image"
(http://guides.rubyonrails.org/caching_with_rails.html)
2) you could instead of using the default gravatar redirect to upload an image, you could intercept the click, send it to your own controller, schedule a background task to be run in 15 minutes or so, and then render a javascript response which redirects them to the actual gravatar page to upload their pic. The background worker would then clear the fragment at a later time when it ran. This assumes they actually upload their image and is alltogether I would say a terrible idea.
Honestly though, Im not sure why you are concerned about caching the gravatar to begin with. It's hitting gravatars servers so it causes no load on your server, storing it yourself seems a bit self defeating to the point of using gravatar. Hopefully your question was simpler (and was just: how to clear default cached gravatar image when user uploads their own image), which can be solved by #1), which will allow you to expire the cached gravatar image and recache it using your own image, after the user uploads their image. (which, next time the page was rendered would recache the image because youd have some logic like:
<% cache("user_#{user.id}_profile_image") do %>
<% if user.has_uploaded_image? %>
<%= display_profile_image %>
<% else %>
<%= display_gravatar_image %>
<% end %>
<% end %>

Rails + Devise: How to restrict a user from editing records that do not belong to him

I'm using Rails, Devise and Mongoid.
A user can only have one project (has_one :profile) but all users (and non authenticated users) can see a list of projects (I have this working). When a user is logged in they see "edit and delete" buttons next to the projects (via wrapping those buttons in <% if user_signed_in? %>). However, a signed in user sees these buttons next to all project and can edit or delete them all.
How do I restrict a logged on user to only be able to edit only his project?
As a bonus, is it possible to show specific content or html around the project that belongs to the signed in user (such as text that says "this is your project" or an additional class around the project's html)?
CanCan is great when you have multiple users being able to modify the same resource, and you need to keep track of who has which permissions. But if it's strictly the case that only the user who owns the project can modify it, CanCan is probably overkill.
Hide the links in the view as suggested by iouri, and then in your controller do something like:
def edit
if current_user != #project.user
# Redirect them to an error page
else
# Render the view
end
end
Better yet, create a method like:
def user_owns_project?
#project.user == current_user
end
Then set up a before filter for edit, update and destroy to redirect to the error page if the user doesn't own the project.
EDIT: Your before filter will also ned to find the project and set #project. CanCan takes care of this for you too with load_and_authorize_resource, but I'd still avoid using it unless you need, or expect to need, fine-grained permissions control.
Devise is to control "authentication", this should not to be your responsibility.
You want to control "authorizations", for that CanCan is better.

Resources