Advice on setting up Rails model with multiple relations (belongs_to) - ruby-on-rails

About to make a design decision and looking for a little validation or advice on the "Rails way" before proceeding.
Summary:
Users create Posts
Posts can include (one or more) Photos
Posts can be (optionally) related to an Event
In turn Photos from a Post are also related to said Event
One requirement, among others, would be to easily display all Photos from a given Event. Another would be showing all Photos submitted by a given User.
I originally assumed:
class Photo < ActiveRecord::Base
belongs_to :post
belongs_to :user
belongs_to :event
...
end
But I'm having trouble building all the relationships in the Post controller:
class PostsController < ApplicationController
before_filter :login_required, :except => [:index, :show]
def create
#user = User.find(session[:user_id])
#post = #user.posts.create(params[:post])
# how/where to assign Event?
...
end
...
end
I can loop through and build each :photo param in the Post model...but not sure how/where to assign the event_id? Which makes me wonder if maybe there's a better approach?
Perhaps I should be exploring has_many :through relationships where:
User has_many :photos, :through => :posts
Event has_many :photos, :through => :posts
In a nutshell, should I be storing the user_id & event_id in every Photo to make it easier to grab them as needed? If so, how best to assign the associations? Or, will this become hard to maintain and thus generally frowned upon and I should use a has_many :through approach?

Related

Tags per post per user, the user isn't listed.

My main models are that I have users and I have recipes.
I'm trying to implement a tagging structure such that each user can tag a recipe with individual tags. So when viewing a recipe, they would only see tags that they themselves have added.
I created two models hashtags, and hashtagging that is the join table. It is set up as so:
models/hashtags.rb
class Hashtag < ActiveRecord::Base
has_many :hashtaggings
has_many :recipes, through: :hashtaggings
has_many :users, through: :hashtaggings
end
models/hashtagging.rb
class Hashtagging < ActiveRecord::Base
belongs_to :user
belongs_to :hashtag
belongs_to :recipe
end
models/recipe.rb
class Recipe < ActiveRecord::Base
...
has_and_belongs_to_many :users
has_many :hashtaggings
has_many :hashtags, through: :hashtaggings
....
def all_hashtags=(name)
self.hashtags = name.split(",").map do |name|
Hashtag.where(name: name.strip).first_or_create!
end
end
def all_hashtags
self.hashtags.map(&:name).join(",")
end
end
class User < ActiveRecord::Base
...
has_many :hashtaggings
has_many :hashtags, through: :hashtaggings
...
end
This works great for creating the hash tags however I'm at a loss for how to incorporate the user aspect of it. How when assigning the tags can I also assign the current user to those tags and then just return those?
There are two steps, creation and display...
Creation
This one is going to be tricky, because you can't simply do something like...
#recipe.hashtags.create(name: "Tasty as heck!")
...because neither the recipe, nor the hashtag, knows anything about the user. It's a two-step process.
class RecipeHashtagsController
def create
current_hashtag = Hashtag.find_or_create_by(name: "Scrumptious!")
current_recipe = Recipe.find(params[:recipe_id])
hashtagging = Hashtagging.find_or_create_by(hashtag: current_hashtag, user: current_user, recipe: current_recipe)
# redirect_to somewhere_else...
end
end
A few things I did there:
I'm using find_or_create_by since I'm assuming you don't want either duplicate hashtags or duplicate hashtaggings. You could also just create.
I'm assuming you have some kind of current_user method in ApplicationController or through a gem like Devise.
I'm assuming you have a controller like RecipeHashtags, and a nested resource that matches and will provide an id from the route. I recommend nesting here since you aren't simply creating a hashtag, but you are creating a hashtag within the specific context of a recipe.
Displaying
This gets tricky, because you want to display recipe.hashtags but with a condition on the join table hashtaggings. This is not super straightforward.
What I'm thinking is you might want to be able to do something like...
#recipe.hashtags_for_user(current_user)
...which could be in the form of a method on Recipe.
class Recipe
def hashtags_for_user(user)
Hashtags.joins(:hashtaggings).where(hashtaggings: { user_id: user.id, recipe_id: self.id })
end
end
You can read more about the hash inside the .where call in the Active Record Querying Rails Guide (check out section 12.3).
Edit: The Controller
I recommend creating RecipeHashtags as a nested route pointing to separate controller, since the creation of a hashtag is dependent on which recipe it's being created for.
routes.rb
resources :recipes do
resources :hashtags, only: [:create]
end
...which will show something like the following when you do rake routes in the terminal...
POST /recipes/:recipe_id/hashtags(.:format) recipe_hashtags#create
Note: I'm assuming you have a resource for resource for recipes. If you don't, this may duplicate some routes and have other, unintended results.
The default behavior for Rails is to assume you've got a controller like recipe_hashtags_controller based on how you defined your resources. You can always override this if you like.

Processing Orders in Rails 4 - Model code and associations

I'm having difficulty approaching a common issue of processing orders in Rails 4. I have a model object "Offers" which are then accepted by Users. This action "accept" needs to create a new Order object which saves the same attributes as the Offer object. From what I've read my code should look as follows:
class User
has_many :offers
has_many :orders, through :offers
# ...
end
class Offer
belongs_to :user, dependent: :destroy
has_one: order
# ...
end
class Order
belongs_to :offer
def add_fields_from_offer(order)
order.offer.each do |offer|
offer_id = nil
order << offer
end
end
end
I would appreciate any advice on this code or the structure of this approach. The Offer object is really the transactional product so it should be destroyed once accepted. But I would like the order saved as an object for the User's account history.
This essentially means repeating the same fields but in a different model - is this a good approach or is there a better way?
Many thanks
You can ditch the Offer model entirely and add a boolean accepted or is_offer field to your Order.
After some further research I found it was easier to only use a single model (here "offers") and use the state_machine gem for assistance. This is ideal if your product goes through several stages e.g. accepted, posted, etc etc.
The documentation explains how to implement this in the model e.g.
state_machine :initial => :new do
event :accept do
transition :from => :new, :to => :accepted, :unless => :expired?
end

