Add table invites

This commit is contained in:
Trevor Vallender 2024-05-29 15:24:18 +01:00
parent e1c74ac2f5
commit 18c264b64b
15 changed files with 294 additions and 4 deletions

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
class TableInvitesController < ApplicationController
before_action :set_table, only: [ :new, :create ]
before_action :set_table_invite, only: [ :edit, :update ]
def new
@table_invite = @table.table_invites.new
end
def create
@user = User.find_by(email: table_invite_params[:email])
if @user.blank?
# TODO: Allow inviting non-users, we can send an email invite
flash[:alert] = t(".user_not_found")
render :new, status: :unprocessable_entity and return
end
@table_invite = @table.table_invites.new(table_invite_params)
if @table_invite.save
redirect_to @table, notice: t(".success", email: @table_invite.email)
else
render :new, status: :unprocessable_entity, alert: t(".error")
end
end
def edit
redirect_to @table_invite.table if @table_invite.responded?
end
def update
if params[:accept] == "true"
@table_invite.accept!
redirect_to @table_invite.table, notice: t(".accept_success")
elsif params[:decline] == "true"
@table_invite.decline!
redirect_to :root, notice: t(".decline_success")
end
end
private
def set_table
@table = Current.user.owned_tables.find(params[:table_id])
end
def set_table_invite
@table_invite = TableInvite.where(email: Current.user.email)
.find(params[:id])
end
def table_invite_params
params.require(:table_invite).permit(:email)
end
end

View File

@ -4,6 +4,7 @@ class Table < ApplicationRecord
belongs_to :owner, class_name: "User"
belongs_to :game_system
has_many :players, dependent: :destroy
has_many :table_invites, dependent: :destroy
has_many :users, through: :players
validates :name, presence: true,

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class TableInvite < ApplicationRecord
belongs_to :table
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP },
presence: true,
length: { maximum: 100 }
def accepted?
accepted_at.present?
end
def declined?
declined_at.present?
end
def responded?
accepted? || declined?
end
def accept!
raise "Already declined" if declined?
user = User.find_by(email:)
user.tables << table
user.save
update(accepted_at: Time.zone.now)
end
def decline!
raise "Already accepted" if accepted?
update(declined_at: Time.zone.now)
end
end

View File

@ -0,0 +1,10 @@
<% content_for :title, t(".respond_to_invite") %>
<h2><%= t(".respond_to_invite") %></h2>
<p><%= t(".accept_invite_description", table_name: @table_invite.table.name) %></p>
<%= link_to t(".accept_invite"), table_invite_path(@table_invite, accept: true), data: {
turbo_method: :patch } %>
<%= link_to t(".decline_invite"), table_invite_path(@table_invite, decline: true), data: {
turbo_method: :patch } %>

View File

@ -0,0 +1,11 @@
<% content_for :title, t(".new_table_invite", name: @table.name) %>
<h2><%= t(".new_table_invite", name: @table.name) %></h2>
<p><%= t(".new_invite_description") %></p>
<%= form_with url: table_table_invites_path do |f| %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.submit t(".button_text") %>
<% end %>

View File

@ -2,6 +2,7 @@
<h2><%= @table.name %></h2>
<%= link_to t(".invite_user"), new_table_table_invite_path(@table) %>
<%= link_to t(".edit_table"), edit_table_path(@table) %>
<dl>

View File

@ -60,6 +60,24 @@ en:
destroy:
log_out: Log out
success: "You have signed out."
table_invites:
new:
new_table_invite: Invite a player to %{name}
new_invite_description: Enter an email address to invite a player to your table.
create_table_invite: Send invite
button_text: Send invite
create:
user_not_found: There is no registered user with that email address. Ask them to sign up!
success: Send invite to %{email}
error: Failed to send invite
edit:
respond_to_invite: Respond to your invitation
accept_invite_description: You have been invited to join the table %{table_name}. To start playing, hit accept!
accept_invite: Accept
decline_invite: Decline
update:
accept_success: You accepted your invite
decline_success: You declined your invite
tables:
index:
new_table: Create a table
@ -68,6 +86,7 @@ en:
edit_table: Edit table
game_system: Game system
owner: Owner
invite_user: Invite a new player
new:
new_table: New table
create_table: Create table

View File

@ -12,7 +12,10 @@ Rails.application.routes.draw do
resources :account_verifications, only: [ :show ]
resources :sessions, only: [ :new, :create, :destroy ]
resources :tables
resources :table_invites, only: [ :edit, :update ]
resources :tables do
resources :table_invites, only: [ :new, :create ]
end
resources :admin, only: [ :index ]
namespace :admin do

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class CreateTableInvites < ActiveRecord::Migration[7.1]
def change
create_table :table_invites do |t|
t.belongs_to :table, null: false, foreign_key: true
t.string :email, null: false
t.datetime :accepted_at
t.datetime :declined_at
t.timestamps
end
add_check_constraint :table_invites, "length(email) >= 5", name: "chk_email_min_length"
add_check_constraint :table_invites, "length(email) <= 100", name: "chk_email_max_length"
end
end

