Building an object through two associations in Rails - ruby-on-rails

Is it possible to build an object through two has_many associations? For example:
# posts_controller.rb
def create
#post = current_account.posts.build(params[:post])
#post.author = current_user # I want to compact this line into the previous one
end
I did some research and found this:
#article = current_account.posts.build(params[:post], user_id: current_user.id)
However, that did not work. In console, I kept getting user_id: nil whenever I built a new object.
Another potential solution I could not implement:
#post = current_account.post_with_user(current_user).build(params[:post])
But every implementation of post_with_user I wrote failed.
My associations are as follows:
class Discussion < ActiveRecord::Base
belongs_to :account
belongs_to :author, class_name: 'User', foreign_key: 'user_id', inverse_of: :discussions
end
class User < ActiveRecord::Base
belongs_to :account
has_many :discussions, inverse_of: :author
end
class Account < ActiveRecord::Base
has_many :users, inverse_of: :account
has_many :discussions
end

The thing your code shows you trying to do, you should be able to do. It should look something like this:
#article = current_account.posts.build(params[:post])
Because you're building off of the list of the current account's posts, you don't have to pass the current account's ID. (I'm not sure if your current_user is the same as your current_account, you may wish to clarify this).
To compact your post creation into one line, you can do one of two things.
Turn the relationship between a user/author and a post into a two-way relationship. Check out the documentation http://guides.rubyonrails.org/association_basics.html where an order belongs_to a customer, and a customer has_many orders. You can customize the name of the relationship so that a post has an "author" instead of a user, by calling it "author" but then using the class_name parameter which I assume would take the value :user.
Add a after-create hook to the Post class, and set the author value to the same as the current user. I can't fill in much more detail about this without knowing anything about your user subsystem.

The params variable is just a hash, so something along these lines should work to give you a one liner:
#post = current_account.posts.build params[:post].merge({ :user_id => current_user.id })

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.

group creator transfered to other user if group creator leaves group

First question: I have a group model that belongs_to :creator, :class_name => "User" and if the creator leaves I want someone else in the group to become the "creator". How would I do this?
As a second question, I would also like to give the leaving creator to choose a new creator when leaving, or let it be automatically assigned.
as of right now these are what my models look like:
class Group < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :members, :through => :memberships
has_many :memberships, :foreign_key => "new_group_id"
and my User model:
class User < ActiveRecord::Base
has_many :groups, foreign_key: :creator_id
has_many :memberships, foreign_key: :member_id
has_many :new_groups, through: :memberships
As a third question, I would like the group to be destroyed when the creator leaves. How can I set up this kind of relation?
These are 3 questions, and the first two are quite open, so I'd try to answer all of them in order making some assumptions down the road.
First question
This depends on what do you want the behavior for choosing a new creator to be. Do you want it to be automatically assigned from the current members? Do you want to give other members to have the change to auto-assign themselves as creator? In the latter you need to provide your members a full UI (routes, controllers, views) for that purpose, so I'll show you how would I code the first option.
First, I'd encapsulate the group leaving logic into its own method on the Group model, that we'll use in the controller for this purpose. On Group we define the logic for assigning as new creator. The logic here will be to pass the method a new_creator if we have it, or default to the first of the members if not given.
class Group < ActiveRecord::Base
def reassign(new_creator = nil)
new_creator ||= members.first
if new_creator
self.creator = new_creator
save
else
false
end
end
end
As an alternative approach, you can move this logic into an Observer (or any alternative) that will observe on Group model for the attribute creator_id to be nulled.
Second question
This one will involve a full UI that you'll need to figure out yourself according to your specifications. In short, I'd create a new action in your GroupsController for members to leave groups like this:
# config/routes.rb
resources :groups do
member do
get :leave
patch :reassign
end
end
# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
def leave
#group = Group.find(params[:id])
end
def reassign
#group = Group.find(params[:id])
if #group.reassign(params[:new_creator_id])
redirect_to my_fancy_path
else
render :leave
end
end
end
In your views you'll have your form_for #group with the new member candidates (possible with a select_tag :new_creator_id) and the rest of your UI as you prefer.
Third question
This is the easiest. You just define your association like this and you'll get all User groups destroyed after the user is deleted.
class User < ActiveRecord::Base
has_many :groups, foreign_key: :creator_id, dependent: :destroy
end

Building in model callback returning nil for value

First, thanks for taking the time to read. I'm new to Rails and have been stuck on this one for many hours.
In my Rails 3.2 app, I have three models: User, Organization, and Membership (the last is a join model between User and Organization).
When a user creates an organization, he/she should become a member upon create. So, in my Organization model, I've included a before_create callback that builds a Membership. The problem is that while the Membership builds when the new Organization is created, the user_id on the Membership object is set to "nil.," and therefore the current user is not a member.
Hardcoding in the user_id attribute in the callback actually does correctly build the membership, i.e. (:user_id => "1"), but in general asking the Organization model to be aware of current user state seems like bad MVC practice.
What's the proper way to set the current user ID on the new Membership? It seems like my associations should handle that, but I might be wrong.
Here are my models — I'm leaving out some validation lines for readability's sake. Thanks so much in advance.
user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :organizations, :through => :memberships
end
membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :organization
end
organization.rb
class Organization < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
accepts_nested_attributes_for :memberships, :allow_destroy => true
...
before_create :add_membership
protected
def add_membership
self.memberships.build
end
end
You are right in the fact that allowing your model to magically know about the current user is bad MVC practice. So you have to somehow pass the current user id during creation. you can do this in many ways ; for example in the controller :
def create
#organization = Organization.new( params[:organization] ) do |org|
org.memberships.build( user_id: current_user.id )
end
# save, etc.
end
Doing this in the controller is fine, but it would be better if your business logic would reflect the fact that a user creating an organization should automatically belong to it. You could override new and / or create on Organization (or create your own method if you fear overrides) :
def new( params = {}, options = {} )
creator = options.delete( :creator )
super( params, options ) do |org|
org.memberships.build( user_id: creator.id ) if creator
yield org if block_given?
end
end
passing the user is easy now :
def create
#organization = Organization.new(params[:organization], creator: current_user)
end
If you don't like this approach, or if you don't want to override new or create a specific factory method, you can also make something similar to nested_attributes :
attr_accessible :creator_id
def creator_id=( user_id )
memberships.build user_id: user_id
end
then in your view :
f.hidden_field :creator_id, current_user.id
optional :
with first approach, for additional clarity / ease of use, you can also create a method on User :
def new_organization( params = {}, options = {}, &block )
Organization.new( params, options.merge(creator: self), &block )
end
... ok, Organization is hardcoded here (bad !) but your workflow is now quite understandable :
def create
# we know at first glance that the user is responsible for the organization
# creation, and that there must be specific logic associated to this
#organization = current_user.new_organization( params[:organization] )
# etc
end
with a little more thinking, it should be possible to avoid hardcoding Organization into User (using an association extension for instance)
EDIT
To be able to setup a validation on membership's organization presence, you need to do this :
class Organization < ActiveRecord::Base
has_many :memberships, inverse_of: :organization
end
class Membership < ActiveRecord::Base
belongs_to :organization, inverse_of: :memberships
validates :organization, presence: true
end
Let's explain this :
inverse_of sets up your associations to be bidirectional. By default, associations are one-way, which means that when you do organization.memberships.first.organization, rails tries to load the organisation again because it does not know how to "climb back" the association. When using inverse_of, rails knows it does not have to reload the organization.
validates MUST be setup on organization and NOT on organization_id. This way the validator knows we're "climbing back" the association, it knows that organization is a "parent" record and that it's in the process of being saved - so it does not complain.

how to create a challenge between user and a friend similar to Facebook in rails app?

I have a rails app where users have friends list. Now i have to create a challenge similar to facebook challenge where the user can finish the process(playing game) and he can challenge his friend and his friend can accept or deny the request and if accepted, after the process(playing game) finished the message to both user has to be sent which contains who has won.
How can i do this? please help me.
Sounds like you want a new model called Challenge. This might have a couple of associations:
class Challenge < ActiveRecord::Base
belongs_to :sender, class_name: "User", inverse_of: :sent_challenges
belongs_to :receiver, class_name: "User", inverse_of: :received_challenges
end
The corresponding associations on the User could be
class User < ActiveRecord::Base
# ...
has_many :sent_challenges,
class_name: "Challenge", foreign_key: "sender_id", inverse_of: :sender
has_many :received_challenges,
class_name: "Challenge", foreign_key: "receiver_id", inverse_of: :receiver
end
Then you could perhaps have a method on your User to send a challenge
def send_challenge(friend)
sent_challenges.create(receiver: friend)
end
You might have some actions on your ChallengesController:
def index
#challenges = current_user.received_challenges
end
def create
#challenge = current_user.send_challenge(params[:friend_id])
# now the sender plays the game
render :game
end
def accept
#challenge = current_user.received_challenges.find(params[:id])
# now the receiver plays the game
render :game
end
def deny
current_user.received_challenges.destroy(params[:id])
redirect_to challenges_url
end
def complete
# happens at the end of the game
# work out the winner
# send the emails
end
and of course you'll need to add the corresponding routes to hook it all up, and write views for the index and game. Maybe you'd put links on your friend list that directed to the create action so that people could issue challenges.
Notice how I put everything through current_user.received_challenges instead of just doing a basic Challenge.find(params[:id]) - if you did that, anybody could accept the challenge just by guessing the id! Yikes!
I said "perhaps" and "maybe" a lot because there are different ways you could tackle this. But I hope that's enough to get you started. If not, I would suggest trying a Rails tutorial - Michael Hartl's is the classic.
Do you got the has_many :through relation already?
You need to pass a :source to the users table because a user can also be a friend. This would look like this:
class User < ActiveRecord::Base
has_many :friends
has_many :users, :source => :friend, :through => :friends
end
PS: You need to create the migration for the friends table and run it.
Its possible to add more columns to the join table (friends). There you could add the relationship_status. So that you have in the end:
Friends Table
ID | User_id | Friend_id | relationship_status
based on the relationship_status you can solve your issue!

How to associate a new model with existing models using has_and_belongs_to_many

I have two models with a many to many relationship using has_and_belongs_to_many. Like so:
class Competition < ActiveRecord::Base
has_and_belongs_to_many :teams
accepts_nested_attributes_for :teams
end
class Team < ActiveRecord::Base
has_and_belongs_to_many :competitions
accepts_nested_attributes_for :competitions
end
If we assume that I have already created several Competitions in the database, when I create a new Team, I would like to use a nested form to associate the new Team with any relevant Competitions.
It's at this point onwards that I really do need help (have been stuck on this for hours!) and I think my existing code has already gone about this the wrong way, but I'll show it just in case:
class TeamsController < ApplicationController
def new
#team = Team.new
#competitions.all
#competitions.size.times {#team.competitions.build}
end
def create
#team = Team.new params[:team]
if #team.save
# .. usual if logic on save
end
end
end
And the view... this is where I'm really stuck so I won't both posting my efforts so far. What I'd like it a list of checkboxes for each competition so that the user can just select which Competitions are appropriate, and leave unchecked those that aren't.
I'm really stuck with this one so appreciate any pointing in the right direction you can provide :)
The has_and_belongs_to_many method of joining models together is deprecated in favor of the new has_many ... :through approach. It is very difficult to manage the data stored in a has_and_belongs_to_many relationship, as there are no default methods provided by Rails, but the :through method is a first-class model and can be manipulated as such.
As it relates to your problem, you may want to solve it like this:
class Competition < ActiveRecord::Base
has_many :participating_teams
has_many :teams,
:through => :participating_teams,
:source => :team
end
class Team < ActiveRecord::Base
has_many :participating_teams
has_many :competitions,
:through => :participating_teams,
:source => :competition
end
class ParticipatingTeam < ActiveRecord::Base
belongs_to :competition
belongs_to :team
end
When it comes to creating the teams themselves, you should structure your form so that one of the parameters you receive is sent as an array. Typically this is done by specifying all the check-box fields to be the same name, such as 'competitions[]' and then set the value for each check-box to be the ID of the competition. Then the controller would look something like this:
class TeamsController < ApplicationController
before_filter :build_team, :only => [ :new, :create ]
def new
#competitions = Competitions.all
end
def create
#team.save!
# .. usual if logic on save
rescue ActiveRecord::RecordInvalid
new
render(:action => 'new')
end
protected
def build_team
# Set default empty hash if this is a new call, or a create call
# with missing params.
params[:team] ||= { }
# NOTE: HashWithIndifferentAccess requires keys to be deleted by String
# name not Symbol.
competition_ids = params[:team].delete('competitions')
#team = Team.new(params[:team])
#team.competitions = Competition.find_all_by_id(competition_ids)
end
end
Setting the status of checked or unchecked for each element in your check-box listing is done by something like:
checked = #team.competitions.include?(competition)
Where 'competition' is the one being iterated over.
You can easily add and remove items from your competitions listing, or simply re-assign the whole list and Rails will figure out the new relationships based on it. Your update method would not look that different from the new method, except that you'd be using update_attributes instead of new.

Resources