diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 7d1935010e..2d1dc494f4 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, $window, NavigationCheck, enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu, StatusMessage) -> + .controller "enterpriseCtrl", ($scope, $http, $window, NavigationCheck, enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu, StatusMessage) -> $scope.Enterprise = enterprise $scope.PaymentMethods = EnterprisePaymentMethods.paymentMethods $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods @@ -26,7 +26,7 @@ angular.module("admin.enterprises") # from a directive "nav-check" in the page - if we pass it here it will be called in the test suite, # and on all new uses of this contoller, and we might not want that. enterpriseNavCallback = -> - if $scope.enterprise_form.$dirty + if $scope.enterprise_form != undefined && $scope.enterprise_form.$dirty t('admin.unsaved_confirm_leave') # Register the NavigationCheck callback @@ -51,3 +51,16 @@ angular.module("admin.enterprises") $scope.enterprise_form?.$setDirty() else alert ("#{manager.email}" + " " + t("is_already_manager")) + + $scope.inviteManager = -> + $scope.invite_errors = $scope.invite_success = null + email = $scope.newUser + + $http.post("/admin/manager_invitations", {email: email, enterprise_id: $scope.Enterprise.id}).success (data)-> + $scope.addManager({id: data.user, email: email}) + $scope.invite_success = t('user_invited', email: email) + .error (data) -> + $scope.invite_errors = data.errors + + $scope.resetModal = -> + $scope.newUser = $scope.invite_errors = $scope.invite_success = null diff --git a/app/assets/javascripts/templates/admin/modals/invite_manager.html.haml b/app/assets/javascripts/templates/admin/modals/invite_manager.html.haml new file mode 100644 index 0000000000..bbde3baa49 --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/invite_manager.html.haml @@ -0,0 +1,19 @@ +#invite-manager-modal{ng: {app: 'admin.enterprises', controller: 'enterpriseCtrl'}} + + .margin-bottom-30.text-center + .text-big + = t('js.admin.modals.invite_title') + + %p.alert-box.ok{ng: {show: 'invite_success'}} + {{invite_success}} + + %p.alert-box.error{ng: {show: 'invite_errors'}} + {{invite_errors}} + + %input#invite_email.fullwidth.margin-bottom-20{ng: {model: 'newUser'}} + + .margin-bottom-20.text-center + %button.text-center.margin-top-10{ng: {show: '!invite_success', click: 'inviteManager()'}} + = t('js.admin.modals.invite') + %button.text-center.margin-top-10{ng: {show: 'invite_success', click: 'resetModal(); close()'}} + = t('js.admin.modals.close') diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 9a970c87d4..d10ca748d0 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -1,3 +1,7 @@ +input[type="submit"], input[type="button"], button, .button { + cursor: pointer; +} + .text-center { text-align: center; } diff --git a/app/controllers/admin/manager_invitations_controller.rb b/app/controllers/admin/manager_invitations_controller.rb new file mode 100644 index 0000000000..ed70322f34 --- /dev/null +++ b/app/controllers/admin/manager_invitations_controller.rb @@ -0,0 +1,39 @@ +module Admin + class ManagerInvitationsController < Spree::Admin::BaseController + def create + @email = params[:email] + @enterprise = Enterprise.find(params[:enterprise_id]) + + authorize! :edit, @enterprise + + existing_user = Spree::User.find_by_email(@email) + + if existing_user + render json: { errors: t('admin.enterprises.invite_manager.user_already_exists') }, status: :unprocessable_entity + return + end + + new_user = create_new_manager + + if new_user + render json: { user: new_user.id }, status: :ok + else + render json: { errors: t('admin.enterprises.invite_manager.error') }, status: 500 + end + end + + private + + def create_new_manager + password = Devise.friendly_token + new_user = Spree::User.create(email: @email, unconfirmed_email: @email, password: password) + new_user.reset_password_token = Devise.friendly_token + new_user.save! + + @enterprise.users << new_user + Delayed::Job.enqueue ManagerInvitationJob.new(@enterprise.id, new_user.id) + + new_user + end + end +end diff --git a/app/controllers/user_confirmations_controller.rb b/app/controllers/user_confirmations_controller.rb index f5ee6a10e5..8e3e504eaa 100644 --- a/app/controllers/user_confirmations_controller.rb +++ b/app/controllers/user_confirmations_controller.rb @@ -38,6 +38,10 @@ class UserConfirmationsController < DeviseController 'not_confirmed' end + if resource.reset_password_token.present? + return spree.edit_spree_user_password_path(reset_password_token: resource.reset_password_token) + end + path = (session[:confirmation_return_url] || login_path).to_s path += path.include?('?') ? '&' : '?' path + "validation=#{result}" diff --git a/app/jobs/manager_invitation_job.rb b/app/jobs/manager_invitation_job.rb new file mode 100644 index 0000000000..5a1e894f0e --- /dev/null +++ b/app/jobs/manager_invitation_job.rb @@ -0,0 +1,7 @@ +ManagerInvitationJob = Struct.new(:enterprise_id, :user_id) do + def perform + enterprise = Enterprise.find enterprise_id + user = Spree::User.find user_id + EnterpriseMailer.manager_invitation(enterprise, user).deliver + end +end diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index 350a247d73..03d9279c93 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -12,6 +12,18 @@ class EnterpriseMailer < Spree::BaseMailer :subject => subject) end + def manager_invitation(enterprise, user) + @enterprise = enterprise + @instance = Spree::Config[:site_name] + @instance_email = from_address + + subject = t('enterprise_mailer.invite_manager.subject', enterprise: @enterprise.name) + + mail(to: user.email, + from: from_address, + subject: subject) + end + private def find_enterprise(enterprise) diff --git a/app/views/admin/enterprises/form/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml index 35db6bea9d..adc6d4ba67 100644 --- a/app/views/admin/enterprises/form/_users.html.haml +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -60,3 +60,16 @@ - @enterprise.users.each do |manager| = manager.email %br + +- if full_permissions + %form + .row + .three.columns.alpha + %label + = t('.invite_manager') + %div{'ofn-with-tip' => t('.invite_manager_tip')} + %a= t('admin.whats_this') + .eight.columns.omega + .row + %a.button.help-modal{template: 'admin/modals/invite_manager.html'} + = t('.add_unregistered_user') diff --git a/app/views/enterprise_mailer/manager_invitation.html.haml b/app/views/enterprise_mailer/manager_invitation.html.haml new file mode 100644 index 0000000000..e7256b47c9 --- /dev/null +++ b/app/views/enterprise_mailer/manager_invitation.html.haml @@ -0,0 +1,16 @@ +%h3 + = t('invite_email.greeting') +%p.lead + = t('invite_email.invited_to_manage', enterprise: @enterprise.name, instance: @instance) + +%p + = t('invite_email.confirm_your_email') +%p + = t('invite_email.mistakenly_sent', owner_email: @enterprise.owner.email, instance: @instance, instance_email: @instance_email) + +%p + = t :email_help + += render 'shared/mailers/signoff' + += render 'shared/mailers/social_and_contact' diff --git a/config/locales/en.yml b/config/locales/en.yml index 115769c326..1f817071ee 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -114,6 +114,8 @@ en: subject: "Please confirm the email address for %{enterprise}" welcome: subject: "%{enterprise} is now on %{sitename}" + invite_manager: + subject: "%{enterprise} has invited you to be a manager" producer_mailer: order_cycle: subject: "Order cycle report for %{producer}" @@ -665,6 +667,9 @@ en: notifications_note: 'Note: A new email address may need to be confirmed prior to use' managers: Managers managers_tip: The other users with permission to manage this enterprise. + invite_manager: "Invite Manager" + invite_manager_tip: "Invite an unregistered user to sign up and become a manager of this enterprise." + add_unregistered_user: "Add an unregistered user" email_confirmed: "Email confirmed" email_not_confirmed: "Email not confirmed" actions: @@ -724,6 +729,9 @@ en: welcome_text: You have successfully created a next_step: Next step choose_starting_point: 'Choose your starting point:' + invite_manager: + user_already_exists: "User already exists" + error: "Something went wrong" order_cycles: edit: advanced_settings: Advanced Settings @@ -1319,6 +1327,13 @@ See the %{link} to find out more about %{sitename}'s features and to start using If you are a producer or food enterprise, we are excited to have you as a part of the network." email_signup_help_html: "We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at %{email}" + invite_email: + greeting: "Hello!" + invited_to_manage: "You have been invited to manage %{enterprise} on %{instance}." + confirm_your_email: "You will receive an email shortly to confirm your registration." + set_a_password: "You will then be prompted to set a password before you are able to administer the enterprise." + mistakenly_sent: "Not sure why you have received this email? Please contact %{owner_email} for more information, or you can contact %{instance} at %{instance_email}." + producer_mail_greeting: "Dear" producer_mail_text_before: "We now have all the consumer orders for the next food drop." producer_mail_order_text: "Here is a summary of the orders for your products:" @@ -2133,10 +2148,10 @@ See the %{link} to find out more about %{sitename}'s features and to start using unsaved_changes_confirmation: "Unsaved changes will be lost. Continue anyway?" one_product_unsaved: "Changes to one product remain unsaved." products_unsaved: "Changes to %{n} products remain unsaved." - add_manager: "Add a manager" is_already_manager: "is already a manager!" no_change_to_save: " No change to save" - add_manager: "Add a manager" + user_invited: "%{email} has been invited to manage this enterprise" + add_manager: "Add an existing user" users: "Users" about: "About" images: "Images" @@ -2219,6 +2234,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using enterprise_limit_reached: "You have reached the standard limit of enterprises per account. Write to %{contact_email} if you need to increase it." modals: got_it: Got it + close: "Close" + invite: "Invite" + invite_title: "Invite an unregistered user" tag_rule_help: title: Tag Rules overview: Overview diff --git a/config/routes.rb b/config/routes.rb index 9d79e32300..2062a1d60f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -113,6 +113,8 @@ Openfoodnetwork::Application.routes.draw do resources :tag_rules, only: [:destroy] end + resources :manager_invitations, only: [:create] + resources :enterprise_relationships resources :enterprise_roles diff --git a/spec/controllers/admin/manager_invitations_controller_spec.rb b/spec/controllers/admin/manager_invitations_controller_spec.rb new file mode 100644 index 0000000000..01b7c984d1 --- /dev/null +++ b/spec/controllers/admin/manager_invitations_controller_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +module Admin + describe ManagerInvitationsController, type: :controller do + let!(:existing_user) { create(:user) } + let!(:enterprise) { create(:enterprise) } + let(:admin) { create(:admin_user) } + + describe "#create" do + context "when given email matches an existing user" do + before do + controller.stub spree_current_user: admin + end + + it "returns an error" do + spree_post :create, {email: existing_user.email, enterprise_id: enterprise.id} + + expect(response.status).to eq 422 + expect(json_response['errors']).to eq I18n.t('admin.enterprises.invite_manager.user_already_exists') + end + end + + context "signing up a new user" do + before do + controller.stub spree_current_user: admin + end + + it "creates a new user, sends an invitation email, and returns the user id" do + expect do + spree_post :create, {email: 'un.registered@email.com', enterprise_id: enterprise.id} + end.to enqueue_job Delayed::PerformableMethod + + new_user = Spree::User.find_by_email('un.registered@email.com') + + expect(new_user.reset_password_token).to_not be_nil + expect(response.status).to eq 200 + expect(json_response['user']).to eq new_user.id + end + end + end + end +end diff --git a/spec/controllers/user_confirmations_controller_spec.rb b/spec/controllers/user_confirmations_controller_spec.rb index d264a011a0..678e7ccd86 100644 --- a/spec/controllers/user_confirmations_controller_spec.rb +++ b/spec/controllers/user_confirmations_controller_spec.rb @@ -25,17 +25,17 @@ describe UserConfirmationsController, type: :controller do end context "that has not been confirmed" do - it "redirects the user to login" do - spree_get :show, confirmation_token: unconfirmed_user.confirmation_token - expect(response).to redirect_to login_path(validation: 'confirmed') - end - it "confirms the user" do spree_get :show, confirmation_token: unconfirmed_user.confirmation_token expect(unconfirmed_user.reload.confirmed_at).not_to eq(nil) end - it "redirects to previous url" do + it "redirects the user to #/login by default" do + spree_get :show, confirmation_token: unconfirmed_user.confirmation_token + expect(response).to redirect_to login_path(validation: 'confirmed') + end + + it "redirects to previous url, if present" do session[:confirmation_return_url] = producers_path + '#/login' spree_get :show, confirmation_token: unconfirmed_user.confirmation_token expect(response).to redirect_to producers_path + '#/login?validation=confirmed' @@ -46,6 +46,13 @@ describe UserConfirmationsController, type: :controller do spree_get :show, confirmation_token: unconfirmed_user.confirmation_token expect(response).to redirect_to registration_path + '#/signup?after_login=%2Fregister&validation=confirmed' end + + it "redirects to set password page, if user needs to reset their password" do + unconfirmed_user.reset_password_token = Devise.friendly_token + unconfirmed_user.save! + spree_get :show, confirmation_token: unconfirmed_user.confirmation_token + expect(response).to redirect_to spree.edit_spree_user_password_path(reset_password_token: unconfirmed_user.reset_password_token) + end end end diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb index ee036b9d27..5029a8b054 100644 --- a/spec/features/admin/enterprise_roles_spec.rb +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -83,6 +83,7 @@ feature %q{ let!(:user1) { create(:user, email: 'user1@example.com') } let!(:user2) { create(:user, email: 'user2@example.com') } let!(:user3) { create(:user, email: 'user3@example.com', confirmed_at: nil) } + let(:new_email) { 'new@manager.com' } let!(:enterprise) { create(:enterprise, name: 'Test Enterprise', owner: user1) } let!(:enterprise_role) { create(:enterprise_role, user_id: user2.id, enterprise_id: enterprise.id) } @@ -114,7 +115,9 @@ feature %q{ # user3 has been added and has an unconfirmed email address expect(page).to have_css "tr#manager-#{user3.id}" - expect(page).to have_css 'i.unconfirmed' + within "tr#manager-#{user3.id}" do + expect(page).to have_css 'i.unconfirmed' + end end end @@ -133,6 +136,29 @@ feature %q{ end end end + + it "can invite unregistered users to be managers" do + find('a.button.help-modal').click + expect(page).to have_css '#invite-manager-modal' + + within '#invite-manager-modal' do + fill_in 'invite_email', with: new_email + click_button I18n.t('js.admin.modals.invite') + expect(page).to have_content I18n.t('user_invited', email: new_email) + click_button I18n.t('js.admin.modals.close') + end + + new_user = Spree::User.find_by_email_and_confirmed_at(new_email, nil) + expect(Enterprise.managed_by(new_user)).to include enterprise + + within 'table.managers' do + expect(page).to have_content new_email + + within "tr#manager-#{new_user.id}" do + expect(page).to have_css 'i.unconfirmed' + end + end + end end end diff --git a/spec/mailers/enterprise_mailer_spec.rb b/spec/mailers/enterprise_mailer_spec.rb index bac747860f..a31a9ec0e2 100644 --- a/spec/mailers/enterprise_mailer_spec.rb +++ b/spec/mailers/enterprise_mailer_spec.rb @@ -2,17 +2,29 @@ require 'spec_helper' describe EnterpriseMailer do let!(:enterprise) { create(:enterprise) } + let!(:user) { create(:user) } before do ActionMailer::Base.deliveries = [] Spree::MailMethod.create!(environment: 'test') end - it "sends a welcome email when given an enterprise" do + describe "#welcome" do + it "sends a welcome email when given an enterprise" do EnterpriseMailer.welcome(enterprise).deliver mail = ActionMailer::Base.deliveries.first expect(mail.subject) .to eq "#{enterprise.name} is now on #{Spree::Config[:site_name]}" + end + end + + describe "#manager_invitation" do + it "should send a manager invitation email when given an enterprise and user" do + EnterpriseMailer.manager_invitation(enterprise, user).deliver + expect(ActionMailer::Base.deliveries.count).to eq 1 + mail = ActionMailer::Base.deliveries.first + expect(mail.subject).to eq "#{enterprise.name} has invited you to be a manager" + end end end