From 58c829571e07fb86c7be928a65d94e859666516d Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 3 Jun 2019 19:50:48 +0530 Subject: [PATCH 1/4] Add invite responsible to form It is added so that user can enter email ids of other users, who they want to invite to become booth responsible. --- app/controllers/admin/booths_controller.rb | 3 ++- app/controllers/booths_controller.rb | 3 ++- app/models/booth.rb | 2 ++ app/views/booths/_form.html.haml | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/booths_controller.rb b/app/controllers/admin/booths_controller.rb index aceceee0..4082e69f 100644 --- a/app/controllers/admin/booths_controller.rb +++ b/app/controllers/admin/booths_controller.rb @@ -129,7 +129,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 diff --git a/app/controllers/booths_controller.rb b/app/controllers/booths_controller.rb index df535459..cb14c9bd 100644 --- a/app/controllers/booths_controller.rb +++ b/app/controllers/booths_controller.rb @@ -94,6 +94,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 diff --git a/app/models/booth.rb b/app/models/booth.rb index 85c6c7a8..71ab6e98 100644 --- a/app/models/booth.rb +++ b/app/models/booth.rb @@ -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 diff --git a/app/views/booths/_form.html.haml b/app/views/booths/_form.html.haml index 4f5b6018..dfce22b1 100644 --- a/app/views/booths/_form.html.haml +++ b/app/views/booths/_form.html.haml @@ -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 From a20c1bb882d0736ea11b425f904912dca95f5d8d Mon Sep 17 00:00:00 2001 From: Rahul Date: Sat, 20 Jul 2019 14:28:20 +0530 Subject: [PATCH 2/4] Add selectize to invite responsibles email validation and input as tags is added using selectize --- app/views/booths/_form.html.haml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/views/booths/_form.html.haml b/app/views/booths/_form.html.haml index dfce22b1..ab9080d3 100644 --- a/app/views/booths/_form.html.haml +++ b/app/views/booths/_form.html.haml @@ -29,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 + } + } + } ) }); From 04c3859526070e99e0c36081e0861efa5301526c Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 3 Jun 2019 20:03:13 +0530 Subject: [PATCH 3/4] Install devise invitable Devise invitable gem is required to invite users. --- Gemfile | 1 + Gemfile.lock | 4 ++ app/controllers/application_controller.rb | 6 +++ app/models/user.rb | 4 +- app/views/devise/invitations/edit.html.haml | 20 ++++++++ .../mailer/invitation_instructions.html.erb | 11 +++++ .../mailer/invitation_instructions.text.erb | 11 +++++ config/initializers/devise.rb | 49 +++++++++++++++++++ config/locales/devise_invitable.en.yml | 31 ++++++++++++ ...603143107_devise_invitable_add_to_users.rb | 23 +++++++++ db/schema.rb | 14 +++++- 11 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 app/views/devise/invitations/edit.html.haml create mode 100644 app/views/devise/mailer/invitation_instructions.html.erb create mode 100644 app/views/devise/mailer/invitation_instructions.text.erb create mode 100644 config/locales/devise_invitable.en.yml create mode 100644 db/migrate/20190603143107_devise_invitable_add_to_users.rb diff --git a/Gemfile b/Gemfile index b281e1aa..55e72eb6 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,7 @@ gem 'rails-i18n' # as authentification framework gem 'devise' gem 'devise_ichain_authenticatable' +gem 'devise_invitable' # for openID authentication gem 'omniauth' diff --git a/Gemfile.lock b/Gemfile.lock index 10f0c9bf..36198fa8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c15681aa..02679891 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index ec3132e1..1ff0e96c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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) diff --git a/app/views/devise/invitations/edit.html.haml b/app/views/devise/invitations/edit.html.haml new file mode 100644 index 00000000..5f0ae4af --- /dev/null +++ b/app/views/devise/invitations/edit.html.haml @@ -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' diff --git a/app/views/devise/mailer/invitation_instructions.html.erb b/app/views/devise/mailer/invitation_instructions.html.erb new file mode 100644 index 00000000..27a1c0c4 --- /dev/null +++ b/app/views/devise/mailer/invitation_instructions.html.erb @@ -0,0 +1,11 @@ +