How do I prevent deletion of parent if it has child records?

I have looked through the Ruby on Rails guides and I can't seem to figure out how to prevent someone from deleting a Parent record if it has Children. For example. If my database has CUSTOMERS and each customer can have multiple ORDERS, I want to prevent someone from deleting a customer if it has any orders in the database. They should only be able to delete a customer if it has no orders.
Is there a way when defining the association between models to enforce this behavior?
class Customer < ActiveRecord::Base
has_many :orders, :dependent => :restrict # raises ActiveRecord::DeleteRestrictionError
Edit: as of Rails 4.1, :restrict is not a valid option, and instead you should use either :restrict_with_error or :restrict_with_exception
Eg.:
class Customer < ActiveRecord::Base
has_many :orders, :dependent => :restrict_with_error
You could do this in a callback:
class Customer < ActiveRecord::Base
has_many :orders
before_destroy :check_for_orders
private
def check_for_orders
if orders.count > 0
errors.add_to_base("cannot delete customer while orders exist")
return false
end
end
end
EDIT
see this answer for a better way to do this.
Try using filters to hook in custom code during request processing.
One possibility would be to avoid providing your users a link to deletion in this scenario.
link_to_unless !#customer.orders.empty?
Another way would be to handle this in your controller:
if !#customer.orders.empty?
flash[:notice] = "Cannot delete a customer with orders"
render :action => :some_action
end
Or, as Joe suggests, before_filters could work well here and would probably be a much more DRY way of doing this, especially if you want this type of behavior for more models than just Customer.

Polymorphic has_many through Controllers: Antipattern?

I'm tempted to say yes.
A contrived example, using has_many :through and polymorphs:
class Person < ActiveRecord::Base
has_many :clubs, :through => :memberships
has_many :gyms, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :member, :polymorphic => true
end
class Club < ActiveRecord::Base
has_many :people, :through => :memberships
has_many :memberships, :as => :member
end
etc.
Leaving aside, for the moment, the question of whether a Gym is a Club, or any other design flaws.
To add a User to a Club, it's tempting to be RESTful and POST a person_id and a club_id to MembersController, like so:
form_for club_members_path(#club, :person_id => person.id) ...
In this scenario, when we decide to do:
form_for gym_members_path(#gym, :person_id => person.id) ...
We would need to make MembersController decide whether the parent resource is a Club or a Gym, and act accordingly. One non-DRY solution:
class MembersController < ApplicationController
before_filter :find_parent
...
private
def find_parent
#parent = Gym.find(params[:gym_id]) if params[:gym_id]
#parent = Club.find(params[:club_id]) if params[:club_id]
end
end
Shockingly awful if you do it more than once.
Also, it's predicated on the concept that joining a Club and joining a Gym are roughly the same. Or at least, Gym#add_member and Club#add_member will behave in a more or less parallel manner. But we have to assume that Gyms and Clubs might have different reasons for rejecting an application for membership. MembersController would need to handle flash messages and redirects for two or more error states.
There are solutions in the wild. James Golick's awesome ResourceController has a way of dealing with parent_type, parent_object, etc. Revolution On Rails has a nice solution for DRYing up multiple polymorphic controllers by adding some methods to ApplicationController. And of course, ActionController has #polymorhpic_url for simpler cases like Blog#posts and Article#posts, etc.
All this leaves me wondering, is it really worth putting all that pressure on MembersController at all? Polymorphism is handled pretty well in Rails, but my feeling is that using conditionals (if/unless/case) is a clear indication that you don't know what type you're dealing with. Metaprogramming helps, but only when the types have similar behavior. Both seem to point to the need for a design review.
I'd love to hear your thoughts on this. Is it better to be DRY in this scenario, or to know exactly what parent type you have? Am I being neurotic here?

RESTfully destroy polymorphic association in Rails?

How do I destroy the association itself and leave the objects being associated alone, while keeping this RESTful?
Specifically, I have these models:
class Event < ActiveRecord::Base
has_many :model_surveys, :as => :surveyable, :dependent => :destroy, :include => :survey
has_many :surveys, :through => :model_surveys
end
class ModelSurvey < ActiveRecord::Base
belongs_to :survey
belongs_to :surveyable, :polymorphic => true
end
class Survey < ActiveRecord::Base
has_many :model_surveys
end
That's saying that the Event is :surveyable (ModelSurvey belongs_to Event). My question is, without having to create a ModelSurveysController, how do I destroy the ModelSurvey, while leaving the Event and Survey alone?
Something with map.resources :events, :has_many => :model_surveys? I'm not quite sure what to do in this situation. What needs to happen with the routes, and what needs to happen in the controller? I'm hoping the url could look something like this:
/events/:title/model_surveys/:id
Thanks for your help,
Lance
In Rails 2.3 you have accepts_nested_attributes_for which would let you pass an array of ModelSurveys to the event in question. If you allow destroy through the nested attributes declaration, you'll be able to pass event[model_surveys][1][_destroy]=1 and the association will be removed. Check out the api docs.
Resources domain != model domain
The domain of the controller is not the same as that of the models. It's perfectly fine to update multiple models by changing the state of a resource.
In your case that means doing a PUT or POST to either the Event or the Survey which contains a list of ids for the other. The model for one will update the association.
PUT or POST
Some people (but not Roy Fielding) believe that you should use a PUT to update the resource and provide all of the state again, others feel that a POST with the partial state (ala PATCH) is sufficient.

Resources