Merge pull request #2535 from rahul2240/booth_invitation

Invitation for booth responsibles
This commit is contained in:
Stella Rouzi 2019-07-21 22:23:34 +03:00 committed by GitHub
commit 770a63e15c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 273 additions and 8 deletions

View file

@ -37,6 +37,7 @@ gem 'rails-i18n'
# as authentification framework
gem 'devise'
gem 'devise_ichain_authenticatable'
gem 'devise_invitable'
# for openID authentication
gem 'omniauth'

View file

@ -161,6 +161,9 @@ GEM
warden (~> 1.2.3)
devise_ichain_authenticatable (0.3.2)
devise (>= 2.2)
devise_invitable (2.0.1)
actionmailer (>= 5.0)
devise (>= 4.6)
diff-lcs (1.3)
docile (1.3.1)
domain_name (0.5.20180417)
@ -646,6 +649,7 @@ DEPENDENCIES
delayed_job_active_record
devise
devise_ichain_authenticatable
devise_invitable
dotenv-rails
factory_bot_rails
faker

View file

@ -4,6 +4,7 @@ module Admin
class BoothsController < Admin::BaseController
load_and_authorize_resource :conference, find_by: :short_title
load_and_authorize_resource through: :conference
include Invitation
def index
@file_name = "booths_for_#{@conference.short_title}"
@ -38,6 +39,7 @@ module Admin
@booth.submitter = current_user
if @booth.save
booth_responsible_invite
redirect_to admin_conference_booths_path,
notice: 'Booth successfully created.'
else
@ -54,7 +56,7 @@ module Admin
@url = admin_conference_booth_path(@conference.short_title, @booth.id)
@booth.update_attributes(booth_params)
booth_responsible_invite
if @booth.save
redirect_to admin_conference_booths_path,
notice: "Successfully updated booth for #{@booth.title}."
@ -129,7 +131,8 @@ module Admin
def booth_params
params.require(:booth).permit(:title, :description, :reasoning, :state, :picture, :conference_id,
:created_at, :updated_at, :submitter_relationship, :website_url, responsible_ids: [])
:created_at, :updated_at, :submitter_relationship, :website_url, :invite_responsible,
responsible_ids: [])
end
end
end

View file

@ -9,6 +9,8 @@ class ApplicationController < ActionController::Base
# Ensure every controller authorizes resource or skips authorization (skip_authorization_check)
check_authorization unless: :devise_controller?
before_action :devise_configure_permitted_parameters, if: :devise_controller?
def store_location
# store last url - this is needed for post-login redirect to whatever the user last visited.
return unless request.get?
@ -65,4 +67,8 @@ class ApplicationController < ActionController::Base
def not_found
raise ActionController::RoutingError.new('Not Found')
end
def devise_configure_permitted_parameters
devise_parameter_sanitizer.permit(:accept_invitation, keys: [:username])
end
end

View file

@ -5,6 +5,7 @@ class BoothsController < ApplicationController
load_resource :conference, find_by: :short_title
load_and_authorize_resource through: :conference
skip_authorize_resource only: [:withdraw, :confirm, :restart]
include Invitation
def index
@booths = current_user.booths.where(conference_id: @conference.id).uniq
@ -22,6 +23,7 @@ class BoothsController < ApplicationController
@booth.submitter = current_user
if @booth.save
booth_responsible_invite
redirect_to conference_booths_path,
notice: 'Booth successfully created.'
else
@ -38,6 +40,7 @@ class BoothsController < ApplicationController
@url = conference_booth_path(@conference.short_title, @booth.id)
@booth.update_attributes(booth_params)
booth_responsible_invite
if @booth.save
redirect_to conference_booths_path,
notice: 'Booth successfully updated!'
@ -94,6 +97,7 @@ class BoothsController < ApplicationController
def booth_params
params.require(:booth).permit(:title, :description, :reasoning, :state, :picture, :conference_id,
:created_at, :updated_at, :submitter_relationship, :website_url, responsible_ids: [])
:created_at, :updated_at, :submitter_relationship, :website_url, :invite_responsible,
responsible_ids: [])
end
end

