A simple MVC group invite feature in rails.
Published: 2021-01-07 05:34:14

#First, a quick introduction

I struggle with these blogs. For those not following along at home, I'm doing a development bootcamp; one of the requirements is to do a technical blog after each milestone project. It's a struggle for me because I've been coding in earnest for all of 3 months now; what could I have to say that anyone would, could, or should care about?

I reached out to some of my friends in the program and asked them to have a look at my project, OpenStage (be patient if it takes a moment to spin up, it's hosted on heroku) and tell me if there was anything they wanted to know about. My friend Bella said she'd like to know more about how I managed the invitations to bands, so that's what this blog will be about. At least I know there's one person who will think it's interesting.

#Invites?

Yeah, so my app has a few goals.

* Help musicians form bands
* Help bands find open stage time to play casual gigs
* Help venues draw traffic by having bands play

To have bands, which are made of multiple Muscians (users of class User in my models), you need to be have a join table. There are a number of questions to be asked about how to handle who has privileges in a band, but for this demonstration I decided the user that created the band (the band leader) would have the unilateral power to request gigs and invite members. We'll need a join table so that users can have many bands and bands can have many users, and we've called this join table band_members.

```
class BandMember < ApplicationRecord

belongs_to :user
belongs_to :band

validates_uniqueness_of :user_id, scope: :band_id
end

class User < ApplicationRecord

has_secure_password

has_many :lead_bands, class_name: "Band", foreign_key: :leader_id
has_many :band_members
has_many :bands, through: :band_members
has_many :gigs, through: :bands

validates :name, presence: true 
validates :email, presence: true 
validates :email, uniqueness: true
end

class Band < ApplicationRecord

belongs_to :leader, class_name: "User", foreign_key: :leader_id
has_many :band_members
has_many :users, through: :band_members
has_many :gigs

validates :name, presence: true

def empty_roster
    self.band_members.each do |band_member|
        band_member.delete
    end
end

def unclaim_all_gigs
    self.gigs.each do |gig|
        gig.band_id = nil
        gig.status = nil
        gig.save
    end
end

def clear_associations_before_deletion
    self.empty_roster
    self.unclaim_all_gigs
end
end
```

# Wait, I thought you were going to talk about your invite model?

There's no invite model. Let me explain.

```
  create_table "band_members", force: :cascade do |t|
    t.integer "band_id"
    t.integer "user_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "status"
  end
```

There's no invite model, because a band_member entry with status= "Pending" is an invite. 

```
class UsersController < ApplicationController

...

  def invite_member
    user = User.find(params[:user_id])
    invite = BandMember.new(user_id: params[:user_id], band_id: params[:band_id], status: "pending")
    if invite.save
      redirect_to user, notice: "Invite sent."
    else
      redirect_to user, notice: "Either something went wrong, or this user is already in that band. Please try again later."
    end
  end

  def invites
  end

  def accept_invite
    invite = BandMember.find(params[:id])
    invite.status = "Accepted"
    if invite.save
      redirect_to invite.band, notice: "You've joined #{invite.band.name}."
    else
      redirect_to invites_path, notice: "An error occured, please try again later."
    end
  end

 ...

end
```
"Invites" are just BandMember records with status "Pending", which, when initially created, all BandMember records are (with the exception of the record that is created for the band leader, which happens when the band is created). A user has to post to the accept invite path to change the status of the record to "Accepted"

Users interact with these controller actions from the views:

```
views/users/show.html.erb
...
<% if current_user_is_band_leader? && current_user.id != @user.id %>
    <div class='invite-wrapper'>
        <%= form_with(url: 'bands/band_members/new', method: 'post') do |form| %>
            <%= form.hidden_field :user_id, value: @user.id %>
            <%= form.label "Invite to:" %> 
            <br>
            <%= form.collection_select(:band_id, current_user.lead_bands, :id, :name) %>
            <br>
            <%= form.submit "Invite", class: 'button' %>
        <% end %>
    </div>
<% end %>
...
```

When viewing a musicians show page, if the viewing user is a band leader, they get a form that's just a drop-down with the bands they lead and a button that says "Invite".

![A musician's show page, viewed as a band leader.](https://dev-to-uploads.s3.amazonaws.com/i/vbagvoud3acyxllsmit6.png)

The user then has an "invite" so when they view the invites path, they see this:

![A user's invite page with a pending invite.](https://dev-to-uploads.s3.amazonaws.com/i/ec2fz9hqkdawt4q0ky8q.png)

If they accept, the form is submitted to the accept invite path, and the entry for the associated BandMember record is updated, and those updates are persisted.

The user is redirected to the page for the band that they've joined, and a button to leave the band is now present, so that they have the ability to un-join, as it where.

![A logged in user's view of a show page for a band which they are a member of.](https://dev-to-uploads.s3.amazonaws.com/i/v0it2bmvx3o7tkhpyped.png)

And that is, more or less, the life-cycle of an invite in this model. A user can't decline an invite, they can only let it sit unaccepted, because frankly figuring out how to build a protection against invite-again-on-rejection-abuse seemed like a big hassle to include in the MVP.