diff --git a/app/assets/javascripts/sitewide/invite_users_modal.js.erb b/app/assets/javascripts/sitewide/invite_users_modal.js.erb
new file mode 100644
index 000000000..eb9b5b5ff
--- /dev/null
+++ b/app/assets/javascripts/sitewide/invite_users_modal.js.erb
@@ -0,0 +1,189 @@
+(function() {
+ 'use strict';
+
+ function initializeModal(modal) {
+ var modalDialog = modal.find('.modal-dialog');
+ var type = modal.attr('data-type');
+ var stepForm = modal.find('[data-role=step-form]');
+ var stepResults = modal.find('[data-role=step-results]');
+ var stepResultsDiv = modal.find('[data-role=step-results][data-clear]');
+ var inviteBtn = modal.find('[data-role=invite-btn]');
+ var inviteWithRoleDiv =
+ modal.find('[data-role=invite-with-role-div]');
+ var inviteWithRoleBtn =
+ modal.find('[data-role=invite-with-role-btn]');
+ var orgSelectorCheckbox =
+ modal.find('[data-role=org-selector-checkbox]');
+ var orgSelectorDropdown =
+ modal.find('[data-role=org-selector-dropdown]');
+ var orgSelectorDropdown2 = $();
+ var tagsInput = modal.find('[data-role=tags-input]');
+
+ // Set max tags
+ tagsInput.tagsinput({
+ maxTags: <%= Constants::INVITE_USERS_LIMIT %>
+ });
+
+ modal
+ .on('show.bs.modal', function() {
+ // This cannot be scoped outside this function
+ // because it is generated via JS
+ orgSelectorDropdown2 =
+ orgSelectorDropdown
+ .next('.btn-group.bootstrap-select.form-control')
+ .find('button.dropdown-toggle, li');
+
+ // Show/hide correct step
+ stepForm.show();
+ stepResults.hide();
+
+ // Show/hide buttons & other elements
+ switch (type) {
+ case 'invite_to_org_with_role':
+ case 'invite':
+ case 'invite_with_org_selector':
+ case 'invite_with_org_selector_and_role':
+ inviteBtn.show();
+ inviteWithRoleDiv.hide();
+ break;
+ case 'invite_to_org':
+ inviteBtn.hide();
+ inviteWithRoleDiv.show();
+ break;
+ default:
+ break;
+ }
+
+ // Checkbox toggle event
+ if (
+ type === 'invite_with_org_selector' ||
+ type === 'invite_with_org_selector_and_role'
+ ) {
+ orgSelectorCheckbox.on('change', function() {
+ if ($(this).is(':checked')) {
+ orgSelectorDropdown.removeAttr('disabled');
+ orgSelectorDropdown2.removeClass('disabled');
+ if (type === 'invite_with_org_selector') {
+ inviteBtn.hide();
+ inviteWithRoleDiv.show();
+ }
+ } else {
+ orgSelectorDropdown.attr('disabled', 'disabled');
+ orgSelectorDropdown2.addClass('disabled');
+ if (type === 'invite_with_org_selector') {
+ inviteBtn.show();
+ inviteWithRoleDiv.hide();
+ }
+ }
+ });
+ }
+
+ // Toggle depending on input tags
+ tagsInput
+ .on('itemAdded', function(event) {
+ inviteBtn.removeAttr('disabled');
+ inviteWithRoleBtn.removeAttr('disabled');
+ })
+ .on('itemRemoved', function(event) {
+ if ($(this).val() === null) {
+ inviteBtn.attr('disabled', 'disabled');
+ inviteWithRoleBtn.attr('disabled', 'disabled');
+ }
+ });
+
+ // Click action
+ modal.find('[data-action=invite]').on('click', function() {
+ animateSpinner(modalDialog);
+
+ var data = {
+ emails: tagsInput.val()
+ };
+ switch (type) {
+ case 'invite_to_org':
+ data.organizationId = modal.attr('data-organization-id');
+ data.role = $(this).attr('data-organization-role');
+ break;
+ case 'invite_to_org_with_role':
+ data.organizationId = modal.attr('data-organization-id');
+ data.role = modal.attr('data-organization-role');
+ break;
+ case 'invite':
+ break;
+ case 'invite_with_org_selector':
+ if (orgSelectorCheckbox.is(':checked')) {
+ data.organizationId = orgSelectorDropdown.val();
+ data.role = $(this).attr('data-organization-role');
+ }
+ break;
+ case 'invite_with_org_selector_and_role':
+ if (orgSelectorCheckbox.is(':checked')) {
+ data.organizationId = orgSelectorDropdown.val();
+ data.role = modal.attr('data-organization-role');
+ }
+ break;
+ default:
+ break;
+ }
+
+ $.ajax({
+ method: 'POST',
+ url: modal.attr('data-url'),
+ dataType: 'json',
+ data: data,
+ success: function(data) {
+ animateSpinner(modalDialog, false);
+ stepForm.hide();
+ stepResultsDiv.html(data.html);
+ stepResults.show();
+ // Add 'data-invited="true"' status to modal element
+ modal.attr('data-invited', 'true');
+ },
+ error: function() {
+ animateSpinner(modalDialog, false);
+ modal.modal('hide');
+ alert('Error inviting users.');
+ }
+ });
+ });
+ })
+ .on('shown.bs.modal', function() {
+ tagsInput.tagsinput('focus');
+
+ // Remove 'data-invited="true"' status
+ modal.removeAttr('data-invited');
+ })
+ .on('hide.bs.modal', function() {
+ // 'Reset' modal state
+ tagsInput.tagsinput('removeAll');
+ orgSelectorCheckbox.prop('checked', false);
+ inviteBtn.attr('disabled', 'disabled');
+ inviteWithRoleBtn.attr('disabled', 'disabled');
+ orgSelectorDropdown2.addClass('disabled');
+ animateSpinner(modalDialog, false);
+
+ // Unbind event listeners
+ orgSelectorCheckbox.off('change');
+ tagsInput.off('itemAdded itemRemoved');
+
+ // Hide contents of the results
+ stepResultsDiv.html('');
+ stepResults.hide();
+ stepForm.show();
+ });
+ }
+
+ function initializeModalsToggle() {
+ $("[data-trigger='invite-users']").on('click', function() {
+ var id = $(this).attr('data-modal-id');
+ $('[data-role=invite-users-modal][data-id=' + id + ']')
+ .modal('show');
+ });
+ }
+
+ $(document).ready(function() {
+ $('[data-role=invite-users-modal]').each(function() {
+ initializeModal($(this));
+ });
+ initializeModalsToggle();
+ });
+})();
diff --git a/app/assets/javascripts/users/settings/organization.js b/app/assets/javascripts/users/settings/organization.js
index 8c490b43e..7f1f69908 100644
--- a/app/assets/javascripts/users/settings/organization.js
+++ b/app/assets/javascripts/users/settings/organization.js
@@ -1,5 +1,4 @@
//= require datatables
-//= require users/settings/organizations/add_user_modal
var usersDatatable = null;
@@ -236,8 +235,19 @@ function initRemoveUsers() {
);
}
+function initReloadPageAfterInviteUsers() {
+ $('[data-id=org-invite-users-modal]')
+ .on('hidden.bs.modal', function() {
+ if (!_.isUndefined($(this).attr('data-invited'))) {
+ // Reload the whole table
+ usersDatatable.ajax.reload();
+ }
+ });
+}
+
initEditName();
initEditDescription();
initUsersTable();
initUpdateRoles();
-initRemoveUsers();
\ No newline at end of file
+initRemoveUsers();
+initReloadPageAfterInviteUsers();
diff --git a/app/assets/javascripts/users/settings/organizations/add_user_modal.js b/app/assets/javascripts/users/settings/organizations/add_user_modal.js
deleted file mode 100644
index dcdfbab68..000000000
--- a/app/assets/javascripts/users/settings/organizations/add_user_modal.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/* Global selectors */
-var modal = $("#add-user-modal");
-var modalContent = modal.find(".modal-content");
-var invitingExisting = true;
-var inviteButton = $("[data-id='invite-btn']");
-var inviteLinks = $("[data-action='invite']");
-var inviteExistingCollapsible = $("#invite-existing");
-var inviteExistingForm = $("[data-id='invite-existing-form']");
-var inviteExistingQuery = $("#existing_query");
-var inviteExistingResults = $("#invite-existing-results");
-var inviteNewCollapsible = $("#invite-new");
-var inviteNewForm = $("[data-id='invite-new-form']");
-var inviteNewRoleInput = $("[data-id='new-user-role-input']");
-var inviteNewNameInput = $("[data-id='invite-new-name-input']");
-var inviteNewEmailInput = $("[data-id='invite-new-email-input']");
-
-function disableInviteBtn() {
- inviteButton.attr("disabled", "disabled");
-}
-function enableInviteBtn() {
- inviteButton.removeAttr("disabled");
-}
-
-/**
- * General modal configuration & toggling.
- */
-modal
-.on("shown.bs.modal", function() {
- // Focus the invite existing input
- inviteExistingQuery.focus();
- invitingExisting = true;
-})
-.on("hidden.bs.modal", function() {
- // Disable invite button,
- // reset forms, reset rendered content
- disableInviteBtn();
- inviteExistingForm.clearFormFields();
- inviteExistingForm.clearFormErrors();
- inviteExistingResults.html("");
- inviteNewForm.clearFormFields();
- inviteNewForm.clearFormErrors();
-});
-
-inviteExistingCollapsible
-.on("hidden.bs.collapse", function() {
- // Reset form & rendered content
- inviteExistingForm.clearFormFields();
- inviteExistingForm.clearFormErrors();
- inviteExistingResults.html("");
-})
-.on("hide.bs.collapse", function() {
- // Disable invite button
- disableInviteBtn();
-})
-.on("shown.bs.collapse", function() {
- // Focus input when collapsible is shown
- inviteExistingQuery.focus();
- invitingExisting = true;
-});
-
-inviteNewCollapsible
-.on("hidden.bs.collapse", function() {
- // Reset form
- inviteNewForm.clearFormFields();
- inviteNewForm.clearFormErrors();
-})
-.on("hide.bs.collapse", function() {
- // Disable invite button
- disableInviteBtn();
-})
-.on("shown.bs.collapse", function() {
- // Focus input when collapsible is shown
- inviteNewNameInput.focus();
- invitingExisting = false;
-});
-
-// Invite links simply submit either of the forms
-inviteLinks.on("click", function() {
- var $this = $(this);
-
- if (invitingExisting) {
- var form =
- inviteExistingResults
- .find("form[data-id='create-user-organization-form']");
-
- // Set the role value in the form
- form
- .find("[data-id='existing-user-role-input']")
- .attr("value", $this.attr("data-value"));
-
- // Submit the form inside "invite existing"
- animateSpinner(modalContent);
- form.submit();
- } else {
- // Set the role value in the form
- inviteNewRoleInput
- .attr("value", $this.attr("data-value"));
-
- // Submit the form inside "invite new"
- animateSpinner(modalContent);
- inviteNewForm.submit();
- }
-});
-
-/**
- * Invite existing user functionality.
- */
-
-// Invite existing form submission
-modal
-.on("ajax:success", inviteExistingForm.selector, function(ev, data, status) {
- // Clear form errors
- inviteExistingForm.clearFormErrors();
-
- // Alright, render the html
- inviteExistingResults.html(data.html);
-
- // Disable invite button
- disableInviteBtn();
-})
-.on("ajax:error", inviteExistingForm.selector, function(ev, data, status) {
- //Clear previous info
- inviteExistingResults.html("");
-
- // Display form errors
- inviteExistingForm.renderFormErrors("", data.responseJSON);
-});
-
-// Update values & enable "invite" button
-// when user clicks on existing user
-inviteExistingResults
-.on("change", "[data-action='select-existing-user']", function() {
- var $this = $(this);
- // Set the hidden input user ID
- $("[data-id='existing-user-id-input']")
- .attr("value", $this.attr("data-user-id"));
-
- // Enable button
- enableInviteBtn();
-});
-
-/**
- * Invite new user functionality.
- */
-
-inviteNewForm
-.on("ajax:success", function(ev, data, status) {
- // Reload the page
- location.reload();
-})
-.on("ajax:error", function(ev, data, status) {
- // Render form errors
- animateSpinner(modalContent, false);
- $(this).renderFormErrors("user", data.responseJSON);
-});
-
-
-// Enable/disable invite button depending whether
-// any of the new user inputs are empty
-inviteNewForm
-.on("input", "input[data-role='input']", function() {
- if (
- _.isEmpty(inviteNewNameInput.val()) ||
- _.isEmpty(inviteNewEmailInput.val())
- ) {
- disableInviteBtn();
- } else {
- enableInviteBtn();
- }
-});
-
diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss
index dd40ded79..8ac56d026 100644
--- a/app/assets/stylesheets/themes/scinote.scss
+++ b/app/assets/stylesheets/themes/scinote.scss
@@ -1496,3 +1496,30 @@ html.turbolinks-progress-bar::before {
width: 100px;
}
}
+
+.modal-invite-users {
+ .bootstrap-tagsinput {
+ min-width: 450px;
+ }
+
+ .results-container .alert {
+ margin-bottom: 10px;
+ padding: 5px;
+ }
+
+ .results-container .results-wrap {
+ max-height: 400px;
+ overflow-y: auto;
+ padding-right: 10px;
+ }
+
+ .org-selector .heading {
+ margin-top: 15px;
+ margin-bottom: 5px;
+
+ input[type=checkbox] {
+ vertical-align: middle;
+ margin: 0;
+ }
+ }
+}
diff --git a/app/controllers/users/invitations_controller.rb b/app/controllers/users/invitations_controller.rb
index ff1e7b417..66218a936 100644
--- a/app/controllers/users/invitations_controller.rb
+++ b/app/controllers/users/invitations_controller.rb
@@ -1,30 +1,164 @@
-class Users::InvitationsController < Devise::InvitationsController
+module Users
+ class InvitationsController < Devise::InvitationsController
+ include UsersGenerator
- def update
- @org = Organization.new
- @org.name = params[:organization][:name]
+ before_action :check_invite_users_permission, only: :invite_users
- super do |user|
- if user.errors.empty?
- @org.created_by = user
- @org.save
+ def update
+ @org = Organization.new
+ @org.name = params[:organization][:name]
- UserOrganization.create(
- user: user,
- organization: @org,
- role: 'admin'
- )
+ super do |user|
+ if user.errors.empty?
+ @org.created_by = user
+ @org.save
+
+ UserOrganization.create(
+ user: user,
+ organization: @org,
+ role: 'admin'
+ )
+ end
end
end
- end
- def accept_resource
- resource = super
-
- if not @org.valid?
- resource.errors.add(:base, @org.errors.to_a.first)
+ def accept_resource
+ resource = super
+ resource.errors.add(:base, @org.errors.to_a.first) unless @org.valid?
+ resource
end
- resource
+ def invite_users
+ @invite_results = []
+ @too_many_emails = false
+
+ cntr = 0
+ @emails.each do |email|
+ cntr += 1
+
+ if cntr > Constants::INVITE_USERS_LIMIT
+ @too_many_emails = true
+ break
+ end
+
+ password = generate_user_password
+
+ # Check if user already exists
+ user = nil
+ user = User.find_by_email(email) if User.exists?(email: email)
+
+ result = { email: email }
+
+ if user.present?
+ result[:status] = :user_exists
+ result[:user] = user
+ else
+ # Validate the user data
+ error = !(Constants::BASIC_EMAIL_REGEX === email)
+ error = validate_user(email, email, password).count > 0 unless error
+
+ if !error
+ user = User.invite!(
+ full_name: email,
+ email: email,
+ initials: email.upcase[0..1],
+ skip_invitation: true
+ )
+
+ result[:status] = :user_created
+ result[:user] = user
+
+ # Sending email invitation is done in background job to prevent
+ # issues with email delivery. Also invite method must be call
+ # with :skip_invitation attribute set to true - see above.
+ user.delay.deliver_invitation
+ else
+ # Return invalid status
+ result[:status] = :user_invalid
+ end
+ end
+
+ if @org.present? && result[:status] != :user_invalid
+ if UserOrganization.exists?(user: user, organization: @org)
+ user_org =
+ UserOrganization.where(user: user, organization: @org).first
+
+ result[:status] = :user_exists_and_in_org
+ elsif result[:status] == :user_exists && !user.confirmed?
+ # We don't want to allow inviting unconfirmed
+ # users (that were not invited as part of this action)
+ # into organizations
+ result[:status] = :user_exists_unconfirmed
+ else
+ # Also generate user organization relation
+ user_org = UserOrganization.new(
+ user: user,
+ organization: @org,
+ role: @role
+ )
+ user_org.save
+
+ generate_notification(
+ @user,
+ user,
+ user_org.role_str,
+ user_org.organization
+ )
+
+ if result[:status] == :user_exists
+ result[:status] = :user_exists_invited_to_org
+ else
+ result[:status] = :user_created_invited_to_org
+ end
+ end
+
+ result[:user_org] = user_org
+ end
+
+ @invite_results << result
+ end
+
+ respond_to do |format|
+ format.json do
+ render json: {
+ html: render_to_string(
+ partial: 'shared/invite_users_modal_results.html.erb'
+ )
+ }
+ end
+ end
+ end
+
+ private
+
+ def generate_notification(user, target_user, role, org)
+ title = I18n.t('notifications.assign_user_to_organization',
+ assigned_user: target_user.name,
+ role: role,
+ organization: org.name,
+ assigned_by_user: user.name)
+
+ message = "#{I18n.t('search.index.organization')} #{org.name}"
+ notification = Notification.create(
+ type_of: :assignment,
+ title: ActionController::Base.helpers.sanitize(title),
+ message: ActionController::Base.helpers.sanitize(message)
+ )
+
+ if target_user.assignments_notification
+ UserNotification.create(notification: notification, user: target_user)
+ end
+ end
+
+ def check_invite_users_permission
+ @user = current_user
+ @emails = params[:emails]
+ @org = Organization.find_by_id(params['organizationId'])
+ @role = params['role']
+
+ render_403 if @emails && @emails.empty?
+ render_403 if @org && !is_admin_of_organization(@org)
+ render_403 if @role && !UserOrganization.roles.keys.include?(@role)
+ end
end
end
diff --git a/app/controllers/users/settings_controller.rb b/app/controllers/users/settings_controller.rb
index 931773186..9aa0b3220 100644
--- a/app/controllers/users/settings_controller.rb
+++ b/app/controllers/users/settings_controller.rb
@@ -21,13 +21,7 @@ class Users::SettingsController < ApplicationController
:destroy_organization,
:organization_name,
:organization_description,
- :search_organization_users,
- :organization_users_datatable,
- :create_user_and_user_organization
- ]
-
- before_action :check_create_user_organization_permission, only: [
- :create_user_organization
+ :organization_users_datatable
]
before_action :check_user_organization_permission, only: [
@@ -118,57 +112,6 @@ class Users::SettingsController < ApplicationController
end
end
- def search_organization_users
- respond_to do |format|
- format.json {
- if params.include?(:existing_query) && params[:existing_query].strip
- query = params[:existing_query].strip
- if query.length < Constants::NAME_MIN_LENGTH
- render json: {
- "existing_query": [
- t('general.query.length_too_short',
- min_length: Constants::NAME_MIN_LENGTH)
- ]
- },
- status: :unprocessable_entity
- elsif query.length > Constants::NAME_MAX_LENGTH
- render json: {
- "existing_query": [
- t('general.query.length_too_long',
- max_length: Constants::NAME_MAX_LENGTH)
- ]
- },
- status: :unprocessable_entity
- else
- # Okay, query exists and is non-blank, find users
- nr_of_results = User.search(true, query, @org).count
-
-
- users = User.search(false, query, @org)
- .limit(Constants::MODAL_SEARCH_LIMIT)
-
- nr_of_members = User.organization_search(false, query, @org).count
-
- render json: {
- html: render_to_string({
- partial: "users/settings/organizations/existing_users_search_results.html.erb",
- locals: {
- users: users,
- nr_of_results: nr_of_results,
- nr_of_members: nr_of_members,
- org: @org,
- query: query
- }
- })
- }
- end
- else
- render json: {}, status: :bad_request
- end
- }
- end
- end
-
def organization_users_datatable
respond_to do |format|
format.json {
@@ -213,101 +156,6 @@ class Users::SettingsController < ApplicationController
redirect_to action: :organizations
end
- def create_user_organization
- @new_user_org = UserOrganization.new(create_user_organization_params)
-
- # Check if such association doesn't exist already
- if !UserOrganization.where(
- user: @new_user_org.user,
- organization: @new_user_org.organization
- ).exists? && @new_user_org.save
-
- generate_notification(@user_organization.user,
- @new_user_org.user,
- @new_user_org.role_str,
- @new_user_org.organization)
-
- flash[:notice] = I18n.t(
- 'users.settings.organizations.edit.modal_add_user.existing_flash_success',
- user: @new_user_org.user.full_name,
- role: @new_user_org.role_str
- )
- else
- flash[:alert] =
- I18n.t('users.settings.organizations.edit.modal_add_user.existing_flash_error')
- end
-
- # Either way, redirect back to organization page
- redirect_to action: :organization,
- organization_id: @new_user_org.organization_id
- end
-
- def create_user_and_user_organization
- respond_to do |format|
- # User & organization
- # parameters are already taken care of,
- # so only role needs to be verified
- if !params.include? :role or
- !UserOrganization.roles.keys.include? params[:role]
- format.json {
- render json: "Invalid role provided",
- status: :unprocessable_entity
- }
- else
- password = generate_user_password
- user_params = create_user_params
- full_name = user_params[:full_name]
- email = user_params[:email]
-
- # Validate the user data
- errors = validate_user(full_name, email, password)
-
- if errors.count == 0
- @user = User.invite!(
- full_name: full_name,
- email: email,
- initials: full_name.split(" ").map{|w| w[0].upcase}.join[0..3],
- skip_invitation: true
- )
-
- # Sending email invitation is done in background job to prevent
- # issues with email delivery. Also invite method must be call
- # with :skip_invitation attribute set to true - see above.
- @user.delay.deliver_invitation
-
- # Also generate user organization relation
- @user_org = UserOrganization.new(
- user: @user,
- organization: @org,
- role: params[:role]
- )
- @user_org.save
-
- # Flash message
- flash[:notice] = t(
- "users.settings.organizations.edit.modal_add_user.new_flash_success",
- user: @user.full_name,
- role: @user_org.role_str,
- email: @user.email
- )
- flash.keep
-
- # Return success!
- format.json {
- render json: {
- status: :ok
- }
- }
- else
- format.json {
- render json: errors,
- status: :unprocessable_entity
- }
- end
- end
- end
- end
-
def update_user_organization
respond_to do |format|
if @user_org.update(update_user_organization_params)
@@ -520,13 +368,6 @@ class Users::SettingsController < ApplicationController
end
end
- def check_create_user_organization_permission
- @org = Organization.find_by_id(params[:user_organization][:organization_id])
- unless is_admin_of_organization(@org)
- render_403
- end
- end
-
def check_user_organization_permission
@user_org = UserOrganization.find_by_id(params[:user_organization_id])
@org = @user_org.organization
@@ -565,41 +406,12 @@ class Users::SettingsController < ApplicationController
)
end
- def create_user_organization_params
- params.require(:user_organization).permit(
- :user_id,
- :organization_id,
- :role
- )
- end
-
def update_user_organization_params
params.require(:user_organization).permit(
:role
)
end
- def generate_notification(user, target_user, role, org)
- title = I18n.t('notifications.assign_user_to_organization',
- assigned_user: target_user.name,
- role: role,
- organization: org.name,
- assigned_by_user: user.name)
-
- message = "#{I18n.t('search.index.organization')} #{org.name}"
- notification = Notification.create(
- type_of: :assignment,
- title:
- ActionController::Base.helpers.sanitize(title),
- message:
- ActionController::Base.helpers.sanitize(message),
- )
-
- if target_user.assignments_notification
- UserNotification.create(notification: notification, user: target_user)
- end
- end
-
def reset_user_current_organization(user_org)
ids = user_org.user.organizations_ids
ids -= [user_org.organization.id]
diff --git a/app/models/user.rb b/app/models/user.rb
index 15fc25fdd..2e868538f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -133,39 +133,6 @@ class User < ActiveRecord::Base
.distinct
end
- # Search all active users inside given organization for
- # username & email.
- def self.organization_search(
- active_only,
- query = nil,
- organization = nil
- )
-
- if !organization.present?
- result = nil
- else
-
- result = User.all
-
- if active_only
- result = result.where.not(confirmed_at: nil)
- end
-
- ignored_ids =
- UserOrganization
- .select(:user_id)
- .where(organization_id: organization.id)
- result =
- result
- .where("users.id IN (?)", ignored_ids)
-
- result
- .where_attributes_like([:full_name, :email], query)
- .distinct
- end
- end
-
-
def empty_avatar(name, size)
file_ext = name.split(".").last
self.avatar_file_name = name
diff --git a/app/views/shared/_invite_users_modal.html.erb b/app/views/shared/_invite_users_modal.html.erb
new file mode 100644
index 000000000..b2e17a57b
--- /dev/null
+++ b/app/views/shared/_invite_users_modal.html.erb
@@ -0,0 +1,145 @@
+<%
+# How to use this modal:
+# 1. Render it in the page (HTML) of your choice
+# 2. Add an element (e.g.
) with following attributes:
+# * data-trigger="invite-users",
+# * data-modal-id="modal-id",
+#
+# Modal parameters:
+# * modal_id: unique id so the JS works if multiple modals are present
+# on the same page
+# * type:
+# * 'invite_to_org' => params: organization
+# * 'invite_to_org_with_role' => params: organization, role
+# * 'invite',
+# * 'invite_with_org_selector',
+# * 'invite_with_org_selector_and_role' => params: role
+# * organization: invite users to the specified organization
+# * role: all users are invited as the specified role
+# * (optional) text_title: custom title text for modal
+# * (optional) text_invite_heading: custom invite heading text for modal
+%>
+<%
+text_title ||= nil
+text_invite_heading ||= nil
+invite_to_org = type.in?(%w(invite_to_org invite_to_org_with_role))
+%>
+
+ <%= "data-organization-role=#{role}" if type.in?(%w(invite_to_org_with_role invite_with_org_selector_and_role)) %>
+>
+
+
+
+
+
+
+
+
+ <% if text_invite_heading %>
+ <%= text_invite_heading %>
+ <% else %>
+ <% if invite_to_org %>
+ <%= t('invite_users.to_org.heading', organization: organization.name) %>
+ <% else %>
+ <%= t('invite_users.no_org.heading') %>
+ <% end %>
+ <% end %>
+
+
+
+
+
+
<%= t('invite_users.input_subtitle') %>
+
+ <% if type.in?(['invite_with_org_selector', 'invite_with_org_selector_and_role']) %>
+ <% # Only allow inviting to organizations where user is admin %>
+ <% uos = current_user ? current_user.user_organizations.where(role: UserOrganization.roles[:admin]).joins(:organization) : [] %>
+ <% if uos.count > 0 %>
+
+
+
+ <%= t('invite_users.invite_to_org_heading') %>
+
+ <%= select_tag(
+ 'organization-select',
+ options_for_select(
+ uos.pluck('organizations.name', 'organizations.id')
+ ),
+ {
+ class: 'form-control selectpicker',
+ 'data-role' => 'org-selector-dropdown',
+ disabled: 'disabled'
+ }
+ ) %>
+
+ <% end %>
+ <% end %>
+
+
+
+
+
+
+
+
+
+
diff --git a/app/views/shared/_invite_users_modal_results.html.erb b/app/views/shared/_invite_users_modal_results.html.erb
new file mode 100644
index 000000000..b37644336
--- /dev/null
+++ b/app/views/shared/_invite_users_modal_results.html.erb
@@ -0,0 +1,54 @@
+
+ <%= t('invite_users.results.heading') %>
+
+
+
+<% @invite_results.each do |result| %>
+<% if result[:status] == :user_exists %>
+
+ <%= result[:email] %>
+ -
+ <%= t('invite_users.results.user_exists') %>
+
+<% elsif result[:status] == :user_exists_unconfirmed %>
+
+ <%= result[:email] %>
+ -
+ <%= t('invite_users.results.user_exists_unconfirmed', organization: @org.name) %>
+
+<% elsif result[:status] == :user_exists_and_in_org %>
+
+ <%= result[:email] %>
+ -
+ <%= t('invite_users.results.user_exists_and_in_org', organization: @org.name, role: t("user_organizations.enums.role.#{result[:user_org].role}")) %>
+
+<% elsif result[:status] == :user_exists_invited_to_org %>
+
+ <%= result[:email] %>
+ -
+ <%= t('invite_users.results.user_exists_invited_to_org', organization: @org.name, role: t("user_organizations.enums.role.#{result[:user_org].role}")) %>
+
+<% elsif result[:status] == :user_created %>
+
+ <%= result[:email] %>
+ -
+ <%= t('invite_users.results.user_created') %>
+
+<% elsif result[:status] == :user_created_invited_to_org %>
+
+ <%= result[:email] %>
+ -
+ <%= t('invite_users.results.user_created_invited_to_org', organization: @org.name, role: t("user_organizations.enums.role.#{result[:user_org].role}")) %>
+
+<% elsif result[:status] == :user_invalid %>
+
+ <%= result[:email] %>
+ -
+ <%= t('invite_users.results.user_invalid') %>
+
+<% end %>
+<% end %>
+<% if @too_many_emails %>
+<%= t('invite_users.results.too_many_emails', nr: Constants::INVITE_USERS_LIMIT) %>
+<% end %>
+
\ No newline at end of file
diff --git a/app/views/shared/_navigation.html.erb b/app/views/shared/_navigation.html.erb
index 34ab19eb4..1831d92f8 100644
--- a/app/views/shared/_navigation.html.erb
+++ b/app/views/shared/_navigation.html.erb
@@ -181,7 +181,7 @@
aria-expanded="false">
-
- <%= link_to "#", class: "btn btn-primary", data: { toggle: "modal", target: "#add-user-modal" } do %>
+
<%= t("users.settings.organizations.edit.add_user") %>
- <% end %>
+
@@ -95,7 +95,15 @@
<%= render partial: "users/settings/organizations/name_modal.html.erb" %>
<%= render partial: "users/settings/organizations/description_modal.html.erb" %>
-<%= render partial: "users/settings/organizations/add_user_modal.html.erb", locals: { org: @org } %>
+<%= render(
+ partial: 'shared/invite_users_modal.html.erb',
+ locals: {
+ modal_id: 'org-invite-users-modal',
+ type: 'invite_to_org',
+ organization: @org
+ }
+ )
+%>
<%= render partial: "users/settings/organizations/destroy_modal.html.erb", locals: { org: @org } %>
<%= render partial: "users/settings/organizations/destroy_user_organization_modal.html.erb" %>
<%= stylesheet_link_tag 'datatables' %>
diff --git a/app/views/users/settings/organizations/_add_user_modal.html.erb b/app/views/users/settings/organizations/_add_user_modal.html.erb
deleted file mode 100644
index 0b41bf8d3..000000000
--- a/app/views/users/settings/organizations/_add_user_modal.html.erb
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- <%= bootstrap_form_tag url: search_organization_users_path(org, format: :json), remote: true, method: :get, data: { id: "invite-existing-form" } do |f| %>
-
- <% end %>
-
-
-
-
-
-
-
-
-
-
-
-
- <%= bootstrap_form_for User.new, url: create_user_and_user_organization_path(format: :json), remote: true, data: { id: "invite-new-form" } do |f| %>
-
-
- <%= f.text_field :full_name, label: t("users.settings.organizations.edit.modal_add_user.new_label_name"), placeholder: t("users.settings.organizations.edit.modal_add_user.new_placeholder_name"), data: { id: "invite-new-name-input", role: "input" } %>
- <%= f.text_field :email, label: t("users.settings.organizations.edit.modal_add_user.new_label_email"), placeholder: t("users.settings.organizations.edit.modal_add_user.new_placeholder_email"), data: { id: "invite-new-email-input", role: "input" } %>
- <% end %>
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/views/users/settings/organizations/_existing_users_search_results.html.erb b/app/views/users/settings/organizations/_existing_users_search_results.html.erb
deleted file mode 100644
index dc9c23f0f..000000000
--- a/app/views/users/settings/organizations/_existing_users_search_results.html.erb
+++ /dev/null
@@ -1,36 +0,0 @@
-<% if users.count > 0 %>
- <%= bootstrap_form_for UserOrganization.new, url: create_user_organization_path, data: { id: "create-user-organization-form" } do %>
-
-
-
-
-
- <%= t("users.settings.organizations.edit.modal_add_user.existing_results_title") %>
-
- <% users.each do |user| %>
-
-
- <%= highlight user.full_name, query.strip.split(/\s+/) %>
-
- <% end %>
-
- <% if nr_of_members > 0 %>
-
- <%= t("users.settings.organizations.edit.modal_add_user.existing_users_members_smalltext", count: nr_of_members) %>
-
- <% end %>
- <% if nr_of_results > users.count %>
-
- <%= t("users.settings.organizations.edit.modal_add_user.existing_users_smalltext", count: users.count) %>
-
- <% end %>
- <% end %>
-<% else%>
-
- <% if nr_of_members > 0 %>
- <%= t("users.settings.organizations.edit.modal_add_user.no_existing_users_members_smalltext", count: nr_of_members) %>
- <% else %>
- <%= t("users.settings.organizations.edit.modal_add_user.no_existing_users") %>
- <% end %>
-
-<% end %>
\ No newline at end of file
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index f1a105b3b..55f62d834 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -59,6 +59,7 @@ Rails.application.config.assets.precompile += %w(assets.js)
Rails.application.config.assets.precompile += %w(comments.js)
Rails.application.config.assets.precompile += %w(projects/show.js)
Rails.application.config.assets.precompile += %w(notifications.js)
+Rails.application.config.assets.precompile += %w(users/invite_users_modal.js)
# Libraries needed for Handsontable formulas
Rails.application.config.assets.precompile += %w(lodash.js)
diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb
index becd940ec..ddc6bff5e 100644
--- a/config/initializers/constants.rb
+++ b/config/initializers/constants.rb
@@ -41,6 +41,9 @@ class Constants
# Activity limited query/display elements for pages
ACTIVITY_AND_NOTIF_SEARCH_LIMIT = 10
+ # Maximum number of users that can be invited in a single action
+ INVITE_USERS_LIMIT = 20
+
#=============================================================================
# File and data memory size
#=============================================================================
@@ -196,6 +199,9 @@ class Constants
'text/plain'
].freeze
+ # Very basic regex to check for validity of emails
+ BASIC_EMAIL_REGEX = /^[^@]+@[^@]+\.[^@]+$/
+
# Organization name for default admin user
DEFAULT_PRIVATE_ORG_NAME = 'My projects'.freeze
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4d49ceab0..a4410acb5 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1209,32 +1209,6 @@ en:
can_delete_message: "This team can be deleted because it doesn't have any projects."
delete_text: "Delete team."
cannot_delete_message_projects: "Cannot delete this team. Only empty teams (without any projects) can be deleted."
- modal_add_user:
- title: "Invite user to team"
- existing_heading: "Invite existing sciNote user"
- existing_label: "Find existing user by name or email:"
- existing_placeholder: "Name or email"
- existing_results_title: "Choose user"
- no_existing_users: "No existing users found."
- existing_users_members_smalltext:
- one: "The search matched 1 user that is already member of the team."
- other: "The search matched %{count} users that are already members of the team."
- no_existing_users_members_smalltext:
- one: "No existing users that could be invited found; the search, however, matched 1 user that is already member of the team."
- other: "No existing users that could be invited found; the search, however, matched %{count} users that are already members of the team."
- existing_users_smalltext: "Only showing top %{count} matched results."
- existing_flash_success: "User %{user} successfully invited to team as %{role}."
- existing_flash_error: "Error inviting user to team."
- new_heading: "Invite new user"
- new_label_name: "Type in the new user's full name:"
- new_placeholder_name: "Full name"
- new_label_email: "Type in the new user's email:"
- new_placeholder_email: "Email"
- new_flash_success: "User %{user} successfully invited to team as %{role}. Confirmation email was sent to %{email}."
- invite: "Invite user"
- invite_guest: "as Guest"
- invite_user: "as Normal user"
- invite_admin: "as Administrator"
modal_destroy_organization:
title: "Delete team"
message: "Are you sure you wish to delete team %{org}? All of the users will be removed from the team as well. This action is irreversible."
@@ -1449,6 +1423,30 @@ en:
head_title: "Edit protocol"
no_keywords: "No keywords"
+ invite_users:
+ to_org:
+ title: "Invite users to team %{organization}"
+ heading: "Invite more people to team %{organization} and start using sciNote."
+ no_org:
+ title: "Invite users to sciNote"
+ heading: "Invite more people to start using sciNote."
+ input_subtitle: "Input one or multiple emails, confirm each email with ENTER key."
+ invite_to_org_heading: "Invite users to my team:"
+ invite_btn: "Invite user/s"
+ invite_guest: "as Guest/s"
+ invite_user: "as Normal user/s"
+ invite_admin: "as Administrator/s"
+ results:
+ heading: "Invitation results:"
+ user_exists: "User is already a member of sciNote."
+ user_exists_unconfirmed: "User is already a member of sciNote but is not confirmed yet - cannot invite to team %{organization}."
+ user_exists_and_in_org: "User is already a member of sciNote and team %{organization} as %{role}."
+ user_exists_invited_to_org: "User was already a member of sciNote - successfully invited to team %{organization} as %{role}."
+ user_created: "User succesfully invited to sciNote."
+ user_created_invited_to_org: "User successfully invited to sciNote and team %{organization} as %{role}."
+ user_invalid: "Invalid email."
+ too_many_emails: "Only invited first %{nr} emails. To invite more users, "
+
time:
formats:
full: "%d.%m.%Y %H:%M"
diff --git a/config/routes.rb b/config/routes.rb
index d1aca0457..01073c51f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -29,16 +29,20 @@ Rails.application.routes.draw do
put "users/settings/organizations/:organization_id", to: "users/settings#update_organization", as: "update_organization"
get "users/settings/organizations/:organization_id/name", to: "users/settings#organization_name", as: "organization_name"
get "users/settings/organizations/:organization_id/description", to: "users/settings#organization_description", as: "organization_description"
- get "users/settings/organizations/:organization_id/search", to: "users/settings#search_organization_users", as: "search_organization_users"
post "users/settings/organizations/:organization_id/users_datatable", to: "users/settings#organization_users_datatable", as: "organization_users_datatable"
delete "users/settings/organizations/:organization_id", to: "users/settings#destroy_organization", as: "destroy_organization"
- post "users/settings/user_organizations/new", to: "users/settings#create_user_organization", as: "create_user_organization"
- post "users/settings/users_organizations/new_user", to: "users/settings#create_user_and_user_organization", as: "create_user_and_user_organization"
put "users/settings/user_organizations/:user_organization_id", to: "users/settings#update_user_organization", as: "update_user_organization"
get "users/settings/user_organizations/:user_organization_id/leave_html", to: "users/settings#leave_user_organization_html", as: "leave_user_organization_html"
get "users/settings/user_organizations/:user_organization_id/destroy_html", to: "users/settings#destroy_user_organization_html", as: "destroy_user_organization_html"
delete "users/settings/user_organizations/:user_organization_id", to: "users/settings#destroy_user_organization", as: "destroy_user_organization"
+ # Invite users
+ devise_scope :user do
+ post '/invite',
+ to: 'users/invitations#invite_users',
+ as: 'invite_users'
+ end
+
# Notifications
get 'users/:id/recent_notifications',
to: 'user_notifications#recent_notifications',