I'm curious to get some input on a chunk of code I've been working on recently.
I have a model, photos, which sometimes (but not always) belong_to a collection. I have a page for users to manage a collection, they can add any unassigned photos to the collection or remove photos from the collection.
This is an "edit multiple" situation, so I created two new controller actions: select, which handles the GET request and the view, and assign which handles the PUT request from the checkboxes in the select view.
Because the user can be either adding photos to a collection or removing photos from a collection, my assign action has a condition in it, and it looks like this:
def assign
#photos = Photo.find(params[:photo_ids])
case params[:assignment]
when 'add'
#photos.each do |p|
p.collection = #collection
p.save!
end
notice = "Photos added to collection."
when 'remove'
#photos.each do |p|
p.collection = nil
p.save!
end
notice = "Photos removed from collection."
end
redirect_to select_collection_photos_path(#collection), :notice => notice
end
This works exactly as expected. However, I feel uncomfortable with it, it doesn't seem to fit the "Rails Way."
Other Rails developers, when you have this kind of situation, would you handle it as I have? Would you split this across two controller actions (ie add_to_collection and remove_from_collection), or would you move it to the model? If you were to move it to the model, what would that look like?
I'd appreciate any suggestions and feedback. Thanks!
There's probably a few different ways you could refactor this, but the most obvious one seems to be moving all the photo logic to the Photo model. Even though this is your photos controller, it shouldn't know that much about the Photo model.
I would probably do something along these lines in your controller:
def assign
Photo.update_collection(params, #collection)
redirect_to select_collection_photos_path(#collection), :notice => "Photo collection updated"
end
Then in your Photo model:
class Photo < ActiveRecord::Base
def self.update_collection(params, collection)
photos = Photo.find(params[:photo_ids])
case params[:assignment]
when 'add'
photos.each {|p| p.add_collection(collection) }
when 'remove'
photos.each {|p| p.remove_collection }
end
end
def add_collection(collection)
self.collection = collection
save!
end
def remove_collection
self.collection = nil
save!
end
end
Breaking the functionality up into smaller model methods makes it easier for unit testing, which you should be doing if you're not :)
This is actually a prime candidate for accepts_nested_attributes_for.
Instead of thinking about new actions in the controller, stick to the standard REST conventions whenever possible. Excepting fancy UI display stuff (like your select action), very rarely do I find that I need to deviate from the standard CRUD actions present in a generated scaffold_controller.
If you set accepts_nested_attributes_for :collection in your Photo model, you should be able to build up a special form that assigns collections to photos. I won't go into the full details here, but will instead point you to http://railscasts.com/episodes/196-nested-model-form-part-1 and http://railscasts.com/episodes/197-nested-model-form-part-2 . It'll be more work in the view, but you'll come out far ahead in more simple, easily testable controllers and models.
Related
I have two models, User and Account.
# account.rb
belongs_to :user
# user.rb
has_one :account
Account has an attribute name. And in my views, I was calling current_user.account.name multiple times, and I heard that's not the great of a way to do it. So I was incredibly swift, and I created the following method in my user.rb
def account_name
self.account.name
end
So now in my view, I can simply call current_user.account_name, and if the association changes, I only update it in one place. BUT my question is, do I test this method? If I do, how do I test it without any mystery guests?
I agree there is nothing wrong with current_user.account.name - while Sandi Metz would tell us "User knows too much about Account" this is kind of the thing you can't really avoid w/ Active Record.
If you found you were doing a lot of these methods all over the User model you could use the rails delegate method:
delegate :name, :to => :account, :prefix => true
using the :prefix => true option will prefix the method in the User model so it is account_name. In this case I would assume you could write a very simple unit test on the method that it returns something just incase the attribute in account would ever change your test would fail so you would know you need to update the delegate method.
There's nothing wrong with current_user.account.name
There's no difference between calling it as current_user.account.name, or making current_user.account_name call it for you
You're probably not calling current_user in the model, like you say
You should have a spec for it if you use it
Personally I see no good reason for any of this. Just use current_user.account.name.
If you are worrying about efficiency, have current_user return a user that joins account.
This is going to be a bit off-topic. So, apologies in advance if it's not interesting or helpful.
TL;DR: Don't put knowledge of your models in your views. Keep your controllers skinny. Here's how I've been doing it.
In my current project, I've been working to make sure my views have absolutely no knowledge of anything about the rest of the system (to reduce coupling). This way, if you decide to change how you implement something (say, current_user.account.name versus current_user.account_name), then you don't have to go into your views and make changes.
Every controller action provides a #results hash that contains everything the view needs to render correctly. The structure of the #results hash is essentially a contract between the view and the controller.
So, in my controller, #results might look something like {current_user: {account: {name: 'foo'}}}. And in my view, I'd do something like #results[:current_user][:account][:name]. I like using a HashWithIndifferentAccess so I could also do #results['current_user']['account']['name'] and not have things blow up or misbehave.
Also, I've been moving as much logic as I can out of controllers into service objects (I call them 'managers'). I find my managers (which are POROs) a lot easier to test than controllers. So, I might have:
# app/controllers/some_controller.rb
class SomeController
def create
#results = SomeManager.create(params)
if #results[:success]
# happy routing
else
# sad routing
end
end
end
Now, my controllers are super skinny and contain no logic other than routing. They don't know anything about my models. (In fact, almost all of my controller actions look exactly the same with essentially the same six lines of code.) Again, I like this because it creates separation.
Naturally, I need the manager:
#app/managers/some_manager.rb
class SomeManager
class << self
def create(params)
# do stuff that ends up creating the #results hash
# if things went well, the return will include success: true
# if things did not go well, the return will not include a :success key
end
end
end
So, in truth, the structure of #results is a contract between the view and the manager, not between the view and the controller.
I'm new to Rails (I've worked in MVC but not that much) and I'm trying to do things the "right" way but I'm a little confused here.
I have a site navigation with filters Items by different criteria, meaning:
Items.popular
Items.recommended
User.items
Brand.items # by the parent brand
Category.items # by a category
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
Either I have an action in ItemsController for every filter (big controller) or I put it in ItemsController BrandsController, CategoriesController (repeated logic), but neither provides a "clean" controller.
But I don't know witch one is better or if I should do something else.
Thanks in advance!
You're asking two separate questions. Items.popular and Items.recommended are best achieved in your Item model as a named scope This abstracts what Xavier recommended into the model. Then in your ItemsController, you'd have something like
def popular
#items = Item.popular
end
def recommended
#items = Item.recommended
end
This isn't functionally different than what Xavier recommended, but to me, it is more understandable. (I always try to write my code for the version of me that will come to it in six months to not wonder what the guy clacking on the keyboard was thinking.)
The second thing you're asking is about nested resources. Assuming your code reads something like:
class User
has_many :items
end
then you can route through a user to that user's items by including
resources :users do
resources :items
end
in your routes.rb file. Repeat for the other nested resources.
The last thing you said is
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
If what I've said above doesn't solve this for you (I think it would unless there's a piece you've left out.) this sounds like a case for subclassing. Put the common code in the superclass, do the specific stuff in the subclass and call super.
There's a pretty convenient way to handle this, actually - you just have to be careful and sanitize things, as it involves getting input from the browser pretty close to your database. Basically, in ItemsController, you have a function that looks a lot like this:
def search
#items = Item.where(params[:item_criteria])
end
Scary, no? But effective! For security, I recommend something like:
def search
searchable_attrs = [...] #Possibly load this straight from the model
conditions = params[:item_criteria].keep_if do |k, v|
searchable_attrs.contains? k
end
conditions[:must_be_false] = false
#items = Item.where(conditions)
end
Those first four lines used to be doable with ActiveSupport's Hash#slice method, but that's been deprecated. I assume there's a new version somewhere, since it's so useful, but I'm not sure what it is.
Hope that helps!
I think both answers(#Xaviers and #jxpx777's) is good but should be used in different situations. If your view is exactly the same for popular and recommended items then i think you should use the same action for them both. Especially if this is only a way to filter your index page, and you want a way to filter for both recommended and popular items at the same time. Or maybe popular items belonging to a specific users? However if the views are different then you should use different actions too.
The same applies to the nested resource (user's, brand's and category's items). So a complete index action could look something like this:
# Items controller
before_filter :parent_resource
def index
if #parent
#items = #parent.items
else
#items = Item.scoped
end
if params[:item_criteria]
#items = #items.where(params[:item_criteria])
end
end
private
def parent_resource
#parent = if params[:user_id]
User.find(params[:user_id])
elsif params[:brand_id]
Brand.find(params[:brand_id])
elsif params[:category_id]
Category.find(params[:category_id])
end
end
Scenario: I have a has_many association (Post has many Authors), and I have a nested Post form to accept attributes for Authors.
What I found is that when I call post.update_attributes(params[:post]) where params[:post] is a hash with post and all author attributes to add, there doesn't seem to be a way to ask Rails to only create Authors if certain criteria is met, e.g. the username for the Author already exists. What Rails would do is just failing and rollback update_attributes routine if username has uniqueness validation in the model. If not, then Rails would add a new record Author if one that does not have an id is in the hash.
Now my code for the update action in the Post controller becomes this:
def update
#post = Post.find(params[:id])
# custom code to work around by inspecting the author attributes
# and pre-inserting the association of existing authors into the testrun's author
# collection
params[:post][:authors_attributes].values.each do |author_attribute|
if author_attribute[:id].nil? and author_attribute[:username].present?
existing_author = Author.find_by_username(author_attribute[:username])
if existing_author.present?
author_attribute[:id] = existing_author.id
#testrun.authors << existing_author
end
end
end
if #post.update_attributes(params[:post])
flash[:success] = 'great!'
else
flash[:error] = 'Urgg!'
end
redirect_to ...
end
Are there better ways to handle this that I missed?
EDIT: Thanks for #Robd'Apice who lead me to look into overriding the default authors_attributes= function that accepts_nested_attributes_for inserts into the model on my behalf, I was able to come up with something that is better:
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
if author_attributes[:id].nil? and author_attributes[:username].present?
author = Radar.find_by_username(radar_attributes[:username])
if author.present?
author_attributes[:id] = author.id
self.authors << author
end
end
end
assign_nested_attributes_for_collection_association(:authors, authors_attributes, mass_assignment_options)
end
But I'm not completely satisfied with it, for one, I'm still mucking the attribute hashes from the caller directly which requires understanding of how the logic works for these hashes (:id set or not set, for instance), and two, I'm calling a function that is not trivial to fit here. It would be nice if there are ways to tell 'accepts_nested_attributes_for' to only create new record when certain condition is not met. The one-to-one association has a :update_only flag that does something similar but this is lacking for one-to-many relationship.
Are there better solutions out there?
This kind of logic probably belongs in your model, not your controller. I'd consider re-writing the author_attributes= method that is created by default for your association.
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
author_to_update = Author.find_by_id(author_attributes[:id]) || Author.find_by_username(author_attributes[:username]) || self.authors.build
author_to_update.update_attributes(author_attributes)
end
end
I haven't tested that code, but I think that should work.
EDIT: To retain the other functionality of accepts_nested_Attributes_for, you could use super:
def authors_attributes=(authors_attributes)
authors_attributes.each do |key, author_attributes|
authors_attributes[key][:id] = Author.find_by_username(author_attributes[:username]).id if author_attributes[:username] && !author_attributes[:username].present?
end
super(authors_attributes)
end
If that implementation with super doesn't work, you probably have two options: continue with the 'processing' of the attributes hash in the controller (but turn it into a private method of your controller to clean it up a bit), or continue with my first solution by adding in the functionality you've lost from :destroy => true and reject_if with your own code (which wouldn't be too hard to do). I'd probably go with the first option.
I'd suggest using a form object instead of trying to get accepts_nested_attributes to work. I find that form object are often much cleaner and much more flexible. Check out this railscast
I have Bands has_many Rewards, Rewards belong to Bands.
On the band show page I am looping through a bands rewards:
<% #rewards.each do |r| %>
<b>Reward:</b> <%= r.name %><br>
<b>Cost:</b> <%= r.cost %><br>
<%= button_to "Get This", buy_item_band_path(#band, #reward)%>
<% end %>
buy_item_band_path routes to the Band#buy_item action. I am passing the #band and "r" to it so that I can access both.
I am handling users with Devise so I have access to current_user as well. Basically I am trying to have a user buy a reward. Users have a points attribute, so I am trying to do something like the following in the Band Model:
def pay_for_it
unless current_user.points < self.reward.cost
current_user.points - self.reward.cost
current_user.save
# SEND TWO EMAILS ONE TO THE PURCHASER AND ONE TO THE BAND ADMIN
end
end
That model method is just what I think might work, but I don't know how to reference it in the controller, don't know if I should, don't know if I should somehow do this in the controller.
any help is greatly appreciated.
thanks
First off, if you're trying to pass the reward, that code should probably read as:
<%= button_to "Get This", buy_item_band_path(#band, r)%>
I'm not sure what #reward would have been pulling from, but r will pull from the current iteration of #rewards.
As to the question, all of your business logic should happen inside of your models. Controllers are just there to pass data into your models, analyze the result of model manipulations, and tell Rails which view needs to be displayed.
It sounds like your User is the object that is doing the paying, so your payment method probably belongs in the User model and should take a Reward as its only parameter. There's no clear reason to involve the Band model at all.
The portion of your controller that handles this request will probably look something like this:
def buy_item
reward = Reward.find(params[:reward_id])
if current_user.pay_for_it(reward)
# Send emails and then...
# Render something
else
# Render something that indicates an error happened
# or that the user didn't have enough points.
end
end
And your User model will have something like:
def pay_for_it(reward)
return false unless self.points >= reward.cost
current_user.points -= reward.cost
current_user.save
end
So User#pay_for_it will return true if the points were successfully deducted, at which point your controller can send out the emails and then render whatever view should be displayed after a purchase has been successful.
Hopefully that'll give you a better idea of how to organize logic and control flow in Rails. Models manipulate data (business logic layer), Controllers pass information to and from the business logic layer, and Views display information.
I used the nested model gem to create a Picture that can take tags. Now I have added an attribute to my model Picture so it has an attribute taglist. When I create a new tag, I want this to happen
class TagsController < ApplicationController
def create
#tag = Tag.new(params[:id])
if #tag.save
taglist = picture.taglist
taglist+=#tag.tagcontent
#tag.picture.update_attributes(:taglist => taglist)
end
end
end
and in my routes
resources :pictures do
resources :tags
end
When i make a new tag, nothing happens in the taglist attribute, like nothing happened, why?
It's hard to help due to lack of information, but I see two possible issues:
Tag.new(params[:id]) doesn't make sense. Assuming Tag inherits from ActiveRecord::Base, you need to pass it a hash of attributes (e.g. Tag.new(:name => 'mytag')) You are likely not getting into the if #tag.save block at all due to validation errors. Also, you don't need to provide an id to an object you want to create. The database chooses the id.
Inside the block, picture is undefined on the first line.
Why not try debugging with something like:
if #tag.save
taglist = picture.taglist
taglist+=#tag.tagcontent
#tag.picture.update_attributes(:taglist => taglist)
else
p "ERRORS:"
p #tag.errors.full_messages
end
See what errors that prints out into your console.
I definitely think that picture is probably undefined in the create method of the controller. Can you show us the view, the form you're using to create a new tag? Is there a form field through which you're choosing which photo gets the tag?
Please show us the association and your view for creating the new tag.
Actually, what I'd really recommend instead of cooking up your own is to use:
Agile Web Development's acts_as_taggable_on_steroids
It's an excellent plugin to make tagging easy; it has quite a few nifty features built in, including the searches, tag clouds, etc. We use it on our projects.