View file

@ -0,0 +1,21 @@
module Invitation
extend ActiveSupport::Concern
def booth_responsible_invite
if booth_params[:invite_responsible]
emails_array = booth_params[:invite_responsible].split(',')
emails_array.each do |email|
new_user = User.find_by(email: email)
if new_user.nil?
User.invite!({ email: email }, current_user)
new_user = User.find_by(email: email)
end
if new_user && @booth.responsible_ids.exclude?(new_user.id)
BoothRequest.create(booth_id: @booth.id, user_id: new_user.id,
role: 'responsible')
end
end
end
end
end

View file

@ -4,6 +4,8 @@ class Booth < ApplicationRecord
include ActiveRecord::Transitions
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
attr_accessor :invite_responsible
belongs_to :conference
has_many :booth_requests, dependent: :destroy
has_many :users, through: :booth_requests

View file

@ -49,8 +49,8 @@ class User < ApplicationRecord
[:ichain_authenticatable, :ichain_registerable, :omniauthable, omniauth_providers: []]
else
[:database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
:omniauthable, omniauth_providers: [:suse, :google, :facebook, :github]]
:recoverable, :rememberable, :trackable, :validatable, :invitable,
:confirmable, :omniauthable, omniauth_providers: [:suse, :google, :facebook, :github]]
end
devise(*devise_modules)

View file

@ -12,6 +12,8 @@
hint: 'e.g. employee, comunity manager, etc'
= f.input :website_url
= responsibles_selector_input f
= f.input :invite_responsible,
hint: 'This field is to invite unregistered users using their email to become booth responsibles.'
= image_tag f.object.picture.thumb.url if f.object.picture?
= f.input :picture
@ -27,4 +29,18 @@
plugins: ['remove_button'],
minItems: 2
} )
$('#booth_invite_responsible').selectize({
plugins: ['remove_button'],
delimiter: ',',
persist: false,
create: function(input) {
if (!/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(input)){
return {}
}
return {
value: input,
text: input
}
}
} )
});

View file

@ -0,0 +1,20 @@
.container
.row
.col-md-6.col-md-offset-3
.panel.panel-default
.panel-heading
%h3.panel-title
%h2= t 'devise.invitations.edit.header'
.panel-body
= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :put }) do |f|
= render 'devise/shared/error_messages', resource: resource
= f.hidden_field :invitation_token, readonly: true
- if f.object.class.require_password_on_accepting
= f.label :username
= f.text_field :username, class: 'form-control'
= f.label :password
= f.password_field :password, class: 'form-control'
= f.label :password_confirmation
= f.password_field :password_confirmation, class: 'form-control'
%br
= f.submit t('devise.invitations.edit.submit_button'), class: 'btn btn-default btn-primary'

View file

@ -0,0 +1,11 @@
<p><%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %></p>
<p><%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %></p>
<p><%= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, invitation_token: @token) %></p>
<% if @resource.invitation_due_at %>
<p><%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %></p>
<% end %>
<p><%= t("devise.mailer.invitation_instructions.ignore") %></p>

View file

@ -0,0 +1,11 @@
<%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>
<%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %>
<%= accept_invitation_url(@resource, invitation_token: @token) %>
<% if @resource.invitation_due_at %>
<%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>
<% end %>
<%= t("devise.mailer.invitation_instructions.ignore") %>

View file