<%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>

+ +

<%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %>

+ +

<%= link_to t("devise.mailer.invitation_instructions.accept"), 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") %>

diff --git a/app/views/devise/mailer/invitation_instructions.text.erb b/app/views/devise/mailer/invitation_instructions.text.erb new file mode 100644 index 00000000..f4912bf4 --- /dev/null +++ b/app/views/devise/mailer/invitation_instructions.text.erb @@ -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") %> diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a533ffe2..6d9f2dc9 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -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 diff --git a/config/locales/devise_invitable.en.yml b/config/locales/devise_invitable.en.yml new file mode 100644 index 00000000..f6bfee40 --- /dev/null +++ b/config/locales/devise_invitable.en.yml @@ -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" diff --git a/db/migrate/20190603143107_devise_invitable_add_to_users.rb b/db/migrate/20190603143107_devise_invitable_add_to_users.rb new file mode 100644 index 00000000..7126cb0e --- /dev/null +++ b/db/migrate/20190603143107_devise_invitable_add_to_users.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 2cb9ac88..b199367e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 From 29c15c5d937e81d5797741ddc3a727352de8691c Mon Sep 17 00:00:00 2001 From: Rahul Date: Tue, 4 Jun 2019 18:15:41 +0530 Subject: [PATCH 4/4] Booth invitation to invited users User will enter email of users to invite to become booth responsible, which will create a user in User db with that email and a new booth request will be created. --- app/controllers/admin/booths_controller.rb | 4 ++- app/controllers/booths_controller.rb | 3 +++ app/controllers/concerns/invitation.rb | 21 +++++++++++++++ .../admin/booths_controller_spec.rb | 27 ++++++++++++++++++- spec/controllers/booths_controller_spec.rb | 27 ++++++++++++++++++- spec/factories/booths.rb | 1 + 6 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 app/controllers/concerns/invitation.rb diff --git a/app/controllers/admin/booths_controller.rb b/app/controllers/admin/booths_controller.rb index 4082e69f..045d24a7 100644 --- a/app/controllers/admin/booths_controller.rb +++ b/app/controllers/admin/booths_controller.rb @@ -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}." diff --git a/app/controllers/booths_controller.rb b/app/controllers/booths_controller.rb index cb14c9bd..4600ab61 100644 --- a/app/controllers/booths_controller.rb +++ b/app/controllers/booths_controller.rb @@ -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!' diff --git a/app/controllers/concerns/invitation.rb b/app/controllers/concerns/invitation.rb new file mode 100644 index 00000000..2d1d84c1 --- /dev/null +++ b/app/controllers/concerns/invitation.rb @@ -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 diff --git a/spec/controllers/admin/booths_controller_spec.rb b/spec/controllers/admin/booths_controller_spec.rb index 11b578a4..853fed4c 100644 --- a/spec/controllers/admin/booths_controller_spec.rb +++ b/spec/controllers/admin/booths_controller_spec.rb @@ -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 diff --git a/spec/controllers/booths_controller_spec.rb b/spec/controllers/booths_controller_spec.rb index 8444be9c..f49d2fd9 100644 --- a/spec/controllers/booths_controller_spec.rb +++ b/spec/controllers/booths_controller_spec.rb @@ -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 diff --git a/spec/factories/booths.rb b/spec/factories/booths.rb index fa7e9824..4de471c5 100644 --- a/spec/factories/booths.rb +++ b/spec/factories/booths.rb @@ -12,5 +12,6 @@ FactoryBot.define do submitter { create(:user) } responsible_ids { [create(:user).id] } + invite_responsible { 'user@example.com, example' } end end