Broken links when doing fragment caching in Rails - ruby-on-rails

Suppose there is a blog with posts, comments and users which can comment. Users have SEO-friendly URLs such as http://localhost:3000/users/john (this can be easily done by using permalink_fu).
The model uses touch to simplify caching:
class Post
has_many :comments
end
class Comment
belongs_to :post, :touch=>true
end
And the view code would be something like this:
<%= cache #post do %>
<h1><%= #post.title %></h1>
<%= #post.content %>
<%= #post.comments.each do |comment| %>
<%= link_to h(comment.user), comment.user %> said:
<%= comment.content %>
<% end %>
<% end %>
Now suppose John changes his nick to Johnny -- his URL changes to http://localhost:3000/users/johnny. Because of doing fragment caching on posts and comments, John's comments will point to John's wrong URL unless the fragment is expired. It can be possible to manually touch or expire all posts that contain comments by John in this example, but in a complex application this would require very complex queries and seems very error-prone.
What's the best practice here? Should I use non-SEO-friendly URLs such as /users/13 instead of /users/john ? Or maybe keep a list of old URLs until the cache is expired? No solution looks good to me.
EDIT: Please note this is just a simplified example -- it's definitely very simple to query posts and touch them in this case. But a complex app implies many relationships among objects which makes it hard to track every object that has a reference to a user. I've researched a bit on this -- Facebook only allows setting your username once, so this problem doesn't exist.

I don't see it would be complex to expire cached posts. Set up a sweeper:
class UserSweeper < ActionController::Caching::Sweeper
observe User
def after_save(user)
user.comments.collect(&:post).each do |post|
expire_fragment post
end
end

I'd use a before_save filter for example
class User
has_many :posts
before_save :touch_posts
private
def touch_posts
Post.update_all({:updated_at => Time.now}, {:user_id => self.id}) if self.login_changed?
true
end
end
One query to update every user's post. Not really complex.

Related

Trying to create two different account types in rails

I am trying to build a simple donation app in rails. In this application, patrons would give amounts of money to clients. Both the patron and the client share large amounts of the same functionality. They are both linked to a user and have a username. However, the client is also supposed to have a content_type and a content_list property. At first glance, my guess is that I want to have both my patron and client inherit from the account class. However, the client has additional functionality, which seems to preclude any STI-based implementation (though I will be the first to admit that my understanding of STI is shaky at best). As it stands, it seems to simply make more sense to write out two separate resources, but I would like to keep my code as DRY as humanly possible. Is there a simple way for me to create the behaviors I want through inheritance, or should I simply go with overlapping resources?
Here's an idea, as both of your user-types share the same basic functionalities.
As you said, make one make one unified User or Account model. Include the database fields customer (as boolean) and patron (also as boolean).
In the signup process, the user can then select if they're a patron or customer, as they would regardless.
Then inside your view, you can then call, if you use Devise for instance (which I personally think is great)
<% if current_user.patron == true %>
<!-- all relevant UI functionality for patrons -->
<% end %>
or if the User is a customer
<% if current_user.customer == true %>
<!-- all relevant UI functionality for customers -->
<% end %>
Or if you want to loop through a list of patrons or customers:
<% #user.where(:customer == true).each do |users| %>
<% #user.first_name %> <% #user.last_name %>
<% end %>
These are just basic examples, but it would do just fine for what you're trying to achieve.
P.S You could also create one migration called "account_type" as a
string and then with the help of radio_buttons in the signup process
store the account_type as a string value.
<%= f.radio_button :account_type, "customer" %>
<%= f.radio_button :account_type, "patron" %>
I actually think that would be better. Then you would split the views up
like this:
<% if current_user.account_type => "customer" %>
Show the list that only customer should have or see.
<% end %>
<% if current_user.account_type => "patron" %>
Show the list that only patron should have or see.
<% end %>
<% #user.where(:account_type => "customer").each do |users| %>
<% #user.first_name %> <% #user.last_name %>
<% end %>
Regarding my question: Will a patron ever be a client? Will a client ever be a patron?
If the answer is "yes", you should consider creating a separate entity/model for the resource they're relating to.
With model DonationCampaign (attributes client_id, among others):
has_one :client, :class_name => 'User'
has_many :patrons, :class_name => 'DonationCampaignPatron'
DonationCampaignPatron (attributes patron_id, among others):
belongs_to :patron, :class_name => 'User'
This allows you to keep the shared functionality of User and then extend functionality to specific campaigns, without having to make other models messy, and keeps things DRY.
If a DonationCampaign could then have multiple users (as administrators, per se), to extend, a DonationCampaignRole model would be required, donation_campaign_id, user_id, role
Say you're using CanCanCan,
if can? :manage, #campaign
if can? :contribute, #campaign
:contribute would have to be added to ability.rb and could simply be #campaign.client != User
Also, STI example (the good kind, at least):
class Animal < ActiveRecord::Base
def says
raise "Implement on subclass"
end
end
class Cat < Animal
# always has a type of 'Cat'
def says
"meow"
end
end
class Dog < Animal
# always has a type of 'Dog'
def says
"woof"
end
end
And there's only one table: animals
Edit: Based on your response: From what I can learn by briefly using Patreon they have a single User model (for authentication), and then they likely have a creator_page_id in the User column. They then have a separate model CreatorPage which has all the "Client" (in your terms) info associated with it.
Personally, I would stick with a single User model for authentication and then implement the aforementioned DonationCampaign/DonationCampaignPatreon business logic. It's the most extensible with the least amount of effort (both long and shot-term).
If, for whatever reason, a Client is restricted to contributing to other Clients once they are a Client, I would forego using STI on the User model.

