Creating a new object when creating another - ruby-on-rails

I have 3 models: account, player_team and team. Player team serves to associate accounts and teams. Player_team table has account_id and team_id attributes. When I create the team, i should at least have the account who created it belonging to the team. What am i doing wrong ?Any help would be appreciated, Thanks.
def create
#team = Team.new(team_params)
#team.save
#team_player = current_account.player_teams.build(:account_id => current_account.id, :team_id => #team.id)
#team_player.save
respond_with(#team)
end
class Account < ActiveRecord::Base
has_many :player_teams
has_many :teams, through: :player_teams
class Team < ActiveRecord::Base
has_many :player_teams
has_many :accounts, through: :player_teams
end
class PlayerTeam < ActiveRecord::Base
belongs_to :account
belongs_to :team
end

Because you are creating the object right into the controller (instead of just declaring it and opening a form in the view to enter parameters) , you have to use the
new
keyword.
A solution of your problem would be
#team_player = current_account.player_teams.new(:account_id => current_account.id, :team_id => #team.id)

This should work:
def create
#team = Team.new(team_params)
#team.save
#team_player = current_account.build_player_team(:account_id => current_account.id, :team_id => #team.id)
#team_player.save
respond_with(#team)
end
Build on it's own won't save, and saving the parent won't do anything. You need to use build_player_team, or use create() instead of build. Either would work.
def create
#team = Team.new(team_params)
#team.save
#team_player = current_account.player_teams.create(:account_id => current_account.id, :team_id => #team.id)
#team_player.save
respond_with(#team)
end

Note that there's no need to go through all this trouble manually. You could have just said:
respond_with(#team = current_account.teams.create(team_params))

Related

Rails with ActionMailer and complex activerecord (joins, where, and)

I would like to send an email when a project is registered (with a category and a eligible audience) to each user who has created an alert (with the same category and same eligible audience).
MODEL
class Project
belongs_to :category
belongs_to :fondation
has_many :project_eligibles
has_many :eligibles, through: :project_eligibles
end
class Category
has_many :projects
has_many :alerts
end
class ProjectEligible
belongs_to :project
belongs_to :eligible
end
class Alert
belongs_to :user
belongs_to :category
belongs_to :eligible
end
MAILER
class ProjectMailer < ApplicationMailer
def newproject(project)
#project = project
mail(
to: mails = User.joins(:alerts).where(alerts: {category_id: project.category_id}).collect(&:email).join(","),
subject: "New project for you !"
)
end
end
CONTROLLER
class ProjectsController < ApplicationController
def create
#project = Project.new(project_params)
if #project.save
ProjectMailer.newproject(#project).deliver_now
redirect_to projects_path
else
render :new
end
end
end
it works with the category but I can't do it with the eligible audience (many_to_many association) :
def newproject(project)
#project = project
mail(
to: mails = User.joins(:alerts).where(alerts: {eligible_id: project.project_eligibles.where(:eligible_ids)}).collect(&:email).join(","),
subject: "New project for you !"
)
end
end
And more difficult, I don't know how to do it with the 2 conditions ??
Does anyone have an idea to test?
A thousand thanks in advance for your help!
Try this in your ProjectMailer
project_elegibles = ProjectEligible.whare("project_id =?", #project.id).pluck(:eligible_id)
User.includes(:alerts).where("alerts.category_id =? and alerts.eligible_id IN (?)", #project.category_id, project_elegibles).pluck(:email)
Understand steps that i have used to deal with many-to-many relationship. I can't test this in my machine as I don't have the code. But this will helpful to fix your problem.
Assuming project = #project
mails = User.joins(:alerts).where("alerts.eligible_id IN (?) AND alerts.category_id = ?", project.project_eligibles.pluck(:eligible_id), project.category_id).pluck(:"users.email")
Possibly you should use left_outer_join
For rails 5.x.x
mails = User.left_outer_joins(:alerts).where("alerts.eligible_id IN (?) AND alerts.category_id = ?", project.project_eligibles.pluck(:eligible_id), project.category_id).pluck(:"users.email")

Rails 5.1, delete multiple records with conditions

I try to delete several records in my DB according to a few conditions. Before I talk about my problem I will explain how my app is working.
A User can create groups and links. This User own the groups he created same for the links. Other Users can then join this group by providing a token (which is created automatically when the group is created) via a Member model. Where member has the user_id and the group_id in the DB. Then once the User is part of a group he can share the links he created in the groups via Grouplink model. Where grouplink has groupd_id and link_id.
I managed to code the fact that if there is no member in a group the group is destroyed. But how do I manage to remove the links a user shared in the group if he leaves the group ? (when the user is destroyed automatically all the links are deleted so I think that the sharing should be gone as well, I have to try that tho'). When a User leaves the group I destroy his member record and he is gone but the links remain. I display the shared links, the fact that you can kick users (member destroy) and the list of the members of a group in the group show btw.
I thought about a few things. I write down what I did in the console
g = Group.find(66)
u = g.users.find(1)
u.links
and that give me all the links from the user in the group. Next to that
g.grouplinks
would give me all the shared links in the group.
g.grouplinks.map(&:link_id) returns [16, 17, 14, 13, 15]
u.links.map(&:id) returns [13, 15]
Now what can I do here ? My user is leaving the group. The member record is destroyed and how can I destroy those grouplinks according to the links the users has ?
Is there a magic trick I don't know yet ?
Thanks for your help.
EDIT
class User < ApplicationRecord
has_secure_password
has_many :links, dependent: :destroy
has_many :grouplinks, dependent: :destroy
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
has_one :owned_group, foreign_key: "owner_id", class_name: "Group"
end
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
class Link < ApplicationRecord
has_many :grouplinks, :dependent => :destroy
belongs_to :user
end
class Grouplink < ApplicationRecord
belongs_to :group
belongs_to :link
end
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, through: :members, source: :user
belongs_to :owner, class_name: "User"
has_many :links, through: :grouplinks
has_many :grouplinks, :dependent => :destroy
def to_param
auth_token
end
end
I thought that actually I could add the user_id in the grouplinks so I could delete_all according to the user_id in the links and in the groupslinks. Not sure how to that tho' and don't know if there is a better solution.
EDIT 2
I tried your solution within the models. Actually it is smart and I didn't think about that...
Problem is now with the creation of my grouplink (share the link). I had this :
def create
user = current_user if current_user
group = user.groups.find_by(auth_token: params[:auth_token])
share = group.id
group_link = group.grouplinks.build(link_id: params[:link_id])
gl = group.grouplinks
if gl.where(group_id: share).where(link_id: params[:link_id]).exists?
flash[:error] = "You shared this link in '#{group.name}' already."
redirect_to mylinks_path
else
if group_link.save
group_link.toggle!(:shared)
flash[:success] = "You shared your link in '#{group.name}'."
redirect_to mylinks_path
else
render 'new'
end
end
end
And this is obviously not working anymore and I have this error when I try to share a link : First argument in form cannot contain nil or be empty <%= form_for #grouplink do |f| %>.
I tried to change it like this :
def create
group = Group.find_by(auth_token: params[:auth_token])
share = group.id
group_link = group.grouplinks.build(link_id: params[:link_id])
gl = group.grouplinks
if gl.where(group_id: share).where(link_id: params[:link_id]).exists?
flash[:error] = "You shared this link in '#{group.name}' already."
redirect_to mylinks_path
else
if group_link.save
group_link.toggle!(:shared)
flash[:success] = "You shared your link in '#{group.name}'."
redirect_to mylinks_path
else
render 'new'
end
end
end
But it is not working either
How about:
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
has_many :group_links, dependent: :destroy
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
and
class Grouplink < ApplicationRecord
belongs_to :link
belongs_to :member
end
Now, when a Member record is destroyed (i.e., the user is kicked out of or leaves the group), any links shared with the group (i.e., group_links) are also destroyed. But, if the user has shared the link in another group, the link will continue to be shared with the other groups.
As mentioned by #Pablo in the comments, you probably also want to do:
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :grouplinks, through: :members
has_many :users, through: :members, source: :user
belongs_to :owner, class_name: "User"
has_many :links, through: :grouplinks
def to_param
auth_token
end
end
Which will allow you to do:
group.grouplinks
I also agree with #amr-el-bakry that Member is a bit confusing. I suggest GroupUser as it makes it quite clear that it is an association between Group and User.
Also, I think it might be a bit more conventional to say GroupLink instead of Grouplink. Or, if you want to stick with naming based on associated classes, perhaps MemberLink. If you change Member to GroupUser, then perhaps GroupUserLink.
I'm thinking your create code should probably look something like:
def create
if group
if member
if link
unless group_link
#group_link = member.group_links.build(link: link)
if group_link.save
group_link.toggle!(:shared)
flash[:success] = "You shared your link in '#{group.name}'."
redirect_to mylinks_path
else
render :new
end
else
flash[:error] = "You shared this link in '#{group.name}' already."
else
flash[:error] = "That link does not exist."
redirect_to somewhere #fix this
end
else
flash[:error] = "You must be a member of this group to add a link."
redirect_to somewhere #fix this
end
else
flash[:error] = "There is no group with that token."
redirect_to somewhere #fix this
end
end
private
def group
#group ||= Group.find_by(auth_token: params[:auth_token])
end
def member
#member ||= current_user.members.where(group: group)
end
def link
#link ||= Link.find_by(id: params[:link_id])
end
def group_link
#group_link ||= member.group_links.where(link: link)
end
You may be able to write this as:
def create
flash[:error] = "There is no group with that token."
redirect_to somewhere unless group
flash[:error] = "You must be a member of this group to add a link."
redirect_to somewhere unless member
flash[:error] = "That link does not exist."
redirect_to somewhere unless link
flash[:error] = "You shared this link in '#{group.name}' already."
redirect_to mylinks_path if group_link
flash[:error] = nil
#group_link = member.group_links.build(link: link)
if group_link.save
group_link.toggle!(:shared)
flash[:success] = "You shared your link in '#{group.name}'."
redirect_to mylinks_path
else
render :new
end
end
But I can't remember if those redirects will give you heartache.
What you're looking for is dependent :delete_all
In your Group Model, you want to you should have a line like:
has_many :links, dependent :delete_all
This says, the group has many links and if you destroy the group, destroy all the related links.
In your Member model, you could use an after_destroy callback to destroy all user links in the group after membership record is destroyed:
class Member < ApplicationRecord
after_destroy do |record|
record.user.links.each { |link| link.grouplinks.where(group_id: record.group.id).destroy_all }
end
belongs_to :user
belongs_to :group
...
end
Also, I suggest you change Member to Membership to be more clear.
I think the best idea is that the group_links belong to a member and a link (and not a group and a link). And a member (not a user) has many group_links. When your destroy the member, it will destroy the group_links.
EDIT
This is what jvillian suggested in his answer, just before I did. So I believe his answer is the right one (with some minor enhancements I suggested in comments that jvillian will surely accept and add :-).
EDIT2
Regarding the problem you faced after applying jvillian suggestion, when creating a new grouplink, it must be done from a member (not a group). So in the create action you must search the member (by user_id and group_id) and create the grouplink as member.grouplinks.build

Ruby: How do I assign values in the controller when using nested forms?

I have 3 models: Employers, Partners and Collaborations.
As an Employer, I want to add a record to my Partner model and to my Collaboration model to be able to indicate a collaboration between a Partner and a Employer. I therefore have the following columns in my database/tabels.
Models
class Employer < ActiveRecord::Base
has_many :collaborations
has_many :partners, :through => :collaborations
end
class Partner < ActiveRecord::Base
has_many :collaborations
has_many :employers, :through => :collaborations
accepts_nested_attributes_for :collaborations
end
class Collaboration < ActiveRecord::Base
belongs_to :employer
belongs_to :partner
end
Tables
Collaborations
employer_id:integer
partner_id:integer
tarive:string
Partners
added_by:integer
name:string
Because I want to be able to add a Partner/Collaboration within 1 form, I use nested forms. So I can add a partner (name, etc) and a collaboration (tarive, etc) in one go.
My (simple_form) form looks like this (I have named_space resource).
Te reduce clutter, I removed as much HTML mark_up as I could, this is not the issue.
Form
/views/employer/partners/_form
= simple_form_for [:employer, #partner], html: { multipart: true } do |f|
Partner
= f.input :name, input_html: { class: 'form-control' }
= f.simple_fields_for :collaborations do |ff|
Tarive
= ff.input :tarive, input_html: { class: 'form-control' }
= f.button :submit, "Save"
My controller looks like
class Employer::PartnersController < ActionController::Base
def new
#partner = Partner.new
#partner.collaborations.build
end
def create
#partner = Partner.new(partner_params)
#partner.collaborations.build
#partner.added_by = current_employer.id
#partner.collaborations.employer_id = current_employer.employer_id
#partner.collaborations.partner_id = #partner.id
#partner.collaborations.added_by = current_employer.id
if #partner.save
redirect_to employer_partner_path(#partner), notice: "Succes!"
else
render 'new'
end
end
def partner_params
params.require(:partner).permit(:id, :name, collaborations_attributes: [:id, :employer_id, :partner_id, :tarive])
end
end
Problem
The problem/question I have is this. The attributes are assigned nicely and added in the model. But I want to add a employer_id as well, which I have in current_employer.employer.id (Devise). I do not want to work with hidden forms, just to avoid this issue.
I assigned 'parent' models always like #partner.added_by = current_employer.id and that works beautifully.
When I use:
#partner.collaborations.employer_id = current_employer.employer_id
I get an error, saying #partner.collaborations.employer_id is empty.
Question
How can I assign a variable to the nested_form (Collaboration) in my controller#create?
Or more specifically: how can I assign current_employer.employer_id to #partner.collaborations.employer_id?
There are several ways:
Merge the params
Deal with objects, not foreign keys
Personally, I feel your create method looks really inefficient. Indeed, you should know about fat model skinny controller - most of your associative logic should be kept in the model.
It could be improved using the following:
#app/controllers/employers/partners_controller.rb
class Employers::PartnersController < ApplicationController
def new
#partner = current_employer.partners.new #-> this *should* build the associated collaborations object
end
def create
#partner = current_employer.partners.new partner_params
#partner.save ? redirect_to(employer_partner_path(#partner), notice: "Succes!") : render('new')
end
private
def partner_params
params.require(:partner).permit(:id, :name, collaborations_attributes: [:tarive]) #when dealing with objects, foreign keys are set automatically
end
end
This would allow you to use:
#app/views/employers/partners/new.html.erb
= simple_form_for #partner do |f| #-> #partner is built off the current_employer object
= f.input :name
= f.simple_fields_for :collaborations do |ff|
= ff.input :tarive
= f.submit
... and the models:
#app/models/partner.rb
class Partner < ActiveRecord::Base
belongs_to :employer, foreign_key: :added_by
has_many :collaborations
has_many :employers, through: :collaborations
accepts_nested_attributes_for :collaborations
end
#app/models/collaboration.rb
class Collaboration < ActiveRecord::Base
belongs_to :employer
belongs_to :partner
belongs_to :creator, foreign_key: :added_by
before_create :set_creator
private
def set_creator
self.creator = self.employer_id #-> will probably need to change
end
end
#app/models/employer.rb
class Employer < ActiveRecord::Base
has_many :collaborations
has_many :employers, through: :collaborations
end
This may not give you the ability to set tarive, however if you cut down the manual declarations in your model, we should be able to look at getting that sorted.
The main thing you need to do is slim down your code in the controller. You're being very specific, and as a consequence, you're encountering problems like that which you mentioned.

requesting membership to a group mvc

I have a group model that has_many :members, and has_many :memberships. What I would like to do is make it so that in some groups the creator of the group would make it so that you have to request membership in order to join that specific group. How could I set this up in my rails application?
I have added a boolean field to the memberships ActiveRecord but I dont know how to set it up in a way that would allow me to join groups that dont require the "request a membership" function but also to create a "request a membership" function.
as of right now my models look like this:
class Group < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :members, :through => :memberships
has_many :memberships, :foreign_key => "new_group_id"
has_many :events
end
class User < ActiveRecord::Base
has_many :groups, foreign_key: :creator_id
has_many :memberships, foreign_key: :member_id
has_many :new_groups, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :member, class_name: "User"
belongs_to :new_group, class_name: "Group"
validates :new_group_id, uniqueness: {scope: :member_id}
has_many :accepted_memberships, -> {where(memberships: { approved: true}) }, :through => :memberships
has_many :pending_memberships, -> {where(memberships: { approved: false}) }, :through => :memberships
end
and my membership controller:
class MembershipsController < ApplicationController
def create
#group = Group.find(params[:new_group_id])
#membership = current_user.memberships.build(:new_group_id => params[:new_group_id])
if #membership.save
flash[:notice] = "Joined #{#group.name} "
else
flash[:notice] = "You're already in this group"
end
redirect_to groups_url
end
def destroy
#group = Group.find(params[:id])
#membership = current_user.memberships.find_by(params[membership_id: #group.id]).destroy
redirect_to groups_url
end
end
I believe that you are already very close to your solution, and that it is more of a business problem than a technical one. First I would add a boolean to the group to indicate that approval is required. e.g.
rails generate migration add_require_approval_to_groups require_approval:boolean
This would get set when the creator first creates the group depending upon the type of group that they have created.
Now, somehow a user has to be able to discover that there are groups that they can join, and you need to communicate an awareness to them that for some groups, membership is not automatic, but must be approved by the group creator.
So, assuming that you have communicated this to the user, and that they are on a page with a selection box listing all of the groups that they can become a member of (not necessarily the best design choice, but will do for this example). You need to have a query in your model that will gather all of the available groups that a user can still join.
def self.available_groups(user_id)
where("id not in (select group_id from group_members where user_id = ?)", user_id)
.select("id, name")
.collect{ |g| [g.name, g.id] }
end
In your controller:
#available_groups = Group.available_groups(#current_user)
And in your view:
<h2>Please select the group to join:</h2>
<p>
<%= form_tag :action => 'join_group' do %>
<%= select("group", "id",
#available_groups) %>
<%= submit_tag "Join" %>
<% end %>
</p>
Now, when you process the "post" in your membership_controller, you need to inform the creator that someone is trying to join the group that requires approval (perhaps a mailer). If the require_approval boolean is not set, then you need to automatically approve the user so that they can access the group immediately.

ActiveRecord (Rails 2.3.8) - Update existing, add new record when updating nested attributes

I have a "user" model that "has_one" "membership" (active at a time). For auditing and data integrity reasons, I'd like it so that if the membership changes for a user, the old/current record (if existing) has an inactive/active flag swapped, and a new row is added for the new changed record. If there are no changes to the membership, I'd like to just ignore the update. I've tried implementing this with a "before_save" call-back on my user model, but have failed many times. Any help is greatly appreciated.
models:
class User < ActiveRecord::Base
has_one :membership, :dependent => :destroy
accepts_nested_attributes_for :membership, :allow_destroy => true
end
class Membership < ActiveRecord::Base
default_scope :conditions => {:active => 1}
belongs_to :user
end
I have what I think is a pretty elegant solution. Here's your user model:
class User < ActiveRecord::Base
has_one :membership, :dependent => :destroy
accepts_nested_attributes_for :membership
def update_membership_with_history attributes
self.membership.attributes = attributes
return true unless self.membership.changed?
self.membership.update_attribute(:active, false)
self.build_membership attributes
self.membership.save
end
end
This update_membership_with_history method allows us to handle changed or unchanged records. Next the membership model:
class Membership < ActiveRecord::Base
default_scope :conditions => {:active => true}
belongs_to :user
end
I changed this slightly, since active should be a boolean, not 1's and 0's. Update your migration to match. Now the update action, which is the only part of your scaffold that needs to change:
def update
#user = User.find(params[:id], :include => :membership)
membership_attributes = params[:user].delete(:membership_attributes)
if #user.update_attributes(params[:user]) && #user.update_membership_with_history(membership_attributes)
redirect_to users_path
else
render :action => :edit
end
end
We're simply parsing out the membership attributes (so you can still use fields_for in your view) and updating them separately, and only if needed.
Did you look at acts_as_versioned? In the before_save of the Membership you could create a new version of the User, which would be acts_as_versioned.
Got it working. While it's probably not the best implementation, all my tests are passing. Thanks for the input guys.
before_save :soft_delete_changed_membership
def soft_delete_changed_membership
if !membership.nil? then
if !membership.new_record? && membership.trial_expire_at_changed? then
Membership.update_all( "active = 0", [ "id = ?", self.membership.id ] )
trial_expire_at = self.membership.trial_expire_at
self.membership = nil
Membership.create!(
:user_id => self.id,
:trial_expire_at => trial_expire_at,
:active => true
)
self.reload
end
end
end
Why don't you just assume that the latest membership is the active one. This would save you a lot of headache.
class User < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
end
class Membership < ActiveRecord::Base
nested_scope :active, :order => "created_at DESC", :limit => 1
belongs_to :user
def update(attributes)
self.class.create attributes if changed?
end
end
then you can use
#user.memberships.active
to get the active membership, and you can just update any membership to get a new membership, which will become the active membership because it is the latest.

Resources