Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-04-15 13:48:28 +0300
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-04-15 13:48:28 +0300
commite3d818a4e8f51c00aaba1924690f728aa468d7f2 (patch)
tree2893b6d4528cbb0580d42de94725ff00fcfd5786 /app
parentb75866120b2391d0f2f01d03601a19baa9fa9b4e (diff)
parent3d0c0bd922dadbf09c46f2b8acb58d44565394ce (diff)
Merge branch 'invitation' into 'master'
Allow users to be invited. Addresses private issue https://dev.gitlab.org/gitlab/gitlabhq/issues/2058. The "Add members" panes for both Group Members and Project Members have gained a line of text by the People field. ![Screen_Shot_2015-04-10_at_14.14.32](https://gitlab.com/gitlab-org/gitlab-ce/uploads/fe990e65eccd9203d7324b492941362b/Screen_Shot_2015-04-10_at_14.14.32.png) Entering an email address that is not already a member will give you the option to invite them. ![Screen_Shot_2015-04-10_at_14.14.48](https://gitlab.com/gitlab-org/gitlab-ce/uploads/d6b0d4571ea90f2a2e4af8f5b336e8e1/Screen_Shot_2015-04-10_at_14.14.48.png) Choosing the option will add them to the People field. This works the right way (TM) in combination with adding existing users as members. ![Screen_Shot_2015-04-10_at_14.15.09](https://gitlab.com/gitlab-org/gitlab-ce/uploads/a618e5ec292d79578b16400dca6d4cfe/Screen_Shot_2015-04-10_at_14.15.09.png) The invited member will be shown in the members list as such. The access level can be changed, and the invite can be revoked by deleting the member. ![Screen_Shot_2015-04-10_at_14.15.19](https://gitlab.com/gitlab-org/gitlab-ce/uploads/3695b9a6778d367b275115747579b46e/Screen_Shot_2015-04-10_at_14.15.19.png) The invited user will receive an email with an "Accept invitation" link. ![Screen_Shot_2015-04-10_at_14.17.52](https://gitlab.com/gitlab-org/gitlab-ce/uploads/730121888153117d83c3cd0e4f5c90f6/Screen_Shot_2015-04-10_at_14.17.52.png) If they're not already logged in, clicking this link will redirect them to the sign in/up page with a helpful notice. ![Screen_Shot_2015-04-10_at_14.18.12](https://gitlab.com/gitlab-org/gitlab-ce/uploads/1a26a5fa13321e7ef77ed8b538c8557d/Screen_Shot_2015-04-10_at_14.18.12.png) Signing in or signing up will redirect them back to the invite detail page, where they can actually accept the invitation, which will update the member record in question to point to the user in question. ![Screen_Shot_2015-04-10_at_14.18.48](https://gitlab.com/gitlab-org/gitlab-ce/uploads/7ac33085463a99b8cfa6baa13bfa1235/Screen_Shot_2015-04-10_at_14.18.48.png) Accepting the invitation will redirect them to the group (or project) with an appropriate notice. ![Screen_Shot_2015-04-10_at_14.18.58](https://gitlab.com/gitlab-org/gitlab-ce/uploads/7bf02a2e3bea589a11df401c23e68648/Screen_Shot_2015-04-10_at_14.18.58.png) As currently, they will also receive this information by email. ![Screen_Shot_2015-04-10_at_14.24.00](https://gitlab.com/gitlab-org/gitlab-ce/uploads/b44a342068433a268c0a06ed9e791ffa/Screen_Shot_2015-04-10_at_14.24.00.png) At the same time, the person who initially invited the email address is sent a notification as well, so they know of the new member and to tell them what name the user signed up with. ![Screen_Shot_2015-04-10_at_14.19.07](https://gitlab.com/gitlab-org/gitlab-ce/uploads/b29fea128186f938ec76bd7dec016b83/Screen_Shot_2015-04-10_at_14.19.07.png) The member row on the Members page will now have been updated with the new user account. ![Screen_Shot_2015-04-10_at_14.19.23](https://gitlab.com/gitlab-org/gitlab-ce/uploads/cf503d3d1679614e03acec2e946a28c3/Screen_Shot_2015-04-10_at_14.19.23.png) See merge request !500
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/users_select.js.coffee38
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb21
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/invites_controller.rb83
-rw-r--r--app/controllers/projects/project_members_controller.rb32
-rw-r--r--app/helpers/groups_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/selects_helper.rb2
-rw-r--r--app/mailers/emails/groups.rb42
-rw-r--r--app/mailers/emails/projects.rb47
-rw-r--r--app/models/ability.rb12
-rw-r--r--app/models/group.rb15
-rw-r--r--app/models/member.rb140
-rw-r--r--app/models/members/group_member.rb33
-rw-r--r--app/models/members/project_member.rb58
-rw-r--r--app/models/project_team.rb30
-rw-r--r--app/services/notification_service.rb24
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/views/admin/groups/show.html.haml15
-rw-r--r--app/views/admin/projects/show.html.haml11
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/dashboard/groups/index.html.haml2
-rw-r--r--app/views/groups/group_members/_group_member.html.haml35
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml5
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/milestones/_milestone.html.haml2
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/invites/show.html.haml29
-rw-r--r--app/views/layouts/_head_panel.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml2
-rw-r--r--app/views/notify/group_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/group_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/group_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/group_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/group_member_invited_email.html.haml14
-rw-r--r--app/views/notify/group_member_invited_email.text.erb4
-rw-r--r--app/views/notify/project_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/project_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/project_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/project_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/project_member_invited_email.html.haml13
-rw-r--r--app/views/notify/project_member_invited_email.text.erb4
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml5
-rw-r--r--app/views/projects/project_members/_project_member.html.haml38
49 files changed, 683 insertions, 149 deletions
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index ccd85f2455d..aeeed9ca3cc 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -8,6 +8,7 @@ class @UsersSelect
@groupId = $(select).data('group-id')
showNullUser = $(select).data('null-user')
showAnyUser = $(select).data('any-user')
+ showEmailUser = $(select).data('email-user')
firstUser = $(select).data('first-user')
$(select).select2
@@ -19,20 +20,6 @@ class @UsersSelect
data = { results: users }
if query.term.length == 0
- anyUser = {
- name: 'Any',
- avatar: null,
- username: 'none',
- id: null
- }
-
- nullUser = {
- name: 'Unassigned',
- avatar: null,
- username: 'none',
- id: 0
- }
-
if firstUser
# Move current user to the front of the list
for obj, index in data.results
@@ -40,11 +27,34 @@ class @UsersSelect
data.results.splice(index, 1)
data.results.unshift(obj)
break
+
if showNullUser
+ nullUser = {
+ name: 'Unassigned',
+ avatar: null,
+ username: 'none',
+ id: 0
+ }
data.results.unshift(nullUser)
+
if showAnyUser
+ anyUser = {
+ name: 'Any',
+ avatar: null,
+ username: 'none',
+ id: null
+ }
data.results.unshift(anyUser)
+ if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/)
+ emailUser = {
+ name: "Invite \"#{query.term}\"",
+ avatar: null,
+ username: query.term,
+ id: query.term
+ }
+ data.results.unshift(emailUser)
+
query.callback(data)
initSelection: (element, callback) =>
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 9d9adaa467f..22d045fc388 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -41,7 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level])
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index bc98eab133c..af1faca93f6 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -4,11 +4,11 @@ class ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(resource_name, resource)
if signed_in?(resource_name)
- signed_in_root_path(resource)
+ after_sign_in_path_for(resource)
else
sign_in(resource)
if signed_in?(resource_name)
- signed_in_root_path(resource)
+ after_sign_in_path_for(resource)
else
new_session_path(resource_name)
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index a73b8fa212a..469a6813ee2 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -13,7 +13,7 @@ class Groups::ApplicationController < ApplicationController
end
def authorize_admin_group!
- unless can?(current_user, :manage_group, group)
+ unless can?(current_user, :admin_group, group)
return render_404
end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 2df51c97a22..265cf4f0f4a 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -11,6 +11,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
+ @members = @members.non_invite unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -22,7 +23,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
- @group.add_users(params[:user_ids].split(','), params[:access_level])
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
@@ -38,7 +39,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner.
@group_member.destroy
respond_to do |format|
- format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
+ format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
format.js { render nothing: true }
end
else
@@ -46,12 +47,26 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
+ def resend_invite
+ redirect_path = group_group_members_path(@group)
+
+ @group_member = @group.group_members.find(params[:id])
+
+ if @group_member.invite?
+ @group_member.resend_invite
+
+ redirect_to redirect_path, notice: 'The invitation was successfully resent.'
+ else
+ redirect_to redirect_path, alert: 'The invitation has already been accepted.'
+ end
+ end
+
def leave
@group_member = @group.group_members.where(user_id: current_user.id).first
if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
- redirect_to(dashboard_groups_path, info: "You left #{group.name} group.")
+ redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else
return render_403
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index c46b8fff88f..546ff2cc71f 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -51,6 +51,6 @@ class Groups::MilestonesController < ApplicationController
end
def authorize_group_milestone!
- return render_404 unless can?(current_user, :manage_group, group)
+ return render_404 unless can?(current_user, :admin_group, group)
end
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
new file mode 100644
index 00000000000..1f97ff16c55
--- /dev/null
+++ b/app/controllers/invites_controller.rb
@@ -0,0 +1,83 @@
+class InvitesController < ApplicationController
+ before_filter :member
+ skip_before_filter :authenticate_user!, only: :decline
+
+ respond_to :html
+
+ layout 'navless'
+
+ def show
+
+ end
+
+ def accept
+ if member.accept_invite!(current_user)
+ label, path = source_info(member.source)
+
+ redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}."
+ else
+ redirect_to :back, alert: "The invitation could not be accepted."
+ end
+ end
+
+ def decline
+ if member.decline_invite!
+ label, _ = source_info(member.source)
+
+ path =
+ if current_user
+ dashboard_path
+ else
+ new_user_session_path
+ end
+
+ redirect_to path, notice: "You have declined the invitation to join #{label}."
+ else
+ redirect_to :back, alert: "The invitation could not be declined."
+ end
+ end
+
+ private
+
+ def member
+ return @member if defined?(@member)
+
+ @token = params[:id]
+ @member = Member.find_by_invite_token(@token)
+
+ unless @member
+ render_404 and return
+ end
+
+ @member
+ end
+
+ def authenticate_user!
+ return if current_user
+
+ notice = "To accept this invitation, sign in"
+ notice << " or create an account" if current_application_settings.signup_enabled?
+ notice << "."
+
+ store_location_for :user, request.fullpath
+ redirect_to new_user_session_path, notice: notice
+ end
+
+ def source_info(source)
+ case source
+ when Project
+ project = member.source
+ label = "project #{project.name_with_namespace}"
+ path = namespace_project_path(project.namespace, project)
+ when Group
+ group = member.source
+ label = "group #{group.name}"
+ path = group_path(group)
+ else
+ label = "who knows what"
+ path = dashboard_path
+ end
+
+ [label, path]
+ end
+end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 4ab15db01f7..72967a26ff1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -6,6 +6,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def index
@project_members = @project.project_members
+ @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
@@ -17,6 +18,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group = @project.group
if @group
@group_members = @group.group_members
+ @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -34,30 +36,42 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- users = User.where(id: params[:user_ids].split(','))
- @project.team << [users, params[:access_level]]
+ @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
def update
- @project_member = @project.project_members.find_by(user_id: member)
+ @project_member = @project.project_members.find(params[:id])
@project_member.update_attributes(member_params)
end
def destroy
- @project_member = @project.project_members.find_by(user_id: member)
+ @project_member = @project.project_members.find(params[:id])
@project_member.destroy
respond_to do |format|
format.html do
- redirect_to namespace_project_project_members_path(@project.namespace,
- @project)
+ redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
format.js { render nothing: true }
end
end
+ def resend_invite
+ redirect_path = namespace_project_project_members_path(@project.namespace, @project)
+
+ @project_member = @project.project_members.find(params[:id])
+
+ if @project_member.invite?
+ @project_member.resend_invite
+
+ redirect_to redirect_path, notice: 'The invitation was successfully resent.'
+ else
+ redirect_to redirect_path, alert: 'The invitation has already been accepted.'
+ end
+ end
+
def leave
@project.project_members.find_by(user_id: current_user).destroy
@@ -69,7 +83,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def apply_import
giver = Project.find(params[:source_project_id])
- status = @project.team.import(giver)
+ status = @project.team.import(giver, current_user)
notice = status ? "Successfully imported" : "Import failed"
redirect_to(namespace_project_project_members_path(project.namespace, project),
@@ -78,10 +92,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
- def member
- @member ||= User.find_by(username: params[:id])
- end
-
def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 2d0d0b494f6..add0a776a63 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,6 +1,10 @@
module GroupsHelper
- def remove_user_from_group_message(group, user)
- "Are you sure you want to remove \"#{user.name}\" from \"#{group.name}\"?"
+ def remove_user_from_group_message(group, member)
+ if member.user
+ "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
+ else
+ "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
+ end
end
def leave_group_message(group)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index ebbd2bfd77d..c2a7732e6f0 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,6 +1,10 @@
module ProjectsHelper
- def remove_from_project_team_message(project, user)
- "You are going to remove #{user.name} from #{project.name} project team. Are you sure?"
+ def remove_from_project_team_message(project, member)
+ if member.user
+ "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
+ else
+ "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
+ end
end
def link_to_project(project)
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 54e0f4f9b3e..bec8f2f1aa7 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -8,6 +8,7 @@ module SelectsHelper
null_user = opts[:null_user] || false
any_user = opts[:any_user] || false
+ email_user = opts[:email_user] || false
first_user = opts[:first_user] && current_user ? current_user.username : false
html = {
@@ -15,6 +16,7 @@ module SelectsHelper
'data-placeholder' => placeholder,
'data-null-user' => null_user,
'data-any-user' => any_user,
+ 'data-email-user' => email_user,
'data-first-user' => first_user
}
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
index 626eb593d51..1c43f95dc8c 100644
--- a/app/mailers/emails/groups.rb
+++ b/app/mailers/emails/groups.rb
@@ -3,10 +3,50 @@ module Emails
def group_access_granted_email(group_member_id)
@group_member = GroupMember.find(group_member_id)
@group = @group_member.group
+
@target_url = group_url(@group)
@current_user = @group_member.user
- mail(to: @group_member.user.email,
+
+ mail(to: @group_member.user.notification_email,
subject: subject("Access to group was granted"))
end
+
+ def group_member_invited_email(group_member_id, token)
+ @group_member = GroupMember.find group_member_id
+ @group = @group_member.group
+ @token = token
+
+ @target_url = group_url(@group)
+ @current_user = @group_member.user
+
+ mail(to: @group_member.invite_email,
+ subject: "Invitation to join group #{@group.name}")
+ end
+
+ def group_invite_accepted_email(group_member_id)
+ @group_member = GroupMember.find group_member_id
+ return if @group_member.created_by.nil?
+
+ @group = @group_member.group
+
+ @target_url = group_url(@group)
+ @current_user = @group_member.created_by
+
+ mail(to: @group_member.created_by.notification_email,
+ subject: subject("Invitation accepted"))
+ end
+
+ def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
+ return if created_by_id.nil?
+
+ @group = Group.find(group_id)
+ @current_user = @created_by = User.find(created_by_id)
+ @access_level = access_level
+ @invite_email = invite_email
+
+ @target_url = group_url(@group)
+ mail(to: @created_by.notification_email,
+ subject: subject("Invitation declined"))
+ end
end
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 20a863c3742..2584e9d48b1 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,14 +1,55 @@
module Emails
module Projects
- def project_access_granted_email(user_project_id)
- @project_member = ProjectMember.find user_project_id
+ def project_access_granted_email(project_member_id)
+ @project_member = ProjectMember.find project_member_id
@project = @project_member.project
+
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user
- mail(to: @project_member.user.email,
+
+ mail(to: @project_member.user.notification_email,
subject: subject("Access to project was granted"))
end
+ def project_member_invited_email(project_member_id, token)
+ @project_member = ProjectMember.find project_member_id
+ @project = @project_member.project
+ @token = token
+
+ @target_url = namespace_project_url(@project.namespace, @project)
+ @current_user = @project_member.user
+
+ mail(to: @project_member.invite_email,
+ subject: "Invitation to join project #{@project.name_with_namespace}")
+ end
+
+ def project_invite_accepted_email(project_member_id)
+ @project_member = ProjectMember.find project_member_id
+ return if @project_member.created_by.nil?
+
+ @project = @project_member.project
+
+ @target_url = namespace_project_url(@project.namespace, @project)
+ @current_user = @project_member.created_by
+
+ mail(to: @project_member.created_by.notification_email,
+ subject: subject("Invitation accepted"))
+ end
+
+ def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
+ return if created_by_id.nil?
+
+ @project = Project.find(project_id)
+ @current_user = @created_by = User.find(created_by_id)
+ @access_level = access_level
+ @invite_email = invite_email
+
+ @target_url = namespace_project_url(@project.namespace, @project)
+
+ mail(to: @created_by.notification_email,
+ subject: subject("Invitation declined"))
+ end
+
def project_was_moved_email(project_id, user_id)
@current_user = @user = User.find user_id
@project = Project.find project_id
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d2b39f667f2..85a15596f8d 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -198,11 +198,11 @@ class Ability
])
end
- # Only group owner and administrators can manage group
+ # Only group owner and administrators can admin group
if group.has_owner?(user) || user.admin?
rules.push(*[
- :manage_group,
- :manage_namespace
+ :admin_group,
+ :admin_namespace
])
end
@@ -212,11 +212,11 @@ class Ability
def namespace_abilities(user, namespace)
rules = []
- # Only namespace owner and administrators can manage it
+ # Only namespace owner and administrators can admin it
if namespace.owner == user || user.admin?
rules.push(*[
:create_projects,
- :manage_namespace
+ :admin_namespace
])
end
@@ -254,7 +254,7 @@ class Ability
rules = []
target_user = subject.user
group = subject.group
- can_manage = group_abilities(user, group).include?(:manage_group)
+ can_manage = group_abilities(user, group).include?(:admin_group)
if can_manage && (user != target_user)
rules << :modify_group_member
rules << :destroy_group_member
diff --git a/app/models/group.rb b/app/models/group.rb
index da9621a2a1a..1386a9eccc9 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -46,19 +46,18 @@ class Group < Namespace
@owners ||= group_members.owners.map(&:user)
end
- def add_users(user_ids, access_level)
- user_ids.compact.each do |user_id|
- user = self.group_members.find_or_initialize_by(user_id: user_id)
- user.update_attributes(access_level: access_level)
+ def add_users(user_ids, access_level, current_user = nil)
+ user_ids.each do |user_id|
+ Member.add_user(self.group_members, user_id, access_level, current_user)
end
end
- def add_user(user, access_level)
- self.group_members.create(user_id: user.id, access_level: access_level)
+ def add_user(user, access_level, current_user = nil)
+ add_users([user], access_level, current_user)
end
- def add_owner(user)
- self.add_user(user, Gitlab::Access::OWNER)
+ def add_owner(user, current_user = nil)
+ self.add_user(user, Gitlab::Access::OWNER, current_user)
end
def has_owner?(user)
diff --git a/app/models/member.rb b/app/models/member.rb
index fe3d2f40e87..d151c7b2390 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -11,6 +11,10 @@
# type :string(255)
# created_at :datetime
# updated_at :datetime
+# created_by_id :integer
+# invite_email :string
+# invite_token :string
+# invite_accepted_at :datetime
#
class Member < ActiveRecord::Base
@@ -18,19 +22,151 @@ class Member < ActiveRecord::Base
include Notifiable
include Gitlab::Access
+ attr_accessor :raw_invite_token
+
+ belongs_to :created_by, class_name: "User"
belongs_to :user
belongs_to :source, polymorphic: true
- validates :user, presence: true
+ validates :user, presence: true, unless: :invite?
validates :source, presence: true
- validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source" }
+ validates :user_id, uniqueness: { scope: [:source_type, :source_id],
+ message: "already exists in source",
+ allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validates :invite_email, presence: { if: :invite? },
+ email: { strict_mode: true, allow_nil: true },
+ uniqueness: { scope: [:source_type, :source_id], allow_nil: true }
+ scope :invite, -> { where(user_id: nil) }
+ scope :non_invite, -> { where("user_id IS NOT NULL") }
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) }
+ before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
+ after_create :send_invite, if: :invite?
+ after_create :post_create_hook, unless: :invite?
+ after_update :post_update_hook, unless: :invite?
+ after_destroy :post_destroy_hook, unless: :invite?
+
delegate :name, :username, :email, to: :user, prefix: true
+
+ class << self
+ def find_by_invite_token(invite_token)
+ invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
+ find_by(invite_token: invite_token)
+ end
+
+ # This method is used to find users that have been entered into the "Add members" field.
+ # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
+ def user_for_id(user_id)
+ return user_id if user_id.is_a?(User)
+
+ user = User.find_by(id: user_id)
+ user ||= User.find_by(email: user_id)
+ user ||= user_id
+ user
+ end
+
+ def add_user(members, user_id, access_level, current_user = nil)
+ user = user_for_id(user_id)
+
+ # `user` can be either a User object or an email to be invited
+ if user.is_a?(User)
+ member = members.find_or_initialize_by(user_id: user.id)
+ else
+ member = members.build
+ member.invite_email = user
+ end
+
+ member.created_by ||= current_user
+ member.access_level = access_level
+
+ member.save
+ end
+ end
+
+ def invite?
+ self.invite_token.present?
+ end
+
+ def accept_invite!(new_user)
+ return false unless invite?
+
+ self.invite_token = nil
+ self.invite_accepted_at = Time.now.utc
+
+ self.user = new_user
+
+ saved = self.save
+
+ after_accept_invite if saved
+
+ saved
+ end
+
+ def decline_invite!
+ return false unless invite?
+
+ destroyed = self.destroy
+
+ after_decline_invite if destroyed
+
+ destroyed
+ end
+
+ def generate_invite_token
+ raw, enc = Devise.token_generator.generate(self.class, :invite_token)
+ @raw_invite_token = raw
+ self.invite_token = enc
+ end
+
+ def generate_invite_token!
+ generate_invite_token && save(validate: false)
+ end
+
+ def resend_invite
+ return unless invite?
+
+ generate_invite_token! unless @raw_invite_token
+
+ send_invite
+ end
+
+ private
+
+ def send_invite
+ # override in subclass
+ end
+
+ def post_create_hook
+ system_hook_service.execute_hooks_for(self, :create)
+ end
+
+ def post_update_hook
+ # override in subclass
+ end
+
+ def post_destroy_hook
+ system_hook_service.execute_hooks_for(self, :destroy)
+ end
+
+ def after_accept_invite
+ post_create_hook
+ end
+
+ def after_decline_invite
+ # override in subclass
+ end
+
+ def system_hook_service
+ SystemHooksService.new
+ end
+
+ def notification_service
+ NotificationService.new
+ end
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 28d0b4483b4..84c91372b3f 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -27,10 +27,6 @@ class GroupMember < Member
scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
- after_create :post_create_hook
- after_update :notify_update
- after_destroy :post_destroy_hook
-
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -43,26 +39,37 @@ class GroupMember < Member
access_level
end
+ private
+
+ def send_invite
+ notification_service.invite_group_member(self, @raw_invite_token)
+
+ super
+ end
+
def post_create_hook
notification_service.new_group_member(self)
- system_hook_service.execute_hooks_for(self, :create)
+
+ super
end
- def notify_update
+ def post_update_hook
if access_level_changed?
notification_service.update_group_member(self)
end
- end
- def post_destroy_hook
- system_hook_service.execute_hooks_for(self, :destroy)
+ super
end
- def system_hook_service
- SystemHooksService.new
+ def after_accept_invite
+ notification_service.accept_group_invite(self)
+
+ super
end
- def notification_service
- NotificationService.new
+ def after_decline_invite
+ notification_service.decline_group_invite(self)
+
+ super
end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 6b13e0ff30b..0a3b4d2182b 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -27,10 +27,6 @@ class ProjectMember < Member
validates_format_of :source_type, with: /\AProject\z/
default_scope { where(source_type: SOURCE_TYPE) }
- after_create :post_create_hook
- after_update :post_update_hook
- after_destroy :post_destroy_hook
-
scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
scope :with_user, ->(user) { where(user_id: user.id) }
@@ -55,7 +51,7 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_into_projects(project_ids, user_ids, access)
+ def add_users_into_projects(project_ids, user_ids, access, current_user = nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
@@ -64,12 +60,14 @@ class ProjectMember < Member
raise "Non valid access"
end
+ users = user_ids.map { |user_id| Member.user_for_id(user_id) }
+
ProjectMember.transaction do
project_ids.each do |project_id|
- user_ids.each do |user_id|
- member = ProjectMember.new(access_level: access_level, user_id: user_id)
- member.source_id = project_id
- member.save
+ project = Project.find(project_id)
+
+ users.each do |user|
+ Member.add_user(project.project_members, user, access_level, current_user)
end
end
end
@@ -82,6 +80,7 @@ class ProjectMember < Member
def truncate_teams(project_ids)
ProjectMember.transaction do
members = ProjectMember.where(source_id: project_ids)
+
members.each do |member|
member.destroy
end
@@ -109,41 +108,58 @@ class ProjectMember < Member
access_level
end
+ def project
+ source
+ end
+
def owner?
project.owner == user
end
+ private
+
+ def send_invite
+ notification_service.invite_project_member(self, @raw_invite_token)
+
+ super
+ end
+
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
notification_service.new_project_member(self)
end
- system_hook_service.execute_hooks_for(self, :create)
+ super
end
def post_update_hook
- notification_service.update_project_member(self) if self.access_level_changed?
+ if access_level_changed?
+ notification_service.update_project_member(self)
+ end
+
+ super
end
def post_destroy_hook
event_service.leave_project(self.project, self.user)
- system_hook_service.execute_hooks_for(self, :destroy)
- end
- def event_service
- EventCreateService.new
+ super
end
- def notification_service
- NotificationService.new
+ def after_accept_invite
+ notification_service.accept_project_invite(self)
+
+ super
end
- def system_hook_service
- SystemHooksService.new
+ def after_decline_invite
+ notification_service.decline_project_invite(self)
+
+ super
end
- def project
- source
+ def event_service
+ EventCreateService.new
end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d4a07caf9ef..56e49af2324 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -12,12 +12,12 @@ class ProjectTeam
# @team << [@users, :master]
#
def <<(args)
- users = args.first
+ users, access, current_user = *args
if users.respond_to?(:each)
- add_users(users, args.second)
+ add_users(users, access, current_user)
else
- add_user(users, args.second)
+ add_user(users, access, current_user)
end
end
@@ -43,22 +43,19 @@ class ProjectTeam
member
end
- def add_user(user, access)
- add_users_ids([user.id], access)
- end
-
- def add_users(users, access)
- add_users_ids(users.map(&:id), access)
- end
-
- def add_users_ids(user_ids, access)
+ def add_users(users, access, current_user = nil)
ProjectMember.add_users_into_projects(
[project.id],
- user_ids,
- access
+ users,
+ access,
+ current_user
)
end
+ def add_user(user, access, current_user = nil)
+ add_users([user], access, current_user)
+ end
+
# Remove all users from project team
def truncate
ProjectMember.truncate_team(project)
@@ -88,7 +85,7 @@ class ProjectTeam
@masters ||= fetch_members(:masters)
end
- def import(source_project)
+ def import(source_project, current_user = nil)
target_project = project
source_members = source_project.project_members.to_a
@@ -96,13 +93,14 @@ class ProjectTeam
source_members.reject! do |member|
# Skip if user already present in team
- target_user_ids.include?(member.user_id)
+ !member.invite? && target_user_ids.include?(member.user_id)
end
source_members.map! do |member|
new_member = member.dup
new_member.id = nil
new_member.source = target_project
+ new_member.created_by = current_user
new_member
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 42547f6f481..203e654c18f 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -165,6 +165,18 @@ class NotificationService
end
end
+ def invite_project_member(project_member, token)
+ mailer.project_member_invited_email(project_member.id, token)
+ end
+
+ def accept_project_invite(project_member)
+ mailer.project_invite_accepted_email(project_member.id)
+ end
+
+ def decline_project_invite(project_member)
+ mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id)
+ end
+
def new_project_member(project_member)
mailer.project_access_granted_email(project_member.id)
end
@@ -173,6 +185,18 @@ class NotificationService
mailer.project_access_granted_email(project_member.id)
end
+ def invite_group_member(group_member, token)
+ mailer.group_member_invited_email(group_member.id, token)
+ end
+
+ def accept_group_invite(group_member)
+ mailer.group_invite_accepted_email(group_member.id)
+ end
+
+ def decline_group_invite(group_member)
+ mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id)
+ end
+
def new_group_member(group_member)
mailer.group_access_granted_email(group_member.id)
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 7ffd0b3882a..a7afcf8f64b 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -83,7 +83,7 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create)
unless @project.group
- @project.team << [current_user, :master]
+ @project.team << [current_user, :master, current_user]
end
@project.update_column(:last_activity_at, @project.created_at)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 4ec98696a65..1e4deb6ed39 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -38,7 +38,7 @@ module Projects
#First save the DB entries as they can be rolled back if the repo fork fails
project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id)
if project.save
- project.team << [@current_user, :master]
+ project.team << [@current_user, :master, @current_user]
end
#Now fork the repo
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 7d292118075..14996dcd6a2 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -60,7 +60,7 @@
= form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
- = users_select_tag(:user_ids, multiple: true)
+ = users_select_tag(:user_ids, multiple: true, email_user: true)
%div.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
@@ -74,13 +74,18 @@
%ul.well-list.group-users-list
- @members.each do |member|
- user = member.user
- %li{class: dom_class(member), id: dom_id(user)}
+ %li{class: dom_class(member), id: (dom_id(user) if user)}
.list-item-name
- %strong
- = link_to user.name, admin_user_path(user)
+ - if user
+ %strong
+ = link_to user.name, admin_user_path(user)
+ - else
+ %strong
+ = member.invite_email
+ (invited)
%span.pull-right.light
= member.human_access
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse
.panel-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index b0b23132560..78684c692c7 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -124,14 +124,19 @@
- user = project_member.user
%li.project_member
.list-item-name
- %strong
- = link_to user.name, admin_user_path(user)
+ - if user
+ %strong
+ = link_to user.name, admin_user_path(user)
+ - else
+ %strong
+ = project_member.invite_email
+ (invited)
.pull-right
- if project_member.owner?
%span.light Owner
- else
%span.light= project_member.human_access
- = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
+ = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
%i.fa.fa-times
.panel-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 0a2934d3bda..3524f04c5ed 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -182,7 +182,7 @@
.pull-right
%span.light= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
- else
.nothing-here-block This user has no groups.
@@ -221,7 +221,7 @@
%span.light= member.human_access
- if member.respond_to? :project
- = link_to namespace_project_project_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times
#ssh-keys.tab-pane
= render 'profiles/keys/key_table', admin: true
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 165db214d75..0cb7f764fab 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -17,7 +17,7 @@
- group = group_member.group
%li
.pull-right
- - if can?(current_user, :manage_group, group)
+ - if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do
%i.fa.fa-cogs
Settings
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
index 2462c952090..56b1948a474 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,17 +1,32 @@
- user = member.user
-- return unless user
+- return unless user || member.invite?
- show_roles = true if show_roles.nil?
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)}
- = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
- %strong= user.name
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
+ - if member.user
+ = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
+ %strong= user.name
+ %span.cgray= user.username
+ - if user == current_user
+ %span.label.label-success It's you
+ - if user.blocked?
+ %label.label.label-danger
+ %strong Blocked
+ - else
+ = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ %strong
+ = member.invite_email
+ %span.cgray
+ invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_path(member.created_by)
+ = time_ago_with_tooltip(member.created_at)
+
+ - if show_controls && can?(current_user, :admin_group, @group)
+ = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
+ Resend invite
- if show_roles
%span.pull-right
@@ -27,7 +42,7 @@
= link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse
- else
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse
.edit-member.hide.js-toggle-content
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index a52b8197384..3361d7e2a8d 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -1,7 +1,10 @@
= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
.form-group
= f.label :user_ids, "People", class: 'control-label'
- .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all)
+ .col-sm-10
+ = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
+ .help-block
+ Search for existing users or invite new ones using their email address.
.form-group
= f.label :access_level, "Group Access", class: 'control-label'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 0d501fe7bd3..c0c9cd170ad 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -16,7 +16,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' }
= button_tag 'Search', class: 'btn'
- - if current_user && current_user.can?(:manage_group, @group)
+ - if current_user && current_user.can?(:admin_group, @group)
.pull-right
= button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
Add members
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index 94fc43a581e..30093d2d05d 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -1,6 +1,6 @@
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
.pull-right
- - if can?(current_user, :manage_group, @group)
+ - if can?(current_user, :admin_group, @group)
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index fea70f5cbc3..fb32f2caa4c 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -6,7 +6,7 @@
Open
Milestone #{@group_milestone.title}
.pull-right
- - if can?(current_user, :manage_group, @group)
+ - if can?(current_user, :admin_group, @group)
- if @group_milestone.active?
= link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
- else
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index dd1fa3840d5..0d547984cc9 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -2,7 +2,7 @@
.panel-heading
%strong= @group.name
projects:
- - if can? current_user, :manage_group, @group
+ - if can? current_user, :admin_group, @group
.panel-head-actions
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
%i.fa.fa-plus
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
new file mode 100644
index 00000000000..ab0ecffe4d2
--- /dev/null
+++ b/app/views/invites/show.html.haml
@@ -0,0 +1,29 @@
+%h3.page-title Invitation
+
+%p
+ You have been invited
+ - if inviter = @member.created_by
+ by
+ = link_to inviter.name, user_url(inviter)
+ to join
+ - case @member.source
+ - when Project
+ - project = @member.source
+ project
+ %strong
+ = link_to project.name_with_namespace, namespace_project_url(project.namespace, project)
+ - when Group
+ - group = @member.source
+ group
+ %strong
+ = link_to group.name, group_url(group)
+ as #{@member.human_access}.
+
+- if @member.source.users.include?(current_user)
+ %p
+ However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
+ Sign in using a different account to accept the invitation.
+- else
+ .actions
+ = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
+ = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml
index b1c2e1a7b19..d58582c107a 100644
--- a/app/views/layouts/_head_panel.html.haml
+++ b/app/views/layouts/_head_panel.html.haml
@@ -39,7 +39,7 @@
= link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do
%i.fa.fa-user
%li
- = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do
+ = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Sign out", class: 'has_bottom_tooltip', 'data-original-title' => 'Sign out' do
%i.fa.fa-sign-out
%li.hidden-xs
= link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 32fe0e37df8..f0d92b7a12c 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -30,7 +30,7 @@
%span
Members
- - if can?(current_user, :manage_group, @group)
+ - if can?(current_user, :admin_group, @group)
= nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do
= link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do
%i.fa.fa-cogs
diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml
new file mode 100644
index 00000000000..55efad384a7
--- /dev/null
+++ b/app/views/notify/group_invite_accepted_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ #{@group_member.invite_email}, now known as
+ #{link_to @group_member.user.name, user_url(@group_member.user)},
+ has accepted your invitation to join group
+ #{link_to @group.name, group_url(@group)}.
+
diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb
new file mode 100644
index 00000000000..f8b70f7a5a6
--- /dev/null
+++ b/app/views/notify/group_invite_accepted_email.text.erb
@@ -0,0 +1,3 @@
+<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
+
+<%= group_url(@group) %>
diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml
new file mode 100644
index 00000000000..f9525d84fac
--- /dev/null
+++ b/app/views/notify/group_invite_declined_email.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{@invite_email}
+ has declined your invitation to join group
+ #{link_to @group.name, group_url(@group)}.
+
diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb
new file mode 100644
index 00000000000..6c19a288d15
--- /dev/null
+++ b/app/views/notify/group_invite_declined_email.text.erb
@@ -0,0 +1,3 @@
+<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
+
+<%= group_url(@group) %>
diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml
new file mode 100644
index 00000000000..163e88bfea3
--- /dev/null
+++ b/app/views/notify/group_member_invited_email.html.haml
@@ -0,0 +1,14 @@
+%p
+ You have been invited
+ - if inviter = @group_member.created_by
+ by
+ = link_to inviter.name, user_url(inviter)
+ to join group
+ = link_to @group.name, group_url(@group)
+ as #{@group_member.human_access}.
+
+%p
+ = link_to 'Accept invitation', invite_url(@token)
+ or
+ = link_to 'decline', decline_invite_url(@token)
+
diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb
new file mode 100644
index 00000000000..28ce4819b14
--- /dev/null
+++ b/app/views/notify/group_member_invited_email.text.erb
@@ -0,0 +1,4 @@
+You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>.
+
+Accept invitation: <%= invite_url(@token) %>
+Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml
new file mode 100644
index 00000000000..7e58d30b10a
--- /dev/null
+++ b/app/views/notify/project_invite_accepted_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ #{@project_member.invite_email}, now known as
+ #{link_to @project_member.user.name, user_url(@project_member.user)},
+ has accepted your invitation to join project
+ #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
+
diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb
new file mode 100644
index 00000000000..fcbe752114d
--- /dev/null
+++ b/app/views/notify/project_invite_accepted_email.text.erb
@@ -0,0 +1,3 @@
+<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>.
+
+<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml
new file mode 100644
index 00000000000..c2d7e6f6e3a
--- /dev/null
+++ b/app/views/notify/project_invite_declined_email.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{@invite_email}
+ has declined your invitation to join project
+ #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
+
diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb
new file mode 100644
index 00000000000..484687fa51c
--- /dev/null
+++ b/app/views/notify/project_invite_declined_email.text.erb
@@ -0,0 +1,3 @@
+<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
+
+<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml
new file mode 100644
index 00000000000..79eb89616de
--- /dev/null
+++ b/app/views/notify/project_member_invited_email.html.haml
@@ -0,0 +1,13 @@
+%p
+ You have been invited
+ - if inviter = @project_member.created_by
+ by
+ = link_to inviter.name, user_url(inviter)
+ to join project
+ = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)
+ as #{@project_member.human_access}.
+
+%p
+ = link_to 'Accept invitation', invite_url(@token)
+ or
+ = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb
new file mode 100644
index 00000000000..e0706272115
--- /dev/null
+++ b/app/views/notify/project_member_invited_email.text.erb
@@ -0,0 +1,4 @@
+You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>.
+
+Accept invitation: <%= invite_url(@token) %>
+Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 5daae2708e6..d708b01a114 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,7 +1,10 @@
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f|
.form-group
= f.label :user_ids, "People", class: 'control-label'
- .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all)
+ .col-sm-10
+ = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
+ .help-block
+ Search for existing users or invite new ones using their email address.
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
index 4f053977215..635e4d70941 100644
--- a/app/views/projects/project_members/_project_member.html.haml
+++ b/app/views/projects/project_members/_project_member.html.haml
@@ -1,16 +1,32 @@
- user = member.user
-- return unless user
+- return unless user || member.invite?
%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
%span.list-item-name
- = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
- %strong= user.name
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
+ - if member.user
+ = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
+ %strong
+ = link_to user.name, user_path(user)
+ %span.cgray= user.username
+ - if user == current_user
+ %span.label.label-success It's you
+ - if user.blocked?
+ %label.label.label-danger
+ %strong Blocked
+ - else
+ = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ %strong
+ = member.invite_email
+ %span.cgray
+ invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_path(member.created_by)
+ = time_ago_with_tooltip(member.created_at)
+
+ - if current_user_can_admin_project
+ = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
+ Resend invite
- if current_user_can_admin_project
- unless @project.personal? && user == current_user
@@ -25,12 +41,12 @@
= link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
%i.fa.fa-minus.fa-inverse
- else
- = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
+ = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
%i.fa.fa-minus.fa-inverse
.edit-member.hide.js-toggle-content
%br
- = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f|
+ = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
.prepend-top-10
= f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
.prepend-top-10