'Tell, Don't Ask' whilst maintaining Separation of Concerns

In my Rails app, I have the following association:
Video belongs to Genre (Video does not HAVE to have a genre)
Genre has many Videos (Genre can have no videos)
In the Video model, I have the following method.
# models/video.rb
def genre_name
genre.present? ? genre.name : ''
end
This is to avoid something like this in the view (which just seems messy):
# views/videos/show.html.erb
<% if #video.genre.present? %>
<%= #video.genre.name %>
<% else %>
No Genre Present
<% end %>
Instead, I can just do this (which looks much tidier)
# views/videos/show.html.erb
<%= #video.genre_name %>
However, it doesn't feel right asking for information about the genre in the Video model. What's the best way to organise this code? Should I be using helpers instead?
If you find yourself doing this kind of thing a lot, it may be worth looking into a decorator pattern, which can house view logic like this. (I quite enjoy using Draper for this purpose, but it's not very difficult to roll your own naive implementation.)
Then, your decorator logic can look like this:
class VideoDecorator
def genre_name
object.genre.try(:name).presence || "Fallback"
end
end
You can wrap up the model in a decorator as you render in the controller:
#video = Video.find(params[:id])
respond_with #video.decorate
And your view 'logic' (or lack thereof) can look like this application-wide:
<%= #video.genre_name %>
Thoughtbot has an excellent explanation of the decorator pattern here
You could write in your view
<%= #video.genre.try(:name) || 'No Genre Present' %>
If you don't need the fallback text, just
<%= #video.genre.try(:name) %>
Read more about Object#try here.
If you want the fallback also when name is an empty string (not just nil) you can use Object#presence
<%= #video.genre.try(:name).presence || 'No Genre Present' %>

Rails - showing comments in a different area

Im trying to create a system which allows athletes to respond to their coaches traing plan, to do this i have allowed the coach to create contentsm however i am using a blogging based system to create it... at the moment the page displays like so
CONTENT TITLE
Content info 1...
Content info 2...
Content info 3...
COMMENTS...
comment 1
comment 2
comment 3
.etc
However i want to set it so that there can only be 7 Comments Max per post as well as set out like this per post...
CONTENT TITLE
Content info 1...
comment 1
Content info 2...
comment 2
Content info 3...
comment 3
.etc
I realise this is probably not the best way to do want i want, but it works (just dosnt appear in the place i want it to)
I did do experiments with creating more models, but kept getting errors whenever i tryed to run more than 1 comment system per post. I was wondering if i could have some help in sorting this out, or any methods i could do to make this easier, or even better if the models would work and if i was just doing something wrong?? tell me if this isn't enough information to go off, and ill try provide some more! Thankyou
.
.
EDIT:
The models i have used are
Program - As in the training plan set for the week
Coaches - The coach that is inputing the data to the rider
Riders - To comment on the coaches data with their own data.
I am unsure what files are need exactly so i have included the link to the github page i am pushing to ( https://github.com/effectonedesign/coacheasy1 ), if there is any other info needed, please let me know!
I like what "mind" has said however, i have done everything have said, in my def show (program controller) it is saying there is an error and i keep getting this message undefined method `coaches' for nil:NilClass everything is identical to his but im getting issues, i really do appreciate the help! Thanks
I would probably create 3 models for the above, TrainingPlan, Section (or content, text_block etc.) and Comment.
Then do the following
TrainingPlan has_many :sections
Section belongs_to :training_plan
Section has_one :comment (if you allow only 1 comment per section, otherwise use has_many)
Comment belongs_to :section
Now, to achieve the formatting you wanted do the following in your views:
<% #training_plan.sections.each do |section| %>
<%= section.text %>
<%= section.comment.text %>
<% end %>
If you allow multiple comments:
<% #training_plan.sections.each do |section| %>
<%= section.text %>
<% section.comments.each do |comment| %>
<%= comment.text %>
<% end %>
<% end %>
Form for comments
I haven't tested the following, so you might need to tweak some parts.The training plan controller:
def show
# using includes will query the database 3 times only (once for each table) rather than
# querying it 1 + N + N (in this case 7 sections, 7 comments possibly, so 15 times)
#training_plan = TrainingPlan.includes(:sections, sections: :comment).find(params[:id])
#sections = #training_plan.sections
#sections.each do |section|
# only build a new comment if there is no comment for that section already
section.build_comment unless section.comment
end
end
In your view views/training_plans/show.html.erb
<%= #training_plan.title %> # or whatever
<% #sections.each do |section|
<%= #section.content %>
<% if section.comment %>
<%= section.comment.content %>
<% else %>
<%= render 'comments/form', comment: section.comment %> # or wherever you have the form
<% end %>
<% end %>
views/comments/_form.html.erb
# This might break if you have a separate comment action somewhere which passes an
# instance variable #comment to the form
<%= form_for comment do |f| %>
# normal form stuff
<% end %>
If that all works then on your training plan show page you should see each section, and if it has a comment then that comment will be rendered, otherwise a form will be shown.
Depending on your routes you might need to run rake routes and see where your comment create action is, and then pass that to the form <%= form for comment, url: some_url_helper_here do |comment| %>
If it was me I would create the add comment part through JavaScript, sort of like in this railscast, but since you're new to RoR I've tried to keep it simple.

How to display a tag cloud from Acts as Taggable On on an index page in Ruby on Rails 3.2.7?

I'm very new to Ruby on Rails, so there's probably a simple solution I'm missing.
The tldr version - how do I display an Acts As Taggable On tag cloud of distinct (i.e. no repeating) tags assigned to all instances of a particular model on that model's index page?
The longer version - I have a model called Video in which I have successfully managed to implement a tagging feature using Acts as Taggable On and this fantastic tutorial.
What I'd like to do now is, on the Video's index page (index.html.erb), to display a summary of all the individual tags that a user has assigned to individual videos. For example, lets say I have three videos, each tagged as follows:
Video 1: great, banana, book
Video 2: small, great, apple
Video 3: rubbish, small, banana
I'd like the index page to display the following list of tags:
great, banana, book, small, apple, rubbish.
The code for my model (elided) is as follows:
class Video < ActiveRecord::Base
attr_accessible :tag_list # Lots of other fields in here as well, but not relevant
acts_as_taggable_on :tags
end
The code in my Video helper is as follows:
module VideosHelper
include ActsAsTaggableOn::TagsHelper
end
Finally, as per the gem's documentation, I've added the following code to my controller:
class VideosController < ApplicationController
def tag_cloud
#tags = Video.tag_counts_on(:tags)
end
end
So, what code should I be adding to the index page of my view? I tried the following, again as per the documentation:
<% tag_cloud(#tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
<%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
<% end %>
But this returns the following error when I go to the Video index page:
undefined method `empty?' for nil:NilClass
As I say, I'm obviously missing something simple, but I'm completely new to Rails (and Ruby) so I'm still finding my feet.
OK, after hacking about a bit, I think I've found a solution, in case anyone else wondering how to do this happens to stumble across this question.
However, please be aware that I am very much a beginner at RoR, so this is probably not the best solution - if I'm doing anything wrong, or if you have a better solution, feel free to let me know!
Add this code in your view to display the list of tags for a particular model in order:
#tags = ActsAsTaggableOn::Tag.all(:order=>'name')
<% if #tags.count > 0 %>
<ul>
<% #tags.each do |tag| %>
<li><%= link_to tag.name, tagged_url(:tag => tag.name) %></li>
<% end %>
</ul>
<% else %>
<p>There are no tags on the system.</p>
<% end %>
This results in a very basic display and, due to my inexperience I advise using this approach with caution - I'm sure it's not the best, or even the "safest", method, so beware!

Rails Search Form

I'm creating an application that tracks users and achievements (think, xbox live, etc.) These tables are linked via a join table. I would like to have a search form on my index that lets users type in a users name and a new page is loaded with a list of all achievements that user has earned. I'm not entirely sure how to set up this search form, on the index, to actually search the user table and return the results on a new page. Any help would be greatly appreciated. If you require more information then I'll be happy to provide it.
Here's a bit of skeleton code to get you started based off what I think you need from what you have said. I hope this is useful.
For the search bit you could do something like this in your index view:
<%= form_for User.new, :url => "search" do |f| %>
<%= f.label :name %>
<%- f.text_field :name %>
<%- end %>
In your controller:
def search
q = params[:user][:name]
#users = User.find(:all, :conditions => ["name LIKE %?%",q])
end
and in your search view:
<%-#users.each do |user| %>
Name: <%=user.name %>
<%- user.achievements.each do |achievement| %>
<%= achievement.name %>
<%- end %>
<%- end %>
You would, of course, need to ensure the users and achievement models are correctly linked:
class User << ActiveRecord::Base
has_many :achievements
end
There are plenty of tutorials and things about this e.g.:
http://blog.devinterface.com/2010/05/how-to-model-a-custom-search-form-in-rails/
Look the thing is every basic explanation in Rails3 starting with the Initial Tutorial provided by them explains you how to setup a new Controller/Model. The example was only one of thousands explaining the same problem.
It is a very broad range of different things you can do to achieve this. Basically you have to put some code in the controller:
which handles the search (including the activerecord stuff or whichever technique you use to access your model)
which sets some variables necessary for the search form
Setup two routes etc... Its to broad and completely covered even by the basic official rails3 tutorial.
Here is an application based on searchlogic is very useful and you can search by whatever you want
https://github.com/railscasts/176-searchlogic
You may want to check out the Ransack gem. https://github.com/activerecord-hackery/ransack

Resources