@ -96,6 +96,55 @@ Devise.setup do |config|
# Setup a pepper to generate the encrypted password.
# config.pepper = "9a89de1df41b08b48af904bbbc5b8d713addd9d14e2758f12dd14105acd8627fececa02f33f89c0e9764e74262d8874c4a52534b788f5aa4b1716522ae6888b1"
# ==> Configuration for :invitable
# The period the generated invitation token is valid.
# After this period, the invited resource won't be able to accept the invitation.
# When invite_for is 0 (the default), the invitation won't expire.
# config.invite_for = 2.weeks
# Number of invitations users can send.
# - If invitation_limit is nil, there is no limit for invitations, users can
# send unlimited invitations, invitation_limit column is not used.
# - If invitation_limit is 0, users can't send invitations by default.
# - If invitation_limit n > 0, users can send n invitations.
# You can change invitation_limit column for some users so they can send more
# or less invitations, even with global invitation_limit = 0
# Default: nil
# config.invitation_limit = 5
# The key to be used to check existing users when sending an invitation
# and the regexp used to test it when validate_on_invite is not set.
# config.invite_key = { email: /\A[^@]+@[^@]+\z/ }
# config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil }
# Ensure that invited record is valid.
# The invitation won't be sent if this check fails.
# Default: false
# config.validate_on_invite = true
# Resend invitation if user with invited status is invited again
# Default: true
# config.resend_invitation = false
# The class name of the inviting model. If this is nil,
# the #invited_by association is declared to be polymorphic.
# Default: nil
# config.invited_by_class_name = 'User'
# The foreign key to the inviting model (if invited_by_class_name is set)
# Default: :invited_by_id
# config.invited_by_foreign_key = :invited_by_id
# The column name used for counter_cache column. If this is nil,
# the #invited_by association is declared without counter_cache.
# Default: nil
# config.invited_by_counter_cache = :invitations_count
# Auto-login after the user accepts the invite. If this is false,
# the user will need to manually log in after accepting the invite.
# Default: true
# config.allow_insecure_sign_in_after_accept = false
# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
# confirming his account. For instance, if set to 2.days, the user will be

View file

@ -0,0 +1,31 @@
en:
devise:
failure:
invited: "You have a pending invitation, accept it to finish creating your account."
invitations:
send_instructions: "An invitation email has been sent to %{email}."
invitation_token_invalid: "The invitation token provided is not valid!"
updated: "Your password was set successfully. You are now signed in."
updated_not_active: "Your password was set successfully."
no_invitations_remaining: "No invitations remaining"
invitation_removed: "Your invitation was removed."
new:
header: "Send invitation"
submit_button: "Send an invitation"
edit:
header: "Set your password"
submit_button: "Set my password"
mailer:
invitation_instructions:
subject: "Invitation instructions"
hello: "Hello %{email}"
someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below."
accept: "Accept invitation"
accept_until: "This invitation will be due in %{due_date}."
ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password."
time:
formats:
devise:
mailer:
invitation_instructions:
accept_until_format: "%B %d, %Y %I:%M %p"

View file

@ -0,0 +1,23 @@
class DeviseInvitableAddToUsers < ActiveRecord::Migration[5.1]
def up
change_table :users do |t|
t.string :invitation_token
t.datetime :invitation_created_at
t.datetime :invitation_sent_at
t.datetime :invitation_accepted_at
t.integer :invitation_limit
t.references :invited_by, polymorphic: true
t.integer :invitations_count, default: 0
t.index :invitations_count
t.index :invitation_token, unique: true # for invitable
t.index :invited_by_id
end
end
def down
change_table :users do |t|
t.remove_references :invited_by, polymorphic: true
t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at
end
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20181113195810) do
ActiveRecord::Schema.define(version: 2019_06_03_143107) do
create_table "answers", force: :cascade do |t|
t.string "title"
@ -587,8 +587,20 @@ ActiveRecord::Schema.define(version: 20181113195810) do
t.boolean "is_admin", default: false
t.string "username"
t.boolean "is_disabled", default: false
t.string "invitation_token"
t.datetime "invitation_created_at"
t.datetime "invitation_sent_at"
t.datetime "invitation_accepted_at"
t.integer "invitation_limit"
t.string "invited_by_type"
t.integer "invited_by_id"
t.integer "invitations_count", default: 0
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true
t.index ["invitations_count"], name: "index_users_on_invitations_count"
t.index ["invited_by_id"], name: "index_users_on_invited_by_id"
t.index ["invited_by_type", "invited_by_id"], name: "index_users_on_invited_by_type_and_invited_by_id"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["username"], name: "index_users_on_username", unique: true
end

