Refactoring view logic in Rails - ruby-on-rails

Here's what I need to do. I have a Tournament model, which is connected to User via Signup (N:N).
The only thing that Signup adds is status of the signup. Tournament has a start time, and users can register only until there is 60 minutes before the tournament starts. After that, registered users can check in. So basically I have two options for the state
In short, models looks like this
class Signup < ActiveRecord::Base
REGISTERED = 0
CHECKED = 1
belongs_to :tournament
belongs_to :user
end
class Tournament < ActiveRecord::Base
has_many :signups
has_many :users, :through => :signups
end
class User < ActiveRecord::Base
has_many :signups
has_many :tournaments, :through => :signups
end
I skipped some code to keep this short. The problem is with the view, since I have a lot of conditions to keep in mind. Here's my actual code (using Slim as a templating engine)
- if logged_in?
- if current_user.registered_for?(#tournament)
- if #tournament.starts_at < 60.minutes.from_now
p Signups are closed, only registered users can now check in
- if current_user.registered_for?(#tournament)
= button_to "Checkin!", { :controller => :signups, :action => :update, :id => #tournament.id }, :method => :put
- else
= button_to "Cancel your registration for the tournament", { :controller => :signups, :action => :destroy, :id => #tournament.id }, :method => :delete
- elsif current_user.checked_in?(#tournament)
p You have already checked in.
- elsif #tournament.starts_at > 60.minutes.from_now
= button_to "Sign up for the tournament", :controller => :signups, :action => :create, :method => :post, :id => #tournament.id
- else
p
| The tournament starts in less than 60 minutes, you can't sign in
- else
p
| You need to
|
= link_to "log in", login_path
| to play
The problem is, I have no idea how to make this much cleaner. I mean yes I can add helpers for buttons, but that won't help me with the if if else else ugliness, because there are many different combinations. Here's a short list:
user isn't logged in
it's over 60 until the tournament starts and user hasn't yet registered for the tournament
it's over 60 until the tournament starts and user is already registered
it's under 60 minutes, but user isn't registered yet
it's under 60 minutes and user is registered but hasn't checked in
it's under 60 minutes and user already checked in
And this is just the tip of the iceberg, because admins should see more information than a regular user, but I don't want to complicate this question.
The main problem is, how should I handle cases like this? It just seems so terrible to do this in a view, but I don't see any other simpler way.

A cleaner way would be to create meaningful methods on your models. For example, in your Tournament model, add something like :
def can_register?( user )
!user.registered_for?(self) && self.starts_at > 60.minutes.from_now
end
And then in your view, you can check for can_register? before displaying something. Adding logic into the view like you did is not what is intended in a MVC application.

You should use an object to encapsulate the logic. Maybe something like this:
class UserSignup
def initialize(user, tournament)
#user, #tournament = user, tournament
end
def registered?
#user.registered_for?(#tournament)
end
def signups_closed?
#tournament.start_at < 1.hour.from_now
end
def checked_in?
#user.checked_in?(#tournament)
end
end
Which makes the view a lot simpler and doesn't require to much work. You'll see that a lot of duplication will be removed this way, and you can test your sign up logic independent of the view.
You could also make a presenter, which is a bit more involved, but cleans up your view even more. Have a look at gems like draper to help you with this.
class SignupPresenter
def initialize(user_signup)
#user_signup = user_signup
end
def register_button
view.button_to("sign up") if #user_signup.registered?
end
# etc ...
end
Also, I would consider using different templates or even controllers for different users. So users that haven't signed in at all cannot even access this page and admins have a different controller (even namespace) all together.
I wouldn't just go in and split it into partials, because that would just hide the logic. I also like a separate object more than putting it into a model, because this way the model doesn't get cluttered as much and all the logic stays together, nicely focussed.

Related

Creating a record from a different model view(no new action involved)

I have a has_many & belongs_to associations between company and worker. I want to be able to add a worker to a company via a link in my workers index page. I want that to save the worker record belonging to the logged in company and update the company/workers index page with the new worker added to their workers.
I have been unable to do this. Here is what is happening:
My routes have something like:
namespace :company do
resources :workers, :only => [:index, :create]
end
resources :workers
I have a before_action method that has the cookie session with the company access token in my controllers:
ApplicationController:
#company = Company.find_by_access_token("vZAni6K6")
cookies[:access_token] = #company.access_token
And Company::WorkersController
render :file => "public/401.html", :layout => nil, :status => :unauthorized and return if cookies[:access_token] != #company.access_token
In my workers/index.html.haml path I have:
= "There are #{#workers.count} workers!"
They are:
- #workers.each do |worker|
= worker.name
= link_to "Hire a worker", company_workers_path(:worker_id => worker), :method => :post
%%br
%p= #company.name
I want the to be able to click the "Hire a worker link" and then be redirected to the company/workers.html.haml path that then will list their workers updated with the recent addition of the worker just added via the link.
When I currently click the link(say for worker #2 in by database) instead of taking me to company/workers path it takes me to
company/workers?worker_id=2
And it doesn't save the worker to the association with the company.
My company/workers controller has the following:
def create
#worker = #company.worker.build(:worker_id => params[:worker_id])
#worker.save
redirect_to :action => 'index'
end
Remember I have a before_action on my controllers that saves the #company instance variable before calling other controller methods as well.
I have a model Worker that belongs_to a company & a model Company that has_many workers and I have added the reference key in my migration already.
What is the problem? Why the weird route and why are my records not saving, I am a bit of a newb so forgive the simple question.
At first look, I see you may have two errors:
your Company model class has_many workers, so to assign (or add) a worker to a company I believe it must be something like:
#worker = Worker.find(params[:worker_id])
#worker.company = #company
#worker.save!
# depending in your schema, it could be something like this as well:
# #company.workers << #worker
to redirect to company/workers, as you use nested routes, it must be like
redirect_to company_workers_path(#company)
Hope this helps!

How to set up routes to show additional information in the URL using namespaces?

I am running Ruby on Rails 3 and I would like to set up my routes to show additional information in the URL using namespaces.
In the routes.rb file I have:
namespace "users" do
resources :account
end
So, the URL to show an account page is:
http://<site_name>/users/accounts/1
I would like to rewrite/redirect that URL as/to
http://<site_name>/user/1/Test_Username
where "Test_username" is the username of the user. Also, I would like to redirect all URLs like
# "Not_real_Test_username" is a bad entered username of the user.
http://<site_name>/users/accounts/1/Not_real_Test_username
to the above.
At this time I solved part of my issuelike this:
scope :module => "users" do
match 'user/:id' => "accounts#show"
end
My apologies for not answering your question (#zetetic has done that well enough), but the best practice here is to stay within the RESTful-style Rails URL scheme except for rare exceptions. The way most people make pretty URLs in this way is to use a hyphen, e.g.:
/accounts/1-username
This does not require any routing changes. Simply implement:
class Account < ActiveRecord::Base
def to_param
"#{self.id}-#{self.username}"
end
end
And handle the extra string data in your finds by calling to_i.
class AccountController < ApplicationController
def show
#account = Account.find(params[:id].to_i)
end
end
When you do link_to 'Your Account', account_path(#account), Rails will automatically produce the pretty URL.
It's probably best to do this in the controller, since you need to retrieve the account to get the username:
#account = Account.find(params[:id])
if #account && #account.username
redirect_to("/user/#{#account.id}/#{#account.username}")
return
end
As to the second issue, you can capture the remaining parameter by defining it in the route:
get "/users/accounts/:id(/:other)" => "users/accounts#show"
This maps like so:
/users/accounts/1/something # => {:id => "1", :other => "something"}
/users/accounts/1 # => {:id => "1"}
And you can simply ignore the :other key in the controller.

Voting update on Ruby on Rails

Right now, I'm in the middle of building a social media app on Ruby on Rails, i have implemented a 5 point voting system. Where you can vote the news posted on the site from 1-5, what I'd like to know is, What is the best approach at handling the updates on the voting system.
In example. If a user already voted in an article I'd like to bring back score he gave in the article and soft-lock the voting (since i only allow 1 vote per user and i allow to change your vote at any time), but if he hasn't I'll bring up the article with the the voting on 0.
I know a way to accomplish this, i could do it in the view, and check if the current user has already voted on this article i would send them to the EDIT view otherwise to the SHOW view. (I think)
Anyways, what would be the "correct" approach to do this?
EDIT: I forgot to say that the voting combo box it's a partial that I'm rendering. Am i suppose to just update the partial somehow?
EDIT2:
class Article < ActiveRecord::Base
has_many :votes
belongs_to :user
named_scope :voted_by, lambda {|user| {:joins => :votes, :conditions => ["votes.user_id = ?", user]} }
end
class User < ActiveRecord::Base
has_many :articles
has_many :votes, :dependent => :destroy
def can_vote_on?(article)
Article.voted_by(current_user).include?(article) #Article.voted_by(#user).include?(article)
end
end
Create a method in the User model that responds true if the user can vote on an article:
class User < ActiveRecord::Base
...
def can_vote_on?(article)
articles_voted_on.include?(article) # left as an exercise for the reader...
end
end
In the view, render a form if the user can edit, otherwise render a normal view:
<% if #user.can_vote_on?(#article) %>
<%= render :partial => "vote_form" %>
<% else %>
<%= render :partial => "vote_display" %>
<% end %>
Or you could handle the whole thing in the controller, and render separate templates for the form version and the normal version. The best approach depends on the specifics of your situation.
EDIT2
As you discovered, current_user doesn't work in the model. This makes sense, because the can be called from migrations, libraries, etc., where there is no concept of a session.
There's no need to access the current user anyway, since your instance method is (by definition) being called on an instance. Just refer to self in the model, and call the method from the view on current_user, which is an instance of User:
(in the model)
def can_vote_on?(article)
Article.voted_by(self).include?(article)
end
(in the view)
<% if current_user.can_vote_on?(#article) %>
Or you could substitute #user for current_user if the controller assigns it.
One last thing, I think your named scope should use user.id, like so:
named_scope :voted_by, lambda {|user| {:joins => :votes, :conditions => ["votes.user_id = ?", user.id]} }

Rails: Redirect within XHR

I have a Parent model which has Children. If all the Children of a certain Parent are deleted, I'd like to automatically delete the Parent as well.
In a non-AJAX scenario, in the ChildrenController I would do:
#parent = #child.parent
#child.destroy
if #parent.children.empty?
redirect_to :action => :destroy,
:controller => :parents,
:id => #parent.id
end
But this is impossible when the request is XHR. The redirect causes a GET request.
The only way I can think of to do this with AJAX is add logic to the response RJS, causing it to create a link_to_remote element, "click" it, and then remove it. It seems ugly. Is there a better way?
Clarification
When I use the term redirect, I do not mean an HTTP redirect. What I mean is that instead of returning the RJS associated with destroying Child, I want to perform destroy on Parent and return the RJS associated with destroying Parent.
I would listen to what nathanvda says, but you can do it via ruby syntax (and you don't need erb scriptlets in rjs):
if #parent.children.empty?
page.redirect_to(url_for :action => :destroy,
:controller => :parents,
:id => #parent.id)
else
.. do your normall stuff here ..
end
A better approach to destroying the parent through a redirect is doing it in an after_hook. Not only you don't have to tell your user's browser to make another request, you also don't need to keep track of everywhere in the code where you delete children so you don't end up with hanging parents.
class Parent < ActiveRecord::Base
# also worth getting the dependent destroy, so you don't have hanging children
has_many :chilren, :dependent => :destroy
end
class Child < ActiveRecord::Base
after_destroy { parent.destroy if parent.children.empty? }
end
Then you can just handle however you prefer what to show the user when that happens, like redirecting the user to '/parents'.
I would guess you could set the window.location.href in your rjs, something like
<% if #parent.children.empty? %>
window.location.href='<%= url_for :action => :destroy,
:controller => :parents,
:id => #parent.id %>'
<% else %>
.. do your normall stuff here ..
<% end %>
assuming you render javascript. Not sure if it is completely correct, but hope you get the idea.
[EDIT: added controller code]
TO make it clearer, your controller would look as follows
#parent = #child.parent
#child.destroy
if #parent.children.empty?
render :redirect
end

User Monitoring in Rails

We have an app with an extensive admin section. We got a little trigger happy with features (as you do) and are looking for some quick and easy way to monitor "who uses what".
Ideally a simple gem that will allow us to track controller/actions on a per user basis to build up a picture of the features that are used and those that are not.
Anything out there that you'd recommend..
Thanks
Dom
I don't know that there's a popular gem or plugin for this; in the past, I've implemented this sort of auditing as a before_filter in ApplicationController:
from memory:
class ApplicationController < ActionController::Base
before_filter :audit_events
# ...
protected
def audit_events
local_params = params.clone
controller = local_params.delete(:controller)
action = local_params.delete(:action)
Audit.create(
:user => current_user,
:controller => controller,
:action => action,
:params => local_params
)
end
end
This assumes that you're using something like restful_authentication to get current user, of course.
EDIT: Depending on how your associations are set up, you'd do even better to replace the Audit.create bit with this:
current_user.audits.create({
:controller => controller,
:action => action,
:params => local_params
})
Scoping creations via ActiveRecord assoiations == best practice

Resources