Current User to Dynamically Join/Leave a Team - ruby-on-rails

Simple feature to implement: clicking on a big button called "Join This Team" allows the currently signed in user to--gasp--join the aforementioned team.
So, here's what I got:
class User < ActiveRecord::Base
#devise code
has_many :memberships
has_many :teams, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :team
end
class Team < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
validates :name, uniqueness: true
end
Important question here: it's key that a user can join more than one team. Is using a join table the right approach here? Without one, correct me if I'm wrong, a user will be stuck to only a single team, right?
So, I got a simple button in teams#show
<%= link_to '#', :class => 'jointeam', :data => {:id => #team.id} do %>
<div class='ui button'>Join This Team</div>
<% end %>
Now, upon clicking this button, I want the user to be added to this team. I have some JS that's incomplete:
$('.jointeam').on('click', function(e){
var id = $(this).data("id");
e.preventDefault;
$.ajax({
type: "POST",
url: "/teams/" + id,
data: {_method:"PUT", }
});
});
So, I'm thinking a POST request with a method "PUT" with jQuery. Here's where I'm stuck. Since I used a Join-table called memberships, what's the right way to implement this feature? Do I need a memberships controller? Can I have a method in my Team's controller that allows the current_user to associate to the team in question? Do I need to write a custom method? How does it tie to the jQuery ajax request to be syntactically correct?
Thanks in advance!

You can add a custom RESTful action to teams by editing the team resource in routes.rb:
resources :team do
post 'join', on: :member
end
this will allow URLs like /teams/<team ID>/join
you can then create a join method in the TeamController the basics of which would be:
def join
team = Team.find(params[:id])
membership = Membership.new(team: team, user: current_user)
current_user.memberships << membership
end

Related

Validate presence of nested attributes within a form

I have the following associations:
#models/contact.rb
class Contact < ActiveRecord::Base
has_many :contacts_teams
has_many :teams, through: :contacts
accepts_nested_attributes_for :contacts_teams, allow_destroy: true
end
#models/contacts_team.rb
class ContactsTeam < ActiveRecord::Base
belongs_to :contact
belongs_to :team
end
#models/team.rb
class Team < ActiveRecord::Base
has_many :contacts_team
has_many :contacts, through: :contacts_teams
end
A contact should always have at least one associated team (which is specified in the rich join table of contacts_teams).
If the user tried to create a contact without an associated team: a validation should be thrown. If the user tries to remove all of a contact's associated teams: a validation should be thrown.
How do I do that?
I did look at the nested attributes docs. I also looked at this article and this article which are both a bit dated.
For completion: I am using the nested_form_fields gem to dynamically add new associated teams to a contact. Here is the relevant part on the form (which works, but currently not validating that at least one team was associated to the contact):
<%= f.nested_fields_for :contacts_teams do |ff| %>
<%= ff.remove_nested_fields_link %>
<%= ff.label :team_id %>
<%= ff.collection_select(:team_id, Team.all, :id, :name) %>
<% end %>
<br>
<div><%= f.add_nested_fields_link :contacts_teams, "Add Team"%></div>
So when "Add Team" is not clicked then nothing gets passed through the params related to teams, so no contacts_team record gets created. But when "Add Team" is clicked and a team is selected and form submitted, something like this gets passed through the params:
"contacts_teams_attributes"=>{"0"=>{"team_id"=>"1"}}
This does the validations for both creating and updating a contact: making sure there is at least one associated contacts_team. There is a current edge case which leads to a poor user experience. I posted that question here. For the most part though this does the trick.
#custom validation within models/contact.rb
class Contact < ActiveRecord::Base
...
validate :at_least_one_contacts_team
private
def at_least_one_contacts_team
# when creating a new contact: making sure at least one team exists
return errors.add :base, "Must have at least one Team" unless contacts_teams.length > 0
# when updating an existing contact: Making sure that at least one team would exist
return errors.add :base, "Must have at least one Team" if contacts_teams.reject{|contacts_team| contacts_team._destroy == true}.empty?
end
end
In Rails 5 this can be done using:
validates :contacts_teams, :presence => true
If you have a Profile model nested in a User model, and you want to validate the nested model, you can write something like this: (you also need validates_presence_of because validates_associated doesn't validate the profile if the user doesn't have any associated profile)
class User < ApplicationRecord
has_one :profile
accepts_nested_attributes_for :profile
validates_presence_of :profile
validates_associated :profile
docs recommend using reject_if and passing it a proc:
accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Model Names:
1: approval
2: approval_sirs
Associations:
1: approval
has_many :approval_sirs, :foreign_key => 'approval_id', :dependent => :destroy
accepts_nested_attributes_for :approval_sirs, :allow_destroy => true
2: approval_sirs
belongs_to :approval , :foreign_key => 'approval_id'
In approvals.rb
## nested form validations
validate :mandatory_field_of_demand_report_sirs
private
def mandatory_field_of_demand_report_sirs
self.approval_sirs.each do |approval_sir|
unless approval_sir.marked_for_destruction?
errors.add(:base, "Demand Report Field are mandatory in SIRs' Detail") unless approval_sir.demand_report.present?
end
end
end

Rails 4 self referential many to many relationships

I can't wrap my head around this any help would be appreciated. I went through many articles and other postings on here and I can't seem to get the results I'm looking for.
I have a User model and Team model.
Team Model has user_id and team_id
The user who creates the team will be user_id and users who are members of the team will be in the team_id
User Model
has_many :teams, foreign_key: :team_id
has_many :team_members, class_name: "Team", foreign_key: :user_id
Team Model
belongs_to :user, foreign_key: :team_id
belongs_to :team_member, class_name: "User", foreign_key: :user_id
The end result of what I'm trying to achieve is:
Each user can add as many team members
Each user can see a list of users who are part of their team.
A view where, Users who are part of a team can see which team they are part of.
I believe what you're looking for is a join table model. The issue is that both a User and a Team may have many of each other. So the relationship must be stored separately.
See this answer here on a similar question: https://stackoverflow.com/a/15442583/5113832
So you might choose a model structure of User, Team and TeamMembership.
Updated to destroy dependent team memberships when a user or team is destroyed.
#app/models/user.rb
class User < ActiveRecord::Base
has_many :team_memberships, :dependent => :destroy
has_many :teams, :through => :team_memberships
end
#app/models/team.rb
class Team < ActiveRecord::Base
has_many :team_memberships, :dependent => :destroy
has_many :users, :through => :team_memberships
end
#app/models/team_membership.rb
class TeamMembership < ActiveRecord::Base
belongs_to :user
belongs_to :team
end
Updated to reply to question:
How would my controller look on create? (Adding a user to a team) – Michael
In order to add a user to to a team you COULD do it in UserController, you COULD do it in TeamController. However, because you are now creating a TeamMembership resource you would want to create a TeamMembership record in a TeamMembershipsController. This keeps with the "Rails" way of doing things. So for example:
# app/controllers/team_memberships_controller.rb
class TeamMembershipsController < ApplicationController
def index
#team_memberships = TeamMembership.all
end
def new
#team_membership = TeamMembership.new
end
def create
#team_membership = TeamMembership.new(team_membership_params)
if #team_membership.save
flash[:success] = 'Team membership created'
redirect_to team_memberships_path
else
flash[:error] = 'Team membership not created'
render :new
end
end
def destroy
#team_membership = TeamMembership.find_by_id(params[:id])
if #team_membership && #team_membership.destroy
flash[:success] = 'Team membership destroyed'
else
flash[:error] = 'Team membership not destroyed'
end
redirect_to team_memberships_path
end
private
def team_membership_params
params.require(:team_membership).permit(
:user_id,
:team_id
)
end
end
The advantage to having the TeamMembership resource is using this pattern to manage when a user is added (#create), or removed (#destroy) from a team.
The magic of Rails associations will take care of accessing a team's members (users) and a user's teams for each instance of those models.
You just go about your business doing CRUD on these resources and Rails takes care of the organization for you by your conforming to it's conventions.
Also I updated my original model code to destroy team memberships when a user or team is destroyed. This ensures no orphaned records are in your team_memberships table.
As a final note. You should also be able to easily use form_for to send a TeamMembership to your controller to be created. This could be done using select option dropdowns for users and teams with Rails' collection_select:
<%# app/views/team_memberships/new.html.erb %>
<h1>
Create Team Membership
</h1>
<%= form_for(#team_membership) do |f| %>
<%= f.label(:user) %>
<%= f.collection_select(
:user_id,
User.all,
:id,
:username
) %>
<%= f.label(:team) %>
<%= f.collection_select(
:team_id,
Team.all,
:id,
:name
) %>
<%= f.submit %>
<% end %>
The above code will render dropdown for all users and teams allowing you to select a specific combination to create a team membership from. Deleting a team membership is as easy as sending a DELETE #destroy request with the id of the team membership.
Another consideration might be adding a unique pair constraint to your database table and model within the migration and model validations.
Hope this helps!

'Messaging System' with more than 1 user in conversation, build from scratch in Rails

So I am trying to implement a messaging system in my application, and I have no intention on using any of the gems. I've seen most of them and I'm able to use them. But for learning sake, I want to learn how it works and build one from scratch and be able to customize it how I want it.
I have looked around for tutorial but there isn't any tutorial or anything concrete information except for; this -> http://.novawave.net/public/rails_messaging_tutorial.html, but unfortunately link is down or this -> Rails threaded private messaging, but I still cant wrap my head around everything.
So hope this thread will serve as a point of reference for others.
So based off this thread Rails threaded private messaging, this is what I have including the columns definitions.
But I'm having problem wrapping my head around the logic on adding multiple users in a conversation. The way that I see this:
Click on send a message, which will trigger a new conversation object
Add a subject, and select users that I want in the conversation <-- this is where it gets cloudy
at the bottom without of that same form without any ajax, I guess I could render message form which will submit the text?
Ok so how do I put multiple user ids in the conversation table users_id column? There is a suggestion to use 'act_as_taggable' gem from this thread -> Rails threaded private messaging comes in? If so, how is the database is going to know that it should select all these user in a certain conversation object.
class Conversation < ActiveRecord::Base
#columns -> :subject, :users_id
has_many :messages
has_many :participants
has_many :users, :through => :participants
end
class Message < ActiveRecord::Base
#columns -> :conversation_id, :sender_id, :read
belongs_to :conversation
end
class Participant < ActiveRecord::Base
#columns -> :user_id, :conversation_id
belongs_to :conversation
belongs_to :user
end
class User < ActiveRecord::Base
has_many :conversations
has_many :participants
end
The conversations table should not have a user_id/users_id column as you have setup a many to many relationship in this case.
You can add users to a conversation by doing something like:
#conversation.users << #user
#conversation.save
Or in a form it would be something like:
<%= form_for Conversation.new do |f| %>
<%= f.collection_select :user_ids, User.all, :id, :name, {prompt: true}, {multiple: true} %>
<%= f.submit %>
<% end %>
You can get users in a conversation with #conversion.users.
Also, your user model should be:
class User < ActiveRecord::Base
has_many :participants
has_many :conversations, through: :participants
end

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.

Passing object ID across models Rails

I am trying to create an app that allows users to create and apply for jobs but seem to have hit a problem.
I can't get the job_id to pass into my apps (job applications) table in my database.
To get this app to work succesfully I need to pass the job_id and the user_id to the user's application form so that when they submit their job application this information is stored in my apps table. The job owner will then be able to review the applications they have received.
I have the following associations in my models:
class App < ActiveRecord::Base
belongs_to :job
belongs_to :user
class Job < ActiveRecord::Base
belongs_to :user
has_many :apps
has_many :applicants, :through => :apps, :source => :user
class User < ActiveRecord::Base
has_many :apps
has_many :jobs
has_many :jobs_applied_for, :through => :apps, :source => :job
Defined on my Jobs controller's show page (the page from which the user can click "apply now" to start an application) I have the following:
def show
#job = Job.find(params[:id])
end
The link to "apply now" on the actual page is:
<%=link_to "Apply Now", new_app_path %>
and on my Apps controller's new page I have:
def new
#user = current_user
#app = #user.apps.build
end
My user_id is passing perfectly and appearing in my apps table but I am totally stumped on how to pass the job_id correctly.
If I have missed anything that I can edit into this question to help you answer it then please do let me know.
Thanks in advance for your help!
You are not passing the job_id in your new_app_path link. Try changing it to new_app_path(:job_id => #job.id), and in your controller add #job = Job.find(params[:job_id])
Assuming your routes nest apps inside jobs, your link to the new application page should be something like
link_to 'apply now', new_app_job_path(#job)
In your new action you'll have a params[:job_id] that you can use to set #job
Then on the form, your call to form for should look like
form_for [#job, #app] do |f|
...
This ensures that your create action will also have a :job_id parameter that you can use when creating the application
The action that you should be invoking is create not new.
Change the link_to code as follows:
<%=link_to "Apply Now", apps_path(:app => {:job_id => #job.id}), :method => :post %>
In your controller:
def create
app = current_user.apps.build(params[:app])
if app.save
# handle success
else
# handle error
end
end

Resources