View file

@ -77,6 +77,18 @@ describe Admin::BoothsController do
it 'shows success message' do
expect(flash[:notice]).to match('Booth successfully created.')
end
it 'creates a new user on inviting booth responsible' do
expect(User.where(email: 'user@example.com')).to exist
end
it 'does not create a user with invalid email on inviting booth responsible' do
expect(User.where(email: 'example')).not_to exist
end
it 'invited user should be a part of booth responsibles' do
expect(Booth.last.responsibles.ids).to include(User.find_by(email: 'user@example.com').id)
end
end
context 'create action fails' do
@ -113,7 +125,7 @@ describe Admin::BoothsController do
describe 'PATCH #update' do
context 'updates suchessfully' do
before { patch :update, params: { id: booth.id, booth: attributes_for(:booth, title: 'different'), conference_id: conference.short_title } }
before { patch :update, params: { id: booth.id, booth: attributes_for(:booth, title: 'different', invited_users: 'user@example.com, example'), conference_id: conference.short_title } }
it 'redirects to admin booth index path' do
expect(response).to redirect_to admin_conference_booths_path
end
@ -126,6 +138,19 @@ describe Admin::BoothsController do
booth.reload
expect(booth.title).to eq('different')
end
it 'creates a new user on inviting booth responsible' do
expect(User.where(email: 'user@example.com')).to exist
end
it 'does not create a user with invalid email on inviting booth responsible' do
expect(User.where(email: 'example')).not_to exist
end
it 'invited user should be a part of booth responsibles' do
booth.reload
expect(booth.responsibles.ids).to include(User.find_by(email: 'user@example.com').id)
end
end
end
end

View file

@ -57,6 +57,18 @@ describe BoothsController do
it 'shows success message' do
expect(flash[:notice]).to match('Booth successfully created.')
end
it 'creates a new user on inviting booth responsible' do
expect(User.where(email: 'user@example.com')).to exist
end
it 'does not create a user with invalid email on inviting booth responsible' do
expect(User.where(email: 'example')).not_to exist
end
it 'invited user should be a part of booth responsibles' do
expect(Booth.last.responsibles.ids).to include(User.find_by(email: 'user@example.com').id)
end
end
context 'create action fails' do
@ -93,7 +105,7 @@ describe BoothsController do
describe 'PATCH #update' do
context 'updates suchessfully' do
before { patch :update, params: { id: booth.id, booth: attributes_for(:booth, title: 'different'), conference_id: conference.short_title } }
before { patch :update, params: { id: booth.id, booth: attributes_for(:booth, title: 'different', invited_users: 'user@example.com, example'), conference_id: conference.short_title } }
it 'redirects to booth index path' do
expect(response).to redirect_to conference_booths_path
@ -107,6 +119,19 @@ describe BoothsController do
booth.reload
expect(booth.title).to eq('different')
end
it 'creates a new user on inviting booth responsible' do
expect(User.where(email: 'user@example.com')).to exist
end
it 'does not create a user with invalid email on inviting booth responsible' do
expect(User.where(email: 'example')).not_to exist
end
it 'invited user should be a part of booth responsibles' do
booth.reload
expect(booth.responsibles.ids).to include(User.find_by(email: 'user@example.com').id)
end
end
end
end

View file

@ -12,5 +12,6 @@ FactoryBot.define do
submitter { create(:user) }
responsible_ids { [create(:user).id] }
invite_responsible { 'user@example.com, example' }
end
end