15
db/schema.rb generated
View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_05_29_074949) do
ActiveRecord::Schema[7.1].define(version: 2024_05_29_122012) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -44,6 +44,18 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_29_074949) do
t.index ["user_id"], name: "index_site_roles_users_on_user_id"
end
create_table "table_invites", force: :cascade do |t|
t.bigint "table_id", null: false
t.string "email", null: false
t.datetime "accepted_at"
t.datetime "declined_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["table_id"], name: "index_table_invites_on_table_id"
t.check_constraint "length(email::text) <= 100", name: "chk_email_max_length"
t.check_constraint "length(email::text) >= 5", name: "chk_email_min_length"
end
create_table "tables", force: :cascade do |t|
t.string "name", null: false
t.string "slug", null: false
@ -78,6 +90,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_29_074949) do
add_foreign_key "players", "tables"
add_foreign_key "players", "users"
add_foreign_key "table_invites", "tables"
add_foreign_key "tables", "game_systems"
add_foreign_key "tables", "users", column: "owner_id"
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require "test_helper"
class TableInviteInvitesControllerTest < ActionDispatch::IntegrationTest
test "only a table owner can invite players" do
sign_in users(:trevor)
get new_table_table_invite_url(tables(:gimlis_table))
assert_response :not_found
end
test "should get new" do
sign_in users(:trevor)
get new_table_table_invite_url(tables(:table))
assert_response :success
end
test "should get edit" do
sign_in users(:frodo)
get edit_table_invite_url(table_invites(:unaccepted))
assert_response :success
end
test "should create table_invite" do
sign_in users(:trevor)
assert_changes("TableInvite.count", +1) do
post(table_table_invites_url(tables(:table)), params: { table_invite: { email: "uninvited_user@example.com" } })
end
assert_redirected_to table_path(tables(:table))
end
test "should accept table_invite" do
sign_in users(:frodo)
invite = table_invites(:unaccepted)
assert_nil invite.accepted_at
patch table_invite_url(invite), params: { accept: true }
assert_redirected_to table_path(invite.table)
invite.reload
assert_not_nil invite.accepted_at
end
test "should decline table_invite" do
sign_in users(:frodo)
invite = table_invites(:unaccepted)
assert_nil invite.accepted_at
patch table_invite_url(invite), params: { decline: true }
assert_redirected_to root_path
invite.reload
assert_not_nil invite.declined_at
end
end

20
test/fixtures/table_invites.yml vendored Normal file
View File

@ -0,0 +1,20 @@
one:
table: table
email: trevor@example.com
accepted_at: 2024-05-29 13:20:12
two:
table: table
email: gimli@example.com
accepted_at: 2024-05-29 13:20:12
unaccepted:
table: table
email: frodo@example.com
accepted_at: nil
declined:
table: table
email: admin@example.com
accepted_at: nil
declined_at: 2024-05-29 13:20:12

View File

@ -2,13 +2,17 @@ DEFAULTS: &DEFAULTS
owner: trevor
uuid: <%= SecureRandom.uuid %>
slug: $LABEL
game_system: troika
table:
<<: *DEFAULTS
name: My Table
game_system: troika
my_table_two:
<<: *DEFAULTS
name: My Other Table
game_system: troika
gimlis_table:
<<: *DEFAULTS
name: Gimli's Table
owner: gimli

View File

@ -15,6 +15,16 @@ gimli:
first_name: Gimli
last_name: son of Glóin
frodo:
<<: *DEFAULTS
first_name: Frodo
last_name: Baggins
uninvited_user:
<<: *DEFAULTS
first_name: Fitzchivalry
last_name: Farseer
unverified:
<<: *DEFAULTS
first_name: Unverified

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require "test_helper"
class TableInviteTest < ActiveSupport::TestCase
test "accept! accepts the invite" do
invite = table_invites(:unaccepted)
user = User.find_by(email: invite.email)
assert_not_includes user.tables, invite.table
assert_nil invite.accepted_at
invite.accept!
assert_not_nil invite.accepted_at
assert_includes user.tables, invite.table
end
test "decline! declines the invite" do
invite = table_invites(:unaccepted)
assert_nil invite.declined_at
invite.decline!
assert_not_nil invite.declined_at
end
test "cannot accept a declined invitation" do
assert_raises do
table_invites(:declined).accept!
end
end
test "cannot decline an accepted invitation" do
assert_raises do
table_invites(:one).decline!
end
end
test "responded? is true for both accepted and declined invites" do
assert table_invites(:one).responded?
assert table_invites(:declined).responded?
end
end