Compare commits
7 Commits
b8e75c7c4a
...
022094ce51
| Author | SHA1 | Date | |
|---|---|---|---|
| 022094ce51 | |||
| 2a2b0a90dc | |||
| e44535daee | |||
| c8ccb418b2 | |||
| a792d66c90 | |||
| f7e48ad3a6 | |||
| 8a7d809b92 |
@ -25,4 +25,11 @@ form {
|
||||
.actions {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.accept-terms {
|
||||
margin-top: 2rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5em;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@ class InvitationsController < ApplicationController
|
||||
|
||||
# GET /invitations
|
||||
def index
|
||||
@invitations = current_user.invitations
|
||||
@invitations_unused = current_user.invitations.unused
|
||||
@invitations_used = current_user.invitations.used
|
||||
end
|
||||
|
||||
# GET /invitations/a-random-invitation-token
|
||||
|
||||
@ -104,7 +104,6 @@ class SignupController < ApplicationController
|
||||
password: @user.password
|
||||
)
|
||||
|
||||
@invitation.update_attributes invited_user_id: @user.id,
|
||||
used_at: DateTime.now
|
||||
@invitation.update! invited_user_id: @user.id, used_at: DateTime.now
|
||||
end
|
||||
end
|
||||
|
||||
15
app/models/concerns/email_validatable.rb
Normal file
15
app/models/concerns/email_validatable.rb
Normal file
@ -0,0 +1,15 @@
|
||||
require 'mail'
|
||||
|
||||
module EmailValidatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class EmailValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
begin
|
||||
a = Mail::Address.new(value)
|
||||
rescue Mail::Field::ParseError
|
||||
record.errors[attribute] << (options[:message] || "is not a valid address")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -2,10 +2,16 @@ class Invitation < ApplicationRecord
|
||||
# Relations
|
||||
belongs_to :user
|
||||
|
||||
# Validations
|
||||
validates_presence_of :user
|
||||
|
||||
# Hooks
|
||||
before_create :generate_token
|
||||
|
||||
# Scopes
|
||||
scope :unused, -> { where(used_at: nil) }
|
||||
scope :used, -> { where.not(used_at: nil) }
|
||||
|
||||
private
|
||||
|
||||
def generate_token
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
class User < ApplicationRecord
|
||||
include EmailValidatable
|
||||
|
||||
# Relations
|
||||
has_many :invitations, dependent: :destroy
|
||||
|
||||
validates_uniqueness_of :cn
|
||||
validates_uniqueness_of :email
|
||||
validates_length_of :cn, :minimum => 3
|
||||
validates_uniqueness_of :email
|
||||
validates :email, email: true
|
||||
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||
|
||||
@ -1,23 +1,46 @@
|
||||
<section>
|
||||
<h2>Invitations</h2>
|
||||
<% if @invitations_unused.any? %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @invitations_unused.each do |invitation| %>
|
||||
<tr>
|
||||
<td><%= invitation_url(invitation.token) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>
|
||||
You do not have any invitations to give away yet. All good
|
||||
things come in time.
|
||||
</p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<% if @invitations_used.any? %>
|
||||
<h3>Accepted Invitations</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Token</th>
|
||||
<th>Created at</th>
|
||||
<th colspan="3"></th>
|
||||
<th>URL</th>
|
||||
<th>Used at</th>
|
||||
<th>Invited user</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% @invitations.each do |invitation| %>
|
||||
<% @invitations_used.each do |invitation| %>
|
||||
<tr>
|
||||
<td><%= invitation.token %></td>
|
||||
<td><%= invitation.created_at %></td>
|
||||
<td><%= link_to 'Delete', invitation, method: :delete, data: { confirm: 'Are you sure?' } %></td>
|
||||
<td><%= invitation_url(invitation.token) %></td>
|
||||
<td><%= invitation.used_at %></td>
|
||||
<td><%= User.find(invitation.invited_user_id).address %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@ -48,8 +48,14 @@
|
||||
<p class="error-msg">Password <%= @validation_error %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="accept-terms">
|
||||
<small>
|
||||
By clicking the button below, you accept our future Terms of Service
|
||||
and Privacy Policy. Don't worry, they will be excellent!
|
||||
</small>
|
||||
</p>
|
||||
<div class="actions">
|
||||
<p><%= f.submit "Continue" %></p>
|
||||
<p><%= f.submit "Create account" %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@ -8,5 +8,7 @@ class CreateInvitations < ActiveRecord::Migration[6.0]
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :invitations, :user_id
|
||||
add_index :invitations, :invited_user_id
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
FactoryBot.define do
|
||||
factory :invitation do
|
||||
id { 1 }
|
||||
token { "abcdef123456" }
|
||||
user
|
||||
end
|
||||
|
||||
@ -3,37 +3,101 @@ require "rails_helper"
|
||||
RSpec.describe "Signup", type: :feature do
|
||||
let(:user) { create :user }
|
||||
|
||||
before do
|
||||
@unused_invitation = Invitation.create(user: user)
|
||||
@used_invitation = Invitation.create(user: user)
|
||||
@used_invitation.update_attribute :used_at, DateTime.now - 1.day
|
||||
end
|
||||
describe "Invitation handling" do
|
||||
before do
|
||||
@unused_invitation = Invitation.create(user: user)
|
||||
@used_invitation = Invitation.create(user: user)
|
||||
@used_invitation.update_attribute :used_at, DateTime.now - 1.day
|
||||
end
|
||||
|
||||
scenario "Follow link for non-existing invitation" do
|
||||
visit invitation_url(id: "123")
|
||||
scenario "Follow link for non-existing invitation" do
|
||||
visit invitation_url(id: "123")
|
||||
|
||||
within ".flash-msg.alert" do
|
||||
expect(page).to have_content("doesn't exist")
|
||||
within ".flash-msg.alert" do
|
||||
expect(page).to have_content("doesn't exist")
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Follow link for used invitation" do
|
||||
visit invitation_url(id: @used_invitation.token)
|
||||
|
||||
within ".flash-msg.alert" do
|
||||
expect(page).to have_content("has already been used")
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Follow link for unused invitation" do
|
||||
visit invitation_url(id: @unused_invitation.token)
|
||||
|
||||
expect(current_url).to eq(signup_url)
|
||||
expect(page).to have_content("Welcome")
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Follow link for used invitation" do
|
||||
visit invitation_url(id: @used_invitation.token)
|
||||
|
||||
within ".flash-msg.alert" do
|
||||
expect(page).to have_content("has already been used")
|
||||
describe "Signup steps" do
|
||||
before do
|
||||
@invitation = Invitation.create(user: user)
|
||||
visit invitation_url(id: @invitation.token)
|
||||
click_link "Get started"
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Follow link for unused invitation" do
|
||||
visit invitation_url(id: @unused_invitation.token)
|
||||
scenario "Successful signup (happy path galore)" do
|
||||
expect(page).to have_content("Choose a username")
|
||||
|
||||
expect(current_url).to eq(signup_url)
|
||||
expect(page).to have_content("Welcome")
|
||||
end
|
||||
fill_in "user_cn", with: "tony"
|
||||
click_button "Continue"
|
||||
expect(page).to have_content("What's your email?")
|
||||
|
||||
scenario "Successful signup" do
|
||||
visit invitation_url(id: @unused_invitation.token)
|
||||
click_link "Get started"
|
||||
fill_in "user_email", with: "tony@example.com"
|
||||
click_button "Continue"
|
||||
expect(page).to have_content("Choose a password")
|
||||
|
||||
expect(CreateAccount).to receive(:call)
|
||||
.with(username: "tony", email: "tony@example.com", password: "a-valid-password")
|
||||
.and_return(true)
|
||||
|
||||
fill_in "user_password", with: "a-valid-password"
|
||||
click_button "Create account"
|
||||
within ".flash-msg.notice" do
|
||||
expect(page).to have_content("confirm your address")
|
||||
end
|
||||
expect(page).to have_content("close this window or tab now")
|
||||
expect(User.last.confirmed_at).to be_nil
|
||||
end
|
||||
|
||||
scenario "Validation errors" do
|
||||
fill_in "user_cn", with: "t"
|
||||
click_button "Continue"
|
||||
expect(page).to have_content("Username is too short")
|
||||
fill_in "user_cn", with: "jimmy"
|
||||
click_button "Continue"
|
||||
expect(page).to have_content("Username has already been taken")
|
||||
fill_in "user_cn", with: "tony"
|
||||
click_button "Continue"
|
||||
|
||||
fill_in "user_email", with: "tony@"
|
||||
click_button "Continue"
|
||||
expect(page).to have_content("Email is not a valid address")
|
||||
fill_in "user_email", with: ""
|
||||
click_button "Continue"
|
||||
expect(page).to have_content("Email can't be blank")
|
||||
fill_in "user_email", with: "tony@example.com"
|
||||
click_button "Continue"
|
||||
|
||||
fill_in "user_password", with: "123456"
|
||||
click_button "Create account"
|
||||
expect(page).to have_content("Password is too short")
|
||||
|
||||
expect(CreateAccount).to receive(:call)
|
||||
.with(username: "tony", email: "tony@example.com", password: "a-valid-password")
|
||||
.and_return(true)
|
||||
|
||||
fill_in "user_password", with: "a-valid-password"
|
||||
click_button "Create account"
|
||||
within ".flash-msg.notice" do
|
||||
expect(page).to have_content("confirm your address")
|
||||
end
|
||||
expect(User.last.cn).to eq("tony")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -19,4 +19,26 @@ RSpec.describe Invitation, type: :model do
|
||||
expect(token).not_to eq(invitation_2.token)
|
||||
end
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
before do
|
||||
@unused_invitation = create :invitation, user: user
|
||||
@used_invitation = create :invitation, user: user, used_at: DateTime.now
|
||||
@used_invitation_2 = create :invitation, user: user, used_at: DateTime.now
|
||||
end
|
||||
|
||||
describe "#unused" do
|
||||
it "returns unused invitations" do
|
||||
expect(Invitation.unused.count).to eq(1)
|
||||
expect(Invitation.unused.first).to eq(@unused_invitation)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#used" do
|
||||
it "returns used invitations" do
|
||||
expect(Invitation.used.count).to eq(2)
|
||||
expect(Invitation.used.first).to eq(@used_invitation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user