Commit d8563bd6 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents e7a8fe07 bf4455d1
......@@ -9,6 +9,8 @@ v 8.9.0 (unreleased)
- Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
- Fix issue with arrow keys not working in search autocomplete dropdown
- Fix an issue where note polling stopped working if a window was in the
background during a refresh.
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository
- Don't show tags for revert and cherry-pick operations
......@@ -25,6 +27,7 @@ v 8.9.0 (unreleased)
- Redesign navigation for project pages
- Fix groups API to list only user's accessible projects
- Redesign account and email confirmation emails
- Don't fail builds for projects that are deleted
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0
......@@ -57,6 +60,7 @@ v 8.9.0 (unreleased)
- Use Knapsack only in CI environment
- Cache project build count in sidebar nav
- Add milestone expire date to the right sidebar
- Manually mark a issue or merge request as a todo
- Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
- Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects
......@@ -80,6 +84,13 @@ v 8.9.0 (unreleased)
- Measure CPU time for instrumented methods
- Instrument private methods and private instance methods by default instead just public methods
- Only show notes through JSON on confidential issues that the user has access to
- Updated the allocations Gem to version 1.0.5
- The background sampler now ignores classes without names
- Update design for `Close` buttons
- New custom icons for navigation
- Horizontally scrolling navigation on project, group, and profile settings pages
- Hide global side navigation by default
- Remove tanuki logo from side navigation; center on top nav
v 8.8.5 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds
......
......@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.4)
allocations (1.0.5)
arel (6.0.3)
asana (0.4.0)
faraday (~> 0.9)
......
......@@ -56,13 +56,6 @@ issuable_created = false
Issuable.filterResults $('.filter-form')
$('.js-label-select').trigger('update.label')
toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0
$filteredLabels.removeClass('hidden')
else
$filteredLabels.addClass('hidden')
filterResults: (form) =>
formData = form.serialize()
......@@ -71,58 +64,16 @@ issuable_created = false
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax
type: 'GET'
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issuable.reload()
Issuable.updateStateFilters()
$filteredLabels = $('.filtered-labels')
if typeof Issuable.labelRow is 'function'
$filteredLabels.html(Issuable.labelRow(data))
Issuable.toggleLabelFilters()
dataType: "json"
reload: ->
if Issuable.created
Issuable.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
Turbolinks.visit(issuesUrl);
initChecks: ->
$('.check_all_issues').on 'click', ->
$('.check_all_issues').off('click').on('click', ->
$('.selected_issue').prop('checked', @checked)
Issuable.checkChanged()
)
$('.selected_issue').on 'change', Issuable.checkChanged
updateStateFilters: ->
stateFilters = $('.issues-state-filters, .dropdown-menu-sort')
newParams = {}
paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
labelNameValues = gl.utils.getParameterValues('label_name[]')
if labelNameValues
labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
else
newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
$(this).attr 'href', newUrl
$('.selected_issue').off('change').on('change', Issuable.checkChanged)
checkChanged: ->
checked_issues = $('.selected_issue:checked')
......
......@@ -9,6 +9,9 @@ class @IssuableBulkActions
@bindEvents()
# Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
getElement: (selector) ->
@container.find selector
......
class @LayoutNav
$ ->
$('.fade-left').addClass('end-scroll')
$('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this)
$el = $(event.target)
currentPosition = $this.scrollLeft()
size = bp.getBreakpointSize()
controlBtnWidth = $('.controls').width()
maxPosition = $this.get(0).scrollWidth - $this.parent().width()
maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length
$el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
hideEndFade = ($scrollingTabs) ->
$scrollingTabs.each ->
$this = $(@)
$this
.find('.fade-right')
.toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
$ ->
$('.fade-left').addClass('end-scroll')
hideEndFade($('.scrolling-tabs'))
$(window)
.off 'resize.nav'
.on 'resize.nav', ->
hideEndFade($('.scrolling-tabs'))
$('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this)
currentPosition = $this.scrollLeft()
maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
$this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
......@@ -115,12 +115,14 @@ class @Notes
, @pollingInterval
refresh: =>
return if @refreshing is true
@refreshing = true
if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
getContent: ->
return if @refreshing
@refreshing = true
$.ajax
url: @notes_url
data: "last_fetched_at=" + @last_fetched_at
......
......@@ -43,6 +43,55 @@ class @Sidebar
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
$(document)
.off 'click', '.js-issuable-todo'
.on 'click', '.js-issuable-todo', @toggleTodo
toggleTodo: (e) =>
$this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this)
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
$.ajax(
url: "#{$this.data('url')}#{ajaxUrlExtra}"
type: ajaxType
dataType: 'json'
data:
issuable_id: $this.data('issuable')
issuable_type: $this.data('issuable-type')
beforeSend: =>
@beforeTodoSend($this, $todoLoading)
).done (data) =>
@todoUpdateDone(data, $this, $btnText, $todoLoading)
beforeTodoSend: ($btn, $todoLoading) ->
$btn.disable()
$todoLoading.removeClass 'hidden'
todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
$todoPendingCount = $('.todos-pending-count')
$todoPendingCount.text data.count
$btn.enable()
$todoLoading.addClass 'hidden'
if data.count is 0
$todoPendingCount.addClass 'hidden'
else
$todoPendingCount.removeClass 'hidden'
if data.todo?
$btn
.attr 'aria-label', $btn.data('mark-text')
.attr 'data-id', data.todo.id
$btnText.text $btn.data('mark-text')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-id'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
......@@ -117,5 +166,3 @@ class @Sidebar
getBlock: (name) ->
@sidebar.find(".block.#{name}")
......@@ -280,11 +280,10 @@
}
.dropdown {
margin-left: 7px;
@media (max-width: $screen-xs-min) {
margin-left: 0;
}
position: absolute;
top: 7px;
right: 15px;
z-index: 2;
li.active {
font-weight: bold;
......
......@@ -83,6 +83,12 @@
margin-top: 10px;
}
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
}
a {
width: $sidebar_width;
padding: 7px 15px 7px 23px;
......
......@@ -39,3 +39,20 @@
}
}
}
.groups-cover-block {
.container-fluid {
position: relative;
}
.access-request-button {
@include btn-gray;
position: absolute;
right: 16px;
bottom: 32px;
padding: 3px 10px;
text-transform: none;
background-color: $background-color;
}
}
......@@ -34,6 +34,10 @@
color: inherit;
}
.issuable-header-text {
margin-top: 7px;
}
.block {
@include clearfix;
padding: $gl-padding 0;
......@@ -60,10 +64,6 @@
margin-top: 0;
}
.issuable-count {
margin-top: 7px;
}
.gutter-toggle {
margin-left: 20px;
padding-left: 10px;
......@@ -250,7 +250,7 @@
}
}
.issuable-pager {
.issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
......@@ -263,7 +263,7 @@
}
}
a:not(.issuable-pager) {
a {
&:hover {
color: $md-link-color;
text-decoration: none;
......
......@@ -229,13 +229,20 @@
right: 16px;
bottom: 0;
.btn {
padding: 3px 10px;
background-color: $background-color;
@media (max-width: $screen-lg-min) {
top: 0;
}
@media (max-width: 1304px) {
top: 0;
.access-request-button {
position: absolute;
right: 0;
bottom: 61px;
@media (max-width: $screen-lg-min) {
position: relative;
bottom: 0;
margin-right: 10px;
}
}
}
......@@ -286,10 +293,6 @@
color: #555;
}
.project_member_row form {
margin: 0;
}
.transfer-project .select2-container {
min-width: 200px;
}
......
module MembershipActions
extend ActiveSupport::Concern
include MembersHelper
def request_access
membershipable.request_access(current_user)
redirect_to polymorphic_path(membershipable),
notice: 'Your request for access has been queued for review.'
end
def approve_access_request
@member = membershipable.members.request.find(params[:id])
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
@member.accept_request
redirect_to polymorphic_url([membershipable, :members])
end
def leave
@member = membershipable.members.find_by(user_id: current_user)
return render_403 unless @member
source_type = @member.real_source_type.humanize(capitalize: false)
if can?(current_user, action_member_permission(:destroy, @member), @member)
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
@member.destroy
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
else
if cannot_leave?
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
alert << " Transfer or delete the #{source_type}."
redirect_to polymorphic_url(membershipable), alert: alert
else
render_403
end
end
end
protected
def membershipable
raise NotImplementedError
end
def cannot_leave?
raise NotImplementedError
end
end
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave]
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
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)
@members = @members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
def leave
@group_member = @group.group_members.find_by(user_id: current_user)
if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else
if @group.last_owner?(current_user)
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
else
return render_403
end
end
end
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
# Authorize
before_action :authorize_admin_project_member!, except: [:leave, :index]
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
@project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
......@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC')
@group = @project.group
if @group
@group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
@group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
def leave
@project_member = @project.project_members.find_by(user_id: current_user)
if can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy
respond_to do |format|
format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
format.js { head :ok }
end
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
else
render_403
end
end
end
def apply_import
source_project = Project.find(params[:source_project_id])
......@@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
end
class Projects::TodosController < Projects::ApplicationController
def create
todos = TodoService.new.mark_todo(issuable, current_user)
render json: {
todo: todos,
count: current_user.todos.pending.count,
}
end
def update
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: {
count: current_user.todos.pending.count,
}
end
private
def issuable
@issuable ||= begin
case params[:issuable_type]
when "issue"
@project.issues.find(params[:issuable_id])
when "merge_request"
@project.merge_requests.find(params[:issuable_id])
end
end
end
end
......@@ -36,7 +36,7 @@ class TodosFinder
private
def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
end
def action_id
......
......@@ -13,10 +13,23 @@
# merge_request_path(merge_request)
#
module GitlabRoutingHelper
# Project
def project_path(project, *args)
namespace_project_path(project.namespace, project, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end
......@@ -41,10 +54,6 @@ module GitlabRoutingHelper
activity_namespace_project_path(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def runners_path(project, *args)
namespace_project_runners_path(project.namespace, project, *args)
end
......@@ -65,14 +74,6 @@ module GitlabRoutingHelper
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end
......@@ -92,4 +93,56 @@ module GitlabRoutingHelper
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end
end
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
end
def project_member_path(project_member, *args)
namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def request_access_project_members_path(project, *args)
request_access_namespace_project_project_members_path(project.namespace, project)
end
def leave_project_members_path(project, *args)
leave_namespace_project_project_members_path(project.namespace, project)
end
def approve_access_request_project_member_path(project_member, *args)
approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def resend_invite_project_member_path(project_member, *args)
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
# Groups
## Members
def group_members_url(group, *args)
group_group_members_url(group, *args)
end
def group_member_path(group_member, *args)
group_group_member_path(group_member.source, group_member)
end
def request_access_group_members_path(group, *args)
request_access_group_group_members_path(group)
end
def leave_group_members_path(group, *args)
leave_group_group_members_path(group)
end
def approve_access_request_group_member_path(group_member, *args)
approve_access_request_group_group_member_path(group_member.source, group_member)
end
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
end
end
module GroupsHelper
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)
"Are you sure you want to leave \"#{group}\" group?"
end
def should_user_see_group_roles?(user, group)
if user
user.is_admin? || group.members.exists?(user_id: user.id)
else
false
end
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
......
......@@ -67,6 +67,12 @@ module IssuablesHelper
end
end
def has_todo(issuable)
unless current_user.nil?
current_user.todos.find_by(target_id: issuable.id, state: :pending)
end
end
private
def sidebar_gutter_collapsed?
......
module MembersHelper
# Returns a `<action>_<source>_member` association, e.g.:
# - admin_project_member, update_project_member, destroy_project_member
# - admin_group_member, update_group_member, destroy_group_member
def action_member_permission(action, member)
"#{action}_#{member.type.underscore}".to_sym
end
def can_see_member_roles?(source:, user: nil)
return false unless user
user.is_admin? || source.members.exists?(user_id: user.id)
end
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
text = 'Are you sure you want to '
action =
if member.request?
if member.user == user
'withdraw your access request for'
else
"deny #{member.user.name}'s request to join"
end
elsif member.invite?
"revoke the invitation for #{member.invite_email} to join"
else
"remove #{member.user.name} from"
end
text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
end
def remove_member_title(member)
text = " from #{member.real_source_type.humanize(capitalize: false)}"
text.prepend(member.request? ? 'Deny access request' : 'Remove user')
end
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
end
module ProjectsHelper
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)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
......@@ -115,14 +107,6 @@ module ProjectsHelper
end
end
def user_max_access_in_project(user_id, project)
level = project.team.max_member_access(user_id)
if level
Gitlab::Access.options_with_owner.key(level)
end
end
def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil?
......@@ -286,10 +270,6 @@ module ProjectsHelper
end
end
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
def new_readme_path
ref = @repository.root_ref if @repository
ref ||= 'master'
......
......@@ -12,6 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'marked this as a Todo for'
end
end
......
module Emails
module Groups
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.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
module Emails
module Members
extend ActiveSupport::Concern
include MembersHelper
included do
helper_method :member_source, :member
end
def member_access_requested_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
mail(to: admins,
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_access_granted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
mail(to: member.user.notification_email,
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
end
def member_access_denied_email(member_source_type, source_id, user_id)
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
requester = User.find(user_id)
mail(to: requester.notification_email,
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
end
def member_invited_email(member_source_type, member_id, token)
@member_source_type = member_source_type
@member_id = member_id
@token = token
mail(to: member.invite_email,
subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
end
def member_invite_accepted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
return unless member.created_by
mail(to: member.created_by.notification_email,
subject: subject('Invitation accepted'))
end
def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
return unless created_by_id
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
@invite_email = invite_email
inviter = User.find(created_by_id)
mail(to: inviter.notification_email,
subject: subject('Invitation declined'))
end
def member
@member ||= Member.find(@member_id)
end
def member_source
@member_source ||= member.source
end
private
def member_source_class
@member_source_type.classify.constantize
end
end
end
module Emails
module Projects
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.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, old_path_with_namespace)
@current_user = @user = User.find user_id
@project = Project.find project_id
......
......@@ -6,13 +6,15 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
include Emails::Groups
include Emails::Builds
include Emails::Members
add_template_helper MergeRequestsHelper
add_template_helper DiffHelper
add_template_helper BlobHelper
add_template_helper EmailsHelper
add_template_helper MembersHelper
add_template_helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
......
......@@ -187,6 +187,8 @@ class Ability
project_report_rules
elsif team.guest?(user)
project_guest_rules
else
[]
end
end
......
# == AccessRequestable concern
#
# Contains functionality related to objects that can receive request for access.
#
# Used by Project, and Group.
#
module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
members.create(
access_level: Gitlab::Access::DEVELOPER,
user: user,
requested_at: Time.now.utc)
end
end
......@@ -3,11 +3,12 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
include AccessRequestable
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
......@@ -58,6 +59,10 @@ class Group < Namespace
"#{self.class.reference_prefix}#{name}"
end
def web_url
Gitlab::Routing.url_helpers.group_url(self)
end
def human_name
name
end
......
......@@ -26,20 +26,28 @@ class Member < ActiveRecord::Base
allow_nil: true
}
scope :invite, -> { where(user_id: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") }
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
scope :non_pending, -> { non_request.non_invite }
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) }
scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?
after_create :create_notification_setting, unless: :invite?
after_create :post_create_hook, unless: :invite?
after_update :post_update_hook, unless: :invite?
after_destroy :post_destroy_hook, unless: :invite?
after_create :send_request, if: :request?
after_create :create_notification_setting, unless: :pending?
after_create :post_create_hook, unless: :pending?
after_update :post_update_hook, unless: :pending?
after_destroy :post_destroy_hook, unless: :pending?
after_destroy :post_decline_request, if: :request?
delegate :name, :username, :email, to: :user, prefix: true
......@@ -96,10 +104,31 @@ class Member < ActiveRecord::Base
end
end
def real_source_type
source_type
end
def invite?
self.invite_token.present?
end
def request?
requested_at.present?
end
def pending?
invite? || request?
end
def accept_request
return false unless request?
updated = self.update(requested_at: nil)
after_accept_request if updated
updated
end
def accept_invite!(new_user)
return false unless invite?
......@@ -157,6 +186,10 @@ class Member < ActiveRecord::Base
# override in subclass
end
def send_request
# override in subclass
end
def post_create_hook
system_hook_service.execute_hooks_for(self, :create)
end
......@@ -177,6 +210,14 @@ class Member < ActiveRecord::Base
# override in subclass
end
def after_accept_request
post_create_hook
end
def post_decline_request
# override in subclass
end
def system_hook_service
SystemHooksService.new
end
......
......@@ -8,9 +8,6 @@ class GroupMember < Member
validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) }
scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
def self.access_level_roles
Gitlab::Access.options_with_owner
end
......@@ -23,6 +20,11 @@ class GroupMember < Member
access_level
end
# Because source_type is `Namespace`...
def real_source_type
'Group'
end
private
def send_invite
......@@ -31,6 +33,12 @@ class GroupMember < Member
super
end
def send_request
notification_service.new_group_access_request(self)
super
end
def post_create_hook
notification_service.new_group_member(self)
......@@ -56,4 +64,10 @@ class GroupMember < Member
super
end
def post_decline_request
notification_service.decline_group_access_request(self)
super
end
end
......@@ -11,8 +11,6 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
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) }
before_destroy :delete_member_todos
......@@ -84,7 +82,7 @@ class ProjectMember < Member
Gitlab::Access.sym_options
end
def access_roles
def access_level_roles
Gitlab::Access.options
end
end
......@@ -113,6 +111,12 @@ class ProjectMember < Member
super
end
def send_request
notification_service.new_project_access_request(self)
super
end
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
......@@ -148,6 +152,12 @@ class ProjectMember < Member
super
end
def post_decline_request
notification_service.decline_project_access_request(self)
super
end
def event_service
EventCreateService.new
end
......
......@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
include AccessRequestable
include Referable
include Sortable
include AfterCommitQueue
......@@ -102,8 +103,9 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
has_many :users, through: :project_members
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
alias_method :members, :project_members
has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members
has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
......@@ -680,16 +682,6 @@ class Project < ActiveRecord::Base
end
end
def project_member_by_name_or_email(name = nil, email = nil)
user = users.find_by('name like ? or email like ?', name, email)
project_members.where(user: user) if user
end
# Get Team Member record by user id
def project_member_by_id(user_id)
project_members.find_by(user_id: user_id)
end
def name_with_namespace
@name_with_namespace ||= begin
if namespace
......@@ -699,6 +691,7 @@ class Project < ActiveRecord::Base
end
end
end
alias_method :human_name, :name_with_namespace
def path_with_namespace
if namespace
......
......@@ -21,23 +21,13 @@ class ProjectTeam
end
end
def find(user_id)
user = project.users.find_by(id: user_id)
if group
user ||= group.users.find_by(id: user_id)
end
user
end
def find_member(user_id)
member = project.project_members.find_by(user_id: user_id)
member = project.members.non_request.find_by(user_id: user_id)
# If user is not in project members
# we should check for group membership
if group && !member
member = group.group_members.find_by(user_id: user_id)
member = group.members.non_request.find_by(user_id: user_id)
end
member
......@@ -61,13 +51,10 @@ class ProjectTeam
ProjectMember.truncate_team(project)
end
def users
members
end
def members
@members ||= fetch_members
end
alias_method :users, :members
def guests
@guests ||= fetch_members(:guests)
......@@ -150,7 +137,7 @@ class ProjectTeam
def max_member_access(user_id)
access = []
project.project_members.each do |member|
project.members.non_request.each do |member|
if member.user_id == user_id
access << member.access_field if member.access_field
break
......@@ -158,7 +145,7 @@ class ProjectTeam
end
if group
group.group_members.each do |member|
group.members.non_request.each do |member|
if member.user_id == user_id
access << member.access_field if member.access_field
break
......@@ -173,6 +160,7 @@ class ProjectTeam
access.compact.max
end
private
def max_invited_level(user_id)
project.project_group_links.map do |group_link|
......@@ -189,17 +177,15 @@ class ProjectTeam
end.compact.max
end
private
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
project_members = project.members.non_request
group_members = group ? group.members.non_request : []
invited_members = []
if project.invited_groups.any? && project.allowed_to_share_with_group?
project.project_group_links.each do |group_link|
invited_group = group_link.group
im = invited_group.group_members
im = invited_group.members.non_request
if level
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
......
......@@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
MARKED = 4
belongs_to :author, class_name: "User"
belongs_to :note
......
......@@ -56,8 +56,7 @@ class User < ActiveRecord::Base
# Groups
has_many :members, dependent: :destroy
has_many :project_members, source: 'ProjectMember'
has_many :group_members, source: 'GroupMember'
has_many :group_members, dependent: :destroy, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
......@@ -65,13 +64,13 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
......
......@@ -173,16 +173,26 @@ class NotificationService
end
end
# Project access request
def new_project_access_request(project_member)
mailer.member_access_requested_email(project_member.real_source_type, project_member.id).deliver_later
end
def decline_project_access_request(project_member)
mailer.member_access_denied_email(project_member.real_source_type, project_member.project.id, project_member.user.id).deliver_later
end
def invite_project_member(project_member, token)
mailer.project_member_invited_email(project_member.id, token).deliver_later
mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end
def accept_project_invite(project_member)
mailer.project_invite_accepted_email(project_member.id).deliver_later
mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
end
def decline_project_invite(project_member)
mailer.project_invite_declined_email(
mailer.member_invite_declined_email(
project_member.real_source_type,
project_member.project.id,
project_member.invite_email,
project_member.access_level,
......@@ -191,23 +201,33 @@ class NotificationService
end
def new_project_member(project_member)
mailer.project_access_granted_email(project_member.id).deliver_later
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
def update_project_member(project_member)
mailer.project_access_granted_email(project_member.id).deliver_later
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
# Group access request
def new_group_access_request(group_member)
mailer.member_access_requested_email(group_member.real_source_type, group_member.id).deliver_later
end
def decline_group_access_request(group_member)
mailer.member_access_denied_email(group_member.real_source_type, group_member.group.id, group_member.user.id).deliver_later
end
def invite_group_member(group_member, token)
mailer.group_member_invited_email(group_member.id, token).deliver_later
mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
end
def accept_group_invite(group_member)
mailer.group_invite_accepted_email(group_member.id).deliver_later
mailer.member_invite_accepted_email(group_member.id).deliver_later
end
def decline_group_invite(group_member)
mailer.group_invite_declined_email(
mailer.member_invite_declined_email(
group_member.real_source_type,
group_member.group.id,
group_member.invite_email,
group_member.access_level,
......@@ -216,11 +236,11 @@ class NotificationService
end
def new_group_member(group_member)
mailer.group_access_granted_email(group_member.id).deliver_later
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def update_group_member(group_member)
mailer.group_access_granted_email(group_member.id).deliver_later
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def project_was_moved(project, old_path_with_namespace)
......
......@@ -139,10 +139,16 @@ class TodoService
pending_todos(user, attributes).update_all(state: :done)
end
# When user marks an issue as todo
def mark_todo(issuable, current_user)
attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED)
create_todos(current_user, attributes)
end
private
def create_todos(users, attributes)
Array(users).each do |user|
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
Todo.create(attributes.merge(user_id: user.id))
end
......
......@@ -109,7 +109,7 @@
%span.pull-right.light
= member.human_access
- if can?(current_user, :destroy_group_member, member)
= 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
= link_to group_group_member_path(@group, member), data: { confirm: remove_member_message(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'
......@@ -142,7 +142,7 @@
%i.fa.fa-pencil-square-o
%ul.well-list
- @group_members.each do |member|
= render 'groups/group_members/group_member', member: member, show_controls: false
= render 'shared/members/member', member: member, show_controls: false
.panel-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
......@@ -172,7 +172,7 @@
%span.light Owner
- else
%span.light= project_member.human_access
= 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
= link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_member_message(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'
......@@ -13,7 +13,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, group_member) }, 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_member_message(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.
......@@ -38,6 +38,5 @@
%span.light= member.human_access
- if member.respond_to? :project
= 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
= link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times
......@@ -6,12 +6,13 @@
.panel-heading
Add new user to group
.panel-body
- if should_user_see_group_roles?(current_user, @group)
%p.light
Members of group have access to all group projects.
%p.light
Members of group have access to all group projects.
.new-group-member-holder
= render "new_group_member"
= render 'shared/members/requests', membership_source: @group, members: @members.request
.panel.panel-default
.panel-heading
%strong #{@group.name}
......@@ -25,9 +26,8 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- @members.each do |member|
= render 'groups/group_members/group_member', member: member, show_controls: true
= paginate @members, theme: 'gitlab'
= render partial: 'shared/members/member', collection: @members.non_request, as: :member
= paginate @members.non_request, theme: 'gitlab'
:javascript
$('form.member-search-form').on('submit', function(event) {
......
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}');
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member))}');
......@@ -19,6 +19,9 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
- if current_user
= render 'shared/members/access_request_buttons', source: @group
%div{ class: container_class }
.top-area
%ul.nav-links
......
......@@ -27,9 +27,8 @@
%li
= link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw')
- unless todos_pending_count == 0
%span.badge.todos-pending-count
= todos_pending_count
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
= todos_pending_count
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
......
%ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
= navbar_icon('project')
.icon-container
= navbar_icon('project')
%span
Projects
= nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do
= icon('bell fw')
.icon-container
= icon('bell fw')
%span
Todos
%span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
= navbar_icon('activity')
.icon-container
= navbar_icon('activity')
%span
Activity
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
= navbar_icon('group')
.icon-container
= navbar_icon('group')
%span
Groups
= nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
= navbar_icon('milestones')
.icon-container
= navbar_icon('milestones')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
= navbar_icon('issues')
.icon-container
= navbar_icon('issues')
%span
Issues
%span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
= navbar_icon('mr')
.icon-container
= navbar_icon('mr')
%span
Merge Requests
%span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
.icon-container
= icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
= icon('question-circle fw')
.icon-container
= icon('question-circle fw')
%span
Help
= nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw')
.icon-container
= icon('user fw')
%span
Profile Settings
......@@ -14,7 +14,3 @@
%li
= link_to edit_group_path(@group) do
Edit Group
%li
= link_to leave_group_group_members_path(@group),
data: { confirm: leave_group_message(@group.name) }, method: :delete, title: 'Leave group' do
Leave Group
- if current_user
- access = user_max_access_in_project(current_user.id, @project)
- can_edit = can?(current_user, :admin_project, @project)
.controls
.dropdown.project-settings-dropdown
%a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- access = @project.team.max_member_access(current_user.id)
- can_edit = can?(current_user, :admin_project, @project)
= render 'layouts/nav/project_settings', access: access, can_edit: can_edit
- if can_edit || access
%li.divider
- if can_edit
......@@ -16,8 +18,8 @@
Edit Project
- if access
%li
= link_to leave_namespace_project_project_members_path(@project.namespace, @project),
data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project
%div{ class: nav_control_class }
......
%p
= "You have been granted #{@group_member.human_access} access to group"
= link_to group_url(@group) do
= @group.name
You have been granted <%= @group_member.human_access %> access to group <%= @group.name %>
<%= url_for(group_url(@group)) %>
%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)}.
<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
<%= group_url(@group) %>
%p
#{@invite_email}
has declined your invitation to join group
#{link_to @group.name, group_url(@group)}.
<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
<%= group_url(@group) %>
%p
Your request to join the
#{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}
has been denied.
Your request to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> has been denied.
<%= member_source.web_url %>
%p
You have been granted #{member.human_access} access to the
#{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
You have been granted <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= member_source.web_url %>
%p
#{link_to member.user.name, member.user} requested #{member.human_access}
access to the #{link_to member_source.human_name, polymorphic_url([member_source, :members])} #{member_source.model_name.singular}.
<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= polymorphic_url([member_source, :members]) %>
%p
#{member.invite_email}, now known as
#{link_to member.user.name, user_url(member.user)},
has accepted your invitation to join the
#{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= member_source.web_url %>
%p
#{@invite_email}
has declined your invitation to join the
#{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
<%= @invite_email %> has declined your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= member_source.web_url %>
%p
You have been invited
- if inviter = @group_member.created_by
- if 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}.
= link_to member.created_by.name, user_url(member.created_by)
to join the
= link_to member_source.human_name, member_source.web_url
#{member_source.model_name.singular} as #{member.human_access}.
%p
= link_to 'Accept invitation', invite_url(@token)
or
= link_to 'decline', decline_invite_url(@token)
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 %>.
You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_invite_url(@token) %>
%p
= "You have been granted #{@project_member.human_access} access to project"
%p
= link_to namespace_project_url(@project.namespace, @project) do
= @project.name_with_namespace
You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %>
<%= url_for(namespace_project_url(@project.namespace, @project)) %>
%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)}.
<%= @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) %>
%p
#{@invite_email}
has declined your invitation to join project
#{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
<%= namespace_project_url(@project.namespace, @project) %>
%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)
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) %>
......@@ -29,10 +29,13 @@
.project-clone-holder
= render "shared/clone_panel"
.project-repo-buttons.btn-group.project-right-buttons
= render "projects/buttons/download"
= render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications'
.project-repo-buttons.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
.btn-group
= render "projects/buttons/download"
= render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications'
:javascript
new Star();
- if @notification_setting
= form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
= f.hidden_field :level
.dropdown
.dropdown.hidden-sm
%button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } }
= icon('bell')
= notification_title(@notification_setting.level)
......
......@@ -6,11 +6,14 @@
(#{members.count})
- if can?(current_user, :admin_group_member, @group)
.controls
= link_to group_group_members_path(@group), class: 'btn' do
Manage group members
= link_to 'Manage group members',
group_group_members_path(@group),
class: 'btn'
%ul.content-list
- members.limit(20).each do |member|
= render 'groups/group_members/group_member', member: member, show_controls: false
- if members.count > 20
= render partial: 'shared/members/member',
collection: members.limit(20),
as: :member,
locals: { show_controls: false }
- if members.size > 20
%li
and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
......@@ -9,7 +9,7 @@
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
.col-sm-10
= select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2"
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
......
- user = member.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
- if member.user
= image_tag avatar_icon(user, 24), class: "avatar s24", 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, 24), class: "avatar s24", 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 can?(current_user, :admin_project_member, @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 can?(current_user, :admin_project_member, @project)
.pull-right
%strong= member.human_access
- if can?(current_user, :update_project_member, member)
= button_tag class: "btn-xs btn-grouped inline btn js-toggle-button",
title: 'Edit access level', type: 'button' do
= icon('pencil')
- if can?(current_user, :destroy_project_member, member)
&nbsp;
- if current_user == user
= link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
= icon("sign-out")
Leave
- else
= 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
= icon('trash')
.edit-member.hide.js-toggle-content
%br
= 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
= f.submit 'Save', class: 'btn btn-save'
......@@ -14,8 +14,10 @@
%i.fa.fa-pencil-square-o
Edit group members
%ul.content-list
- shared_group.group_members.order('access_level DESC').limit(20).each do |member|
= render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
= render partial: 'shared/members/member',
collection: shared_group.group_members.order(access_level: :desc).limit(20),
as: :member,
locals: { show_controls: false, show_roles: false }
- if shared_group_users_count > 20
%li
and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
......@@ -11,8 +11,7 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- members.each do |project_member|
= render 'project_member', member: project_member
= render partial: 'shared/members/member', collection: members, as: :member
:javascript
$('form.member-search-form').on('submit', function (event) {
......
......@@ -13,7 +13,9 @@
Users with access to this project are listed below.
= render "new_project_member"
= render "team", members: @project_members
= render 'shared/members/requests', membership_source: @project, members: @project_members.request
= render 'team', members: @project_members.non_request
- if @group
= render "group_members", members: @group_members
......
......@@ -9,7 +9,7 @@
= link_to edit_group_path(group), class: "btn" do
= icon('cogs')
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do
= icon('sign-out')
.stats
......
- todo = has_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
%a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
- if current_user
%span.issuable-header-text.hide-collapsed.pull-left
Todo
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon
- if current_user
%button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", id: (todo.id unless todo.nil?), issuable: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project) } }
%span.js-issuable-todo-text
- if todo.nil?
Add Todo
- else
Mark Done
= icon('spin spinner', class: 'hidden js-issuable-todo-loading')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
......
- member = source.members.find_by(user_id: current_user.id)
- if member
- if member.request?
= link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn access-request-button hidden-xs'
- else
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn access-request-button hidden-xs'
- user = member.user
- return unless user || member.invite?
- show_roles = local_assigns.fetch(:show_roles, true)
- show_controls = local_assigns.fetch(:show_controls, true)
- user = member.user
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)}
- if member.user
%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
%span{ class: ("list-item-name" if show_controls) }
- if user
= image_tag avatar_icon(user, 24), class: "avatar s24", 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
- if member.request?
%span.cgray
– Requested
= time_ago_with_tooltip(member.requested_at)
- else
= image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
%strong
= member.invite_email
%strong= member.invite_email
%span.cgray
invited
– 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_member, @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_controls && can?(current_user, action_member_permission(:admin, member), member.source)
= link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
method: :post,
class: 'btn-xs btn'
- if show_roles && should_user_see_group_roles?(current_user, @group)
- if show_roles && can_see_member_roles?(source: member.source, user: current_user)
%span.pull-right
%strong.member-access-level= member.human_access
%strong= member.human_access
- if show_controls
- if can?(current_user, :update_group_member, member)
= button_tag class: "btn-xs btn btn-grouped inline js-toggle-button",
title: 'Edit access level', type: 'button' do
= icon('pencil')
- if can?(current_user, action_member_permission(:update, member), member)
= button_tag icon('pencil'),
type: 'button',
class: 'btn-xs btn btn-grouped inline js-toggle-button',
title: 'Edit access level'
- if member.request?
&nbsp;
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
method: :post,
class: 'btn-xs btn btn-success',
title: 'Grant access'
- if can?(current_user, :destroy_group_member, member)
- if can?(current_user, action_member_permission(:destroy, member), member)
&nbsp;
- if current_user == user
= 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
= icon("sign-out")
Leave
= link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(member.source) },
class: 'btn-xs btn btn-remove'
- else
= 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
= icon('trash')
= link_to icon('trash'), member,
remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn-xs btn btn-remove',
title: remove_member_title(member)
.edit-member.hide.js-toggle-content
%br
= form_for [@group, member], remote: true do |f|
= form_for member, remote: true do |f|
.prepend-top-10
= f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control'
= f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
.prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm'
- if members.any?
.panel.panel-default
.panel-heading
%strong= membership_source.name
access requests
%small= "(#{members.size})"
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
......@@ -6,7 +6,7 @@ class StuckCiBuildsWorker
def perform
Rails.logger.info 'Cleaning stuck builds'
builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
builds.find_each(batch_size: 50).each do |build|
Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
build.drop
......
......@@ -30,6 +30,11 @@ Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
end
concern :access_requestable do
post :request_access, on: :collection
post :approve_access_request, on: :member
end
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
......@@ -409,7 +414,7 @@ Rails.application.routes.draw do
end
scope module: :groups do
resources :group_members, only: [:index, :create, :update, :destroy] do
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
end
......@@ -766,7 +771,7 @@ Rails.application.routes.draw do
end
end
resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do
resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
collection do
delete :leave
......@@ -790,6 +795,8 @@ Rails.application.routes.draw do
end
end
resources :todos, only: [:create, :update], constraints: { id: /\d+/ }
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
......
class AddRequestedAtToMembers < ActiveRecord::Migration
def change
add_column :members, :requested_at, :datetime
end
end
......@@ -537,6 +537,7 @@ ActiveRecord::Schema.define(version: 20160610301627) do
t.string "invite_email"
t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
......
......@@ -147,7 +147,8 @@ bt
To output a backtrace from all threads at once:
```
apply all thread bt
set pagination off
thread apply all bt
```
Once you're done debugging with `gdb`, be sure to detach from the process and
......
......@@ -62,6 +62,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
end
step 'I should see the "Can not leave message"' do
expect(page).to have_content "You can not leave Owned group because you're the last owner"
expect(page).to have_content "You can not leave the \"Owned\" group."
end
end
......@@ -53,7 +53,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
page.within '.content-list' do
expect(page).to have_content('sjobs@apple.com')
expect(page).to have_content('invited')
expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
......@@ -116,11 +116,9 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
find(".js-toggle-button").click
page.within "#edit_group_member_#{member.id}" do
select 'Developer', from: 'group_member_access_level'
click_on 'Save'
end
click_button "Edit access level"
select 'Developer', from: 'group_member_access_level'
click_on 'Save'
end
end
......@@ -128,9 +126,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
page.within '.member-access-level' do
expect(page).to have_content "Developer"
end
expect(page).to have_content "Developer"
end
end
......
......@@ -26,8 +26,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "Mike" in team list as "Reporter"' do
page.within ".access-reporter" do
user = User.find_by(name: 'Mike')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Mike')
expect(page).to have_content('Reporter')
end
end
......@@ -40,16 +43,20 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
page.within ".access-reporter" do
project_member = project.project_members.find_by(invite_email: 'sjobs@apple.com')
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('sjobs@apple.com')
expect(page).to have_content('invited')
expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
step 'I should see "Dmitriy" in team list as "Developer"' do
page.within ".access-developer" do
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
expect(page).to have_content('Developer')
end
end
......@@ -65,15 +72,14 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "Dmitriy" in team list as "Reporter"' do
page.within ".access-reporter" do
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
expect(page).to have_content('Reporter')
end
end
step 'I click link "Remove from team"' do
click_link "Remove from team"
end
step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy")
expect(page).not_to have_content(user.name)
......@@ -120,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
click_link('Remove user from team')
click_link('Remove user from project')
end
end
......
......@@ -88,10 +88,7 @@ module API
class Group < Grape::Entity
expose :id, :name, :path, :description, :visibility_level
expose :avatar_url
expose :web_url do |group, options|
Gitlab::Routing.url_helpers.group_url(group)
end
expose :web_url
end
class GroupDetail < Group
......
......@@ -46,7 +46,7 @@ module API
required_attributes! [:user_id, :access_level]
# either the user is already a team member or a new one
project_member = user_project.project_member_by_id(params[:user_id])
project_member = user_project.project_member(params[:user_id])
if project_member.nil?
project_member = user_project.project_members.new(
user_id: params[:user_id],
......
......@@ -66,7 +66,11 @@ module Gitlab
def sample_objects
sample = Allocations.to_hash
counts = sample.each_with_object({}) do |(klass, count), hash|
hash[klass.name] = count
name = klass.name
next unless name
hash[name] = count
end
# Symbols aren't allocated so we'll need to add those manually.
......
......@@ -4,17 +4,211 @@ describe Groups::GroupMembersController do
let(:user) { create(:user) }
let(:group) { create(:group) }
context "index" do
describe '#index' do
before do
group.add_owner(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'renders index with group members' do
get :index, group_id: group.path
get :index, group_id: group
expect(response.status).to eq(200)
expect(response).to render_template(:index)
end
end
describe '#destroy' do
let(:group) { create(:group, :public) }
context 'when member is not found' do
it 'returns 403' do
delete :destroy, group_id: group,
id: 42
expect(response.status).to eq(403)
end
end
context 'when member is found' do
let(:user) { create(:user) }
let(:group_user) { create(:user) }
let(:member) do
group.add_developer(group_user)
group.members.find_by(user_id: group_user)
end
context 'when user does not have enough rights' do
before do
group.add_developer(user)
sign_in(user)
end
it 'returns 403' do
delete :destroy, group_id: group,
id: member
expect(response.status).to eq(403)
expect(group.users).to include group_user
end
end
context 'when user has enough rights' do
before do
group.add_owner(user)
sign_in(user)
end
it '[HTML] removes user from members' do
delete :destroy, group_id: group,
id: member
expect(response).to set_flash.to 'User was successfully removed from group.'
expect(response).to redirect_to(group_group_members_path(group))
expect(group.users).not_to include group_user
end
it '[JS] removes user from members' do
xhr :delete, :destroy, group_id: group,
id: member
expect(response).to be_success
expect(group.users).not_to include group_user
end
end
end
end
describe '#leave' do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
context 'when member is not found' do
before { sign_in(user) }
it 'returns 403' do
delete :leave, group_id: group
expect(response.status).to eq(403)
end
end
context 'when member is found' do
context 'and is not an owner' do
before do
group.add_developer(user)
sign_in(user)
end
it 'removes user from members' do
delete :leave, group_id: group
expect(response).to set_flash.to "You left the \"#{group.name}\" group."
expect(response).to redirect_to(dashboard_groups_path)
expect(group.users).not_to include user
end
end
context 'and is an owner' do
before do
group.add_owner(user)
sign_in(user)
end
it 'cannot removes himself from the group' do
delete :leave, group_id: group
expect(response).to redirect_to(group_path(group))
expect(response).to set_flash[:alert].to "You can not leave the \"#{group.name}\" group. Transfer or delete the group."
expect(group.users).to include user
end
end
context 'and is a requester' do
before do
group.request_access(user)
sign_in(user)
end
it 'removes user from members' do
delete :leave, group_id: group
expect(response).to set_flash.to 'Your access request to the group has been withdrawn.'
expect(response).to redirect_to(dashboard_groups_path)
expect(group.members.request).to be_empty
expect(group.users).not_to include user
end
end
end
end
describe '#request_access' do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
before do
sign_in(user)
end
it 'creates a new GroupMember that is not a team member' do
post :request_access, group_id: group
expect(response).to set_flash.to 'Your request for access has been queued for review.'
expect(response).to redirect_to(group_path(group))
expect(group.members.request.exists?(user_id: user)).to be_truthy
expect(group.users).not_to include user
end
end
describe '#approve_access_request' do
let(:group) { create(:group, :public) }
context 'when member is not found' do
it 'returns 403' do
post :approve_access_request, group_id: group,
id: 42
expect(response.status).to eq(403)
end
end
context 'when member is found' do
let(:user) { create(:user) }
let(:group_requester) { create(:user) }
let(:member) do
group.request_access(group_requester)
group.members.request.find_by(user_id: group_requester)
end
context 'when user does not have enough rights' do
before do
group.add_developer(user)
sign_in(user)
end
it 'returns 403' do
post :approve_access_request, group_id: group,
id: member
expect(response.status).to eq(403)
expect(group.users).not_to include group_requester
end
end
context 'when user has enough rights' do
before do
group.add_owner(user)
sign_in(user)
end
it 'adds user to members' do
post :approve_access_request, group_id: group,
id: member
expect(response).to redirect_to(group_group_members_path(group))
expect(group.users).to include group_requester
end
end
end
end
end
require('spec_helper')
describe Projects::ProjectMembersController do
let(:project) { create(:project) }
let(:another_project) { create(:project, :private) }
let(:user) { create(:user) }
let(:member) { create(:user) }
before do
project.team << [user, :master]
another_project.team << [member, :guest]
sign_in(user)
end
describe '#apply_import' do
let(:project) { create(:project) }
let(:another_project) { create(:project, :private) }
let(:user) { create(:user) }
let(:member) { create(:user) }
before do
project.team << [user, :master]
another_project.team << [member, :guest]
sign_in(user)
end
shared_context 'import applied' do
before do
post(:apply_import, namespace_id: project.namespace.to_param,
project_id: project.to_param,
post(:apply_import, namespace_id: project.namespace,
project_id: project,
source_project_id: another_project.id)
end
end
......@@ -48,18 +48,231 @@ describe Projects::ProjectMembersController do
end
describe '#index' do
let(:project) { create(:project, :private) }
context 'when user is member' do
let(:member) { create(:user) }
before do
project = create(:project, :private)
member = create(:user)
project.team << [member, :guest]
sign_in(member)
get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
get :index, namespace_id: project.namespace, project_id: project
end
it { expect(response.status).to eq(200) }
end
end
describe '#destroy' do
let(:project) { create(:project, :public) }
context 'when member is not found' do
it 'returns 404' do
delete :destroy, namespace_id: project.namespace,
project_id: project,
id: 42
expect(response.status).to eq(404)
end
end
context 'when member is found' do
let(:user) { create(:user) }
let(:team_user) { create(:user) }
let(:member) do
project.team << [team_user, :developer]
project.members.find_by(user_id: team_user.id)
end
context 'when user does not have enough rights' do
before do
project.team << [user, :developer]
sign_in(user)
end
it 'returns 404' do
delete :destroy, namespace_id: project.namespace,
project_id: project,
id: member
expect(response.status).to eq(404)
expect(project.users).to include team_user
end
end
context 'when user has enough rights' do
before do
project.team << [user, :master]
sign_in(user)
end
it '[HTML] removes user from members' do
delete :destroy, namespace_id: project.namespace,
project_id: project,
id: member
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
)
expect(project.users).not_to include team_user
end
it '[JS] removes user from members' do
xhr :delete, :destroy, namespace_id: project.namespace,
project_id: project,
id: member
expect(response).to be_success
expect(project.users).not_to include team_user
end
end
end
end
describe '#leave' do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
context 'when member is not found' do
before { sign_in(user) }
it 'returns 403' do
delete :leave, namespace_id: project.namespace,
project_id: project
expect(response.status).to eq(403)
end
end
context 'when member is found' do
context 'and is not an owner' do
before do
project.team << [user, :developer]
sign_in(user)
end
it 'removes user from members' do
delete :leave, namespace_id: project.namespace,
project_id: project
expect(response).to set_flash.to "You left the \"#{project.human_name}\" project."
expect(response).to redirect_to(dashboard_projects_path)
expect(project.users).not_to include user
end
end
context 'and is an owner' do
before do
project.update(namespace_id: user.namespace_id)
project.team << [user, :master, user]
sign_in(user)
end
it 'cannot remove himself from the project' do
delete :leave, namespace_id: project.namespace,
project_id: project
expect(response).to redirect_to(
namespace_project_path(project.namespace, project)
)
expect(response).to set_flash[:alert].to "You can not leave the \"#{project.human_name}\" project. Transfer or delete the project."
expect(project.users).to include user
end
end
context 'and is a requester' do
before do
project.request_access(user)
sign_in(user)
end
it 'removes user from members' do
delete :leave, namespace_id: project.namespace,
project_id: project
expect(response).to set_flash.to 'Your access request to the project has been withdrawn.'
expect(response).to redirect_to(dashboard_projects_path)
expect(project.members.request).to be_empty
expect(project.users).not_to include user
end
end
end
end
describe '#request_access' do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
before do
sign_in(user)
end
it 'creates a new ProjectMember that is not a team member' do
post :request_access, namespace_id: project.namespace,
project_id: project
expect(response).to set_flash.to 'Your request for access has been queued for review.'
expect(response).to redirect_to(
namespace_project_path(project.namespace, project)
)
expect(project.members.request.exists?(user_id: user)).to be_truthy
expect(project.users).not_to include user
end
end
describe '#approve' do
let(:project) { create(:project, :public) }
context 'when member is not found' do
it 'returns 404' do
post :approve_access_request, namespace_id: project.namespace,
project_id: project,
id: 42
expect(response.status).to eq(404)
end
end
context 'when member is found' do
let(:user) { create(:user) }
let(:team_requester) { create(:user) }
let(:member) do
project.request_access(team_requester)
project.members.request.find_by(user_id: team_requester.id)
end
context 'when user does not have enough rights' do
before do
project.team << [user, :developer]
sign_in(user)
end
it 'returns 404' do
post :approve_access_request, namespace_id: project.namespace,
project_id: project,
id: member
expect(response.status).to eq(404)
expect(project.users).not_to include team_requester
end
end
context 'when user has enough rights' do
before do
project.team << [user, :master]
sign_in(user)
end
it 'adds user to members' do
post :approve_access_request, namespace_id: project.namespace,
project_id: project,
id: member
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
)
expect(project.users).to include team_requester
end
end
end
end
end
require 'spec_helper'
feature 'Groups > Members > Owner manages access requests', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
background do
group.request_access(user)
group.add_owner(owner)
login_as(owner)
end
scenario 'owner can see access requests' do
visit group_group_members_path(group)
expect_visible_access_request(group, user)
end
scenario 'master can grant access' do
visit group_group_members_path(group)
expect_visible_access_request(group, user)
perform_enqueued_jobs { click_on 'Grant access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted"
end
scenario 'master can deny access' do
visit group_group_members_path(group)
expect_visible_access_request(group, user)
perform_enqueued_jobs { click_on 'Deny access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was denied"
end
def expect_visible_access_request(group, user)
expect(group.members.request.exists?(user_id: user)).to be_truthy
expect(page).to have_content "#{group.name} access requests (1)"
expect(page).to have_content user.name
end
end
require 'spec_helper'
feature 'Groups > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
background do
group.add_owner(owner)
login_as(user)
visit group_path(group)
end
scenario 'user can request access to a group' do
perform_enqueued_jobs { click_link 'Request Access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [owner.notification_email]
expect(ActionMailer::Base.deliveries.last.subject).to match "Request to join the #{group.name} group"
expect(group.members.request.exists?(user_id: user)).to be_truthy
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
end
scenario 'user is not listed in the group members page' do
click_link 'Request Access'
expect(group.members.request.exists?(user_id: user)).to be_truthy
click_link 'Members'
page.within('.content') do
expect(page).not_to have_content(user.name)
end
end
scenario 'user can withdraw its request for access' do
click_link 'Request Access'
expect(group.members.request.exists?(user_id: user)).to be_truthy
click_link 'Withdraw Access Request'
expect(group.members.request.exists?(user_id: user)).to be_falsey
expect(page).to have_content 'Your access request to the group has been withdrawn.'
end
end
......@@ -56,8 +56,9 @@ feature 'Issue filtering by Labels', feature: true do
end
it 'should remove label "bug"' do
first('.js-label-filter-remove').click
expect(find('.filtered-labels')).to have_no_content "bug"
find('.js-label-filter-remove').click
wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end
end
......@@ -142,7 +143,8 @@ feature 'Issue filtering by Labels', feature: true do
end
it 'should remove label "enhancement"' do
first('.js-label-filter-remove').click
find('.js-label-filter-remove', match: :first).click
wait_for_ajax
expect(find('.filtered-labels')).to have_no_content "enhancement"
end
end
......@@ -179,6 +181,7 @@ feature 'Issue filtering by Labels', feature: true do
before do
page.within '.labels-filter' do
click_button 'Label'
wait_for_ajax
click_link 'bug'
find('.dropdown-menu-close').click
end
......@@ -189,14 +192,11 @@ feature 'Issue filtering by Labels', feature: true do
end
it 'should allow user to remove filtered labels' do
page.within '.filtered-labels' do
first('.js-label-filter-remove').click
expect(page).not_to have_content 'bug'
end
first('.js-label-filter-remove').click
wait_for_ajax
page.within '.labels-filter' do
expect(page).not_to have_content 'bug'
end
expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
expect(find('.labels-filter')).not_to have_content 'bug'
end
end
......
require 'rails_helper'
describe 'Filter issues', feature: true do
include WaitForAjax
let!(:project) { create(:project) }
let!(:user) { create(:user)}
......@@ -21,7 +22,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
sleep 2
wait_for_ajax
end
context 'assignee', js: true do
......@@ -53,7 +54,7 @@ describe 'Filter issues', feature: true do
find('.milestone-filter .dropdown-content a', text: milestone.title).click
sleep 2
wait_for_ajax
end
context 'milestone', js: true do
......@@ -80,23 +81,21 @@ describe 'Filter issues', feature: true do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click
wait_for_ajax
end
it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
wait_for_ajax
page.within '.labels-filter' do
expect(page).to have_content 'Any Label'
end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label')
expect(find('.labels-filter')).to have_content 'Label'
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
wait_for_ajax
page.within '.labels-filter' do
expect(page).to have_content 'No Label'
......@@ -122,14 +121,14 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
sleep 2
wait_for_ajax
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
wait_for_ajax
end
context 'assignee and label', js: true do
......@@ -276,9 +275,12 @@ describe 'Filter issues', feature: true do
it 'should be able to filter and sort issues' do
click_button 'Label'
wait_for_ajax
page.within '.labels-filter' do
click_link 'bug'
end
find('.dropdown-menu-close-icon').click
wait_for_ajax
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
......@@ -288,6 +290,7 @@ describe 'Filter issues', feature: true do
page.within '.dropdown-menu-sort' do
click_link 'Oldest created'
end
wait_for_ajax
page.within '.issues-list' do
expect(first('.issue')).to have_content('Frontend')
......
require 'rails_helper'
feature 'Manually create a todo item from issue', feature: true, js: true do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
project.team << [user, :master]
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'should create todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
expect(page).to have_content 'Mark Done'
end
page.within '.header-content .todos-pending-count' do
expect(page).to have_content '1'
end
end
it 'should mark a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
click_button 'Mark Done'
end
expect(page).to have_selector('.todos-pending-count', visible: false)
end
end
require 'spec_helper'
feature 'Projects > Members > Master manages access requests', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
let(:project) { create(:project, :public) }
background do
project.request_access(user)
project.team << [master, :master]
login_as(master)
end
scenario 'master can see access requests' do
visit namespace_project_project_members_path(project.namespace, project)
expect_visible_access_request(project, user)
end
scenario 'master can grant access' do
visit namespace_project_project_members_path(project.namespace, project)
expect_visible_access_request(project, user)
perform_enqueued_jobs { click_on 'Grant access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted"
end
scenario 'master can deny access' do
visit namespace_project_project_members_path(project.namespace, project)
expect_visible_access_request(project, user)
perform_enqueued_jobs { click_on 'Deny access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied"
end
def expect_visible_access_request(project, user)
expect(project.members.request.exists?(user_id: user)).to be_truthy
expect(page).to have_content "#{project.name} access requests (1)"
expect(page).to have_content user.name
end
end
require 'spec_helper'
feature 'Projects > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
let(:project) { create(:project, :public) }
background do
project.team << [master, :master]
login_as(user)
visit namespace_project_path(project.namespace, project)
end
scenario 'user can request access to a project' do
perform_enqueued_jobs { click_link 'Request Access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project"
expect(project.members.request.exists?(user_id: user)).to be_truthy
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
end
scenario 'user is not listed in the project members page' do
click_link 'Request Access'
expect(project.members.request.exists?(user_id: user)).to be_truthy
open_project_settings_menu
click_link 'Members'
visit namespace_project_project_members_path(project.namespace, project)
page.within('.content') do
expect(page).not_to have_content(user.name)
end
end
scenario 'user can withdraw its request for access' do
click_link 'Request Access'
expect(project.members.request.exists?(user_id: user)).to be_truthy
click_link 'Withdraw Access Request'
expect(project.members.request.exists?(user_id: user)).to be_falsey
expect(page).to have_content 'Your access request to the project has been withdrawn.'
end
def open_project_settings_menu
find('#project-settings-button').click
end
end
require 'spec_helper'
describe GitlabRoutingHelper do
describe 'Project URL helpers' do
describe '#project_members_url' do
let(:project) { build_stubbed(:empty_project) }
it { expect(project_members_url(project)).to eq namespace_project_project_members_url(project.namespace, project) }
end
describe '#project_member_path' do
let(:project_member) { create(:project_member) }
it { expect(project_member_path(project_member)).to eq namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
end
describe '#request_access_project_members_path' do
let(:project) { build_stubbed(:empty_project) }
it { expect(request_access_project_members_path(project)).to eq request_access_namespace_project_project_members_path(project.namespace, project) }
end
describe '#leave_project_members_path' do
let(:project) { build_stubbed(:empty_project) }
it { expect(leave_project_members_path(project)).to eq leave_namespace_project_project_members_path(project.namespace, project) }
end
describe '#approve_access_request_project_member_path' do
let(:project_member) { create(:project_member) }
it { expect(approve_access_request_project_member_path(project_member)).to eq approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
end
describe '#resend_invite_project_member_path' do
let(:project_member) { create(:project_member) }
it { expect(resend_invite_project_member_path(project_member)).to eq resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
end
end
describe 'Group URL helpers' do
describe '#group_members_url' do
let(:group) { build_stubbed(:group) }
it { expect(group_members_url(group)).to eq group_group_members_url(group) }
end
describe '#group_member_path' do
let(:group_member) { create(:group_member) }
it { expect(group_member_path(group_member)).to eq group_group_member_path(group_member.source, group_member) }
end
describe '#request_access_group_members_path' do
let(:group) { build_stubbed(:group) }
it { expect(request_access_group_members_path(group)).to eq request_access_group_group_members_path(group) }
end
describe '#leave_group_members_path' do
let(:group) { build_stubbed(:group) }
it { expect(leave_group_members_path(group)).to eq leave_group_group_members_path(group) }
end
describe '#approve_access_request_group_member_path' do
let(:group_member) { create(:group_member) }
it { expect(approve_access_request_group_member_path(group_member)).to eq approve_access_request_group_group_member_path(group_member.source, group_member) }
end
describe '#resend_invite_group_member_path' do
let(:group_member) { create(:group_member) }
it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end
end
end
require 'spec_helper'
describe MembersHelper do
describe '#action_member_permission' do
let(:project_member) { build(:project_member) }
let(:group_member) { build(:group_member) }
it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
end
describe '#can_see_member_roles?' do
let(:project) { create(:empty_project) }
let(:group) { create(:group) }
let(:user) { build(:user) }
let(:admin) { build(:user, :admin) }
let(:project_member) { create(:project_member, project: project) }
let(:group_member) { create(:group_member, group: group) }
it { expect(can_see_member_roles?(source: project, user: nil)).to be_falsy }
it { expect(can_see_member_roles?(source: group, user: nil)).to be_falsy }
it { expect(can_see_member_roles?(source: project, user: admin)).to be_truthy }
it { expect(can_see_member_roles?(source: group, user: admin)).to be_truthy }
it { expect(can_see_member_roles?(source: project, user: project_member.user)).to be_truthy }
it { expect(can_see_member_roles?(source: group, user: group_member.user)).to be_truthy }
end
describe '#remove_member_message' do
let(:requester) { build(:user) }
let(:project) { create(:project) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
let(:group) { create(:group) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" }
it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" }
it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" }
it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" }
it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
end
describe '#remove_member_title' do
let(:requester) { build(:user) }
let(:project) { create(:project) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
let(:group) { create(:group) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_request) { group.request_access(requester) }
it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
it { expect(remove_member_title(group_member)).to eq 'Remove user from group' }
it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
end
describe '#leave_confirmation_message' do
let(:project) { build_stubbed(:project) }
let(:group) { build_stubbed(:group) }
let(:user) { build_stubbed(:user) }
it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" }
it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
end
end
......@@ -45,16 +45,6 @@ describe ProjectsHelper do
end
end
describe 'user_max_access_in_project' do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.team.add_user(user, Gitlab::Access::MASTER)
end
it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') }
end
describe "readme_cache_key" do
let(:project) { create(:project) }
......
......@@ -72,14 +72,25 @@ describe Gitlab::Metrics::Sampler do
end
end
describe '#sample_objects' do
it 'adds a metric containing the amount of allocated objects' do
expect(sampler).to receive(:add_metric).
with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
at_least(:once).
and_call_original
if Gitlab::Metrics.mri?
describe '#sample_objects' do
it 'adds a metric containing the amount of allocated objects' do
expect(sampler).to receive(:add_metric).
with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
at_least(:once).
and_call_original
sampler.sample_objects
end
sampler.sample_objects
it 'ignores classes without a name' do
expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
expect(sampler).not_to receive(:add_metric).
with('object_counts', an_instance_of(Hash), type: nil)
sampler.sample_objects
end
end
end
......
......@@ -400,26 +400,136 @@ describe Notify do
end
end
describe 'project access requested' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
project.members.request.find_by(user_id: user.id)
end
subject { Notify.member_access_requested_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
is_expected.to have_body_text /#{project.name_with_namespace}/
is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
is_expected.to have_body_text /#{project_member.human_access}/
end
end
describe 'project access denied' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
project.members.request.find_by(user_id: user.id)
end
subject { Notify.member_access_denied_email('project', project.id, user.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
is_expected.to have_body_text /#{project.name_with_namespace}/
is_expected.to have_body_text /#{project.web_url}/
end
end
describe 'project access changed' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { Notify.project_access_granted_email(project_member.id) }
subject { Notify.member_access_granted_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Access to project was granted/
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
is_expected.to have_body_text /#{project.name_with_namespace}/
is_expected.to have_body_text /#{project.web_url}/
is_expected.to have_body_text /#{project_member.human_access}/
end
end
it 'contains name of project' do
is_expected.to have_body_text /#{project.name}/
end
def invite_to_project(project:, email:, inviter:)
ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
it 'contains new user role' do
project.project_members.invite.last
end
describe 'project invitation' do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
is_expected.to have_body_text /#{project.name_with_namespace}/
is_expected.to have_body_text /#{project.web_url}/
is_expected.to have_body_text /#{project_member.human_access}/
is_expected.to have_body_text /#{project_member.invite_token}/
end
end
describe 'project invitation accepted' do
let(:project) { create(:project) }
let(:invited_user) { create(:user) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
invitee.accept_invite!(invited_user)
invitee
end
subject { Notify.member_invite_accepted_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
is_expected.to have_body_text /#{project.name_with_namespace}/
is_expected.to have_body_text /#{project.web_url}/
is_expected.to have_body_text /#{project_member.invite_email}/
is_expected.to have_body_text /#{invited_user.name}/
end
end
describe 'project invitation declined' do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
invitee.decline_invite!
invitee
end
subject { Notify.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
is_expected.to have_body_text /#{project.name_with_namespace}/
is_expected.to have_body_text /#{project.web_url}/
is_expected.to have_body_text /#{project_member.invite_email}/
end
end
......@@ -535,27 +645,139 @@ describe Notify do
end
end
describe 'group access changed' do
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:membership) { create(:group_member, group: group, user: user) }
context 'for a group' do
describe 'group access requested' do
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:group_member) do
group.request_access(user)
group.members.request.find_by(user_id: user.id)
end
subject { Notify.member_access_requested_email('group', group_member.id) }
subject { Notify.group_access_granted_email(membership.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject "Request to join the #{group.name} group"
is_expected.to have_body_text /#{group.name}/
is_expected.to have_body_text /#{group_group_members_url(group)}/
is_expected.to have_body_text /#{group_member.human_access}/
end
end
it 'has the correct subject' do
is_expected.to have_subject /Access to group was granted/
describe 'group access denied' do
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:group_member) do
group.request_access(user)
group.members.request.find_by(user_id: user.id)
end
subject { Notify.member_access_denied_email('group', group.id, user.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was denied"
is_expected.to have_body_text /#{group.name}/
is_expected.to have_body_text /#{group.web_url}/
end
end
describe 'group access changed' do
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:group_member) { create(:group_member, group: group, user: user) }
subject { Notify.member_access_granted_email('group', group_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was granted"
is_expected.to have_body_text /#{group.name}/
is_expected.to have_body_text /#{group.web_url}/
is_expected.to have_body_text /#{group_member.human_access}/
end
end
def invite_to_group(group:, email:, inviter:)
GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
group.group_members.invite.last
end
it 'contains name of project' do
is_expected.to have_body_text /#{group.name}/
describe 'group invitation' do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{group.name} group"
is_expected.to have_body_text /#{group.name}/
is_expected.to have_body_text /#{group.web_url}/
is_expected.to have_body_text /#{group_member.human_access}/
is_expected.to have_body_text /#{group_member.invite_token}/
end
end
it 'contains new user role' do
is_expected.to have_body_text /#{membership.human_access}/
describe 'group invitation accepted' do
let(:group) { create(:group) }
let(:invited_user) { create(:user) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
invitee.accept_invite!(invited_user)
invitee
end
subject { Notify.member_invite_accepted_email('group', group_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
is_expected.to have_body_text /#{group.name}/
is_expected.to have_body_text /#{group.web_url}/
is_expected.to have_body_text /#{group_member.invite_email}/
is_expected.to have_body_text /#{invited_user.name}/
end
end
describe 'group invitation declined' do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
invitee.decline_invite!
invitee
end
subject { Notify.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
is_expected.to have_body_text /#{group.name}/
is_expected.to have_body_text /#{group.web_url}/
is_expected.to have_body_text /#{group_member.invite_email}/
end
end
end
......
require 'spec_helper'
describe AccessRequestable do
describe 'Group' do
describe '#request_access' do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
it { expect(group.request_access(user)).to be_a(GroupMember) }
it { expect(group.request_access(user).user).to eq(user) }
end
describe '#access_requested?' do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
before { group.request_access(user) }
it { expect(group.members.request.exists?(user_id: user)).to be_truthy }
end
end
describe 'Project' do
describe '#request_access' do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
it { expect(project.request_access(user)).to be_a(ProjectMember) }
end
describe '#access_requested?' do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
before { project.request_access(user) }
it { expect(project.members.request.exists?(user_id: user)).to be_truthy }
end
end
end
......@@ -5,7 +5,11 @@ describe Group, models: true do
describe 'associations' do
it { is_expected.to have_many :projects }
it { is_expected.to have_many :group_members }
it { is_expected.to have_many(:group_members).dependent(:destroy) }
it { is_expected.to have_many(:users).through(:group_members) }
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
end
describe 'modules' do
......@@ -131,4 +135,46 @@ describe Group, models: true do
expect(described_class.search(group.path.upcase)).to eq([group])
end
end
describe '#has_owner?' do
before { @members = setup_group_members(group) }
it { expect(group.has_owner?(@members[:owner])).to be_truthy }
it { expect(group.has_owner?(@members[:master])).to be_falsey }
it { expect(group.has_owner?(@members[:developer])).to be_falsey }
it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
it { expect(group.has_owner?(@members[:guest])).to be_falsey }
it { expect(group.has_owner?(@members[:requester])).to be_falsey }
end
describe '#has_master?' do
before { @members = setup_group_members(group) }
it { expect(group.has_master?(@members[:owner])).to be_falsey }
it { expect(group.has_master?(@members[:master])).to be_truthy }
it { expect(group.has_master?(@members[:developer])).to be_falsey }
it { expect(group.has_master?(@members[:reporter])).to be_falsey }
it { expect(group.has_master?(@members[:guest])).to be_falsey }
it { expect(group.has_master?(@members[:requester])).to be_falsey }
end
def setup_group_members(group)
members = {
owner: create(:user),
master: create(:user),
developer: create(:user),
reporter: create(:user),
guest: create(:user),
requester: create(:user)
}
group.add_user(members[:owner], GroupMember::OWNER)
group.add_user(members[:master], GroupMember::MASTER)
group.add_user(members[:developer], GroupMember::DEVELOPER)
group.add_user(members[:reporter], GroupMember::REPORTER)
group.add_user(members[:guest], GroupMember::GUEST)
group.request_access(members[:requester])
members
end
end
......@@ -55,11 +55,97 @@ describe Member, models: true do
end
end
describe 'Scopes & finders' do
before do
project = create(:project)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
accepted_invite_user = build(:user)
ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
@accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@requested_member = project.members.request.find_by(user_id: requested_user.id)
accepted_request_user = create(:user).tap { |u| project.request_access(u) }
@accepted_request_member = project.members.request.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
end
describe '.invite' do
it { expect(described_class.invite).not_to include @master }
it { expect(described_class.invite).to include @invited_member }
it { expect(described_class.invite).not_to include @accepted_invite_member }
it { expect(described_class.invite).not_to include @requested_member }
it { expect(described_class.invite).not_to include @accepted_request_member }
end
describe '.non_invite' do
it { expect(described_class.non_invite).to include @master }
it { expect(described_class.non_invite).not_to include @invited_member }
it { expect(described_class.non_invite).to include @accepted_invite_member }
it { expect(described_class.non_invite).to include @requested_member }
it { expect(described_class.non_invite).to include @accepted_request_member }
end
describe '.request' do
it { expect(described_class.request).not_to include @master }
it { expect(described_class.request).not_to include @invited_member }
it { expect(described_class.request).not_to include @accepted_invite_member }
it { expect(described_class.request).to include @requested_member }
it { expect(described_class.request).not_to include @accepted_request_member }
end
describe '.non_request' do
it { expect(described_class.non_request).to include @master }
it { expect(described_class.non_request).to include @invited_member }
it { expect(described_class.non_request).to include @accepted_invite_member }
it { expect(described_class.non_request).not_to include @requested_member }
it { expect(described_class.non_request).to include @accepted_request_member }
end
describe '.non_pending' do
it { expect(described_class.non_pending).to include @master }
it { expect(described_class.non_pending).not_to include @invited_member }
it { expect(described_class.non_pending).to include @accepted_invite_member }
it { expect(described_class.non_pending).not_to include @requested_member }
it { expect(described_class.non_pending).to include @accepted_request_member }
end
describe '.owners_and_masters' do
it { expect(described_class.owners_and_masters).to include @owner }
it { expect(described_class.owners_and_masters).to include @master }
it { expect(described_class.owners_and_masters).not_to include @invited_member }
it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
it { expect(described_class.owners_and_masters).not_to include @requested_member }
it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
end
end
describe "Delegate methods" do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
end
describe 'Callbacks' do
describe 'after_destroy :post_decline_request, if: :request?' do
let(:member) { create(:project_member, requested_at: Time.now.utc) }
it 'calls #post_decline_request' do
expect(member).to receive(:post_decline_request)
member.destroy
end
end
end
describe ".add_user" do
let!(:user) { create(:user) }
let(:project) { create(:project) }
......@@ -97,6 +183,44 @@ describe Member, models: true do
end
end
describe '#accept_request' do
let(:member) { create(:project_member, requested_at: Time.now.utc) }
it { expect(member.accept_request).to be_truthy }
it 'clears requested_at' do
member.accept_request
expect(member.requested_at).to be_nil
end
it 'calls #after_accept_request' do
expect(member).to receive(:after_accept_request)
member.accept_request
end
end
describe '#invite?' do
subject { create(:project_member, invite_email: "user@example.com", user: nil) }
it { is_expected.to be_invite }
end
describe '#request?' do
subject { create(:project_member, requested_at: Time.now.utc) }
it { is_expected.to be_request }
end
describe '#pending?' do
let(:invited_member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:requester) { create(:project_member, requested_at: Time.now.utc) }
it { expect(invited_member).to be_invite }
it { expect(requester).to be_pending }
end
describe "#accept_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
......
......@@ -20,7 +20,7 @@
require 'spec_helper'
describe GroupMember, models: true do
context 'notification' do
describe 'notifications' do
describe "#after_create" do
it "should send email to user" do
membership = build(:group_member)
......@@ -50,5 +50,31 @@ describe GroupMember, models: true do
@group_member.update_attribute(:access_level, GroupMember::OWNER)
end
end
describe '#after_accept_request' do
it 'calls NotificationService.accept_group_access_request' do
member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:new_group_member)
member.__send__(:after_accept_request)
end
end
describe '#post_decline_request' do
it 'calls NotificationService.decline_group_access_request' do
member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:decline_group_access_request)
member.__send__(:post_decline_request)
end
end
describe '#real_source_type' do
subject { create(:group_member).real_source_type }
it { is_expected.to eq 'Group' }
end
end
end
......@@ -33,6 +33,12 @@ describe ProjectMember, models: true do
it { is_expected.to include_module(Gitlab::ShellAdapter) }
end
describe '#real_source_type' do
subject { create(:project_member).real_source_type }
it { is_expected.to eq 'Project' }
end
describe "#destroy" do
let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) }
let(:project) { owner.project }
......@@ -135,4 +141,26 @@ describe ProjectMember, models: true do
it { expect(@project_1.users).to be_empty }
it { expect(@project_2.users).to be_empty }
end
describe 'notifications' do
describe '#after_accept_request' do
it 'calls NotificationService.new_project_member' do
member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:new_project_member)
member.__send__(:after_accept_request)
end
end
describe '#post_decline_request' do
it 'calls NotificationService.decline_project_access_request' do
member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:decline_project_access_request)
member.__send__(:post_decline_request)
end
end
end
end
......@@ -89,11 +89,17 @@ describe Project, models: true do
it { is_expected.to respond_to(:repo_exists?) }
it { is_expected.to respond_to(:update_merge_requests) }
it { is_expected.to respond_to(:execute_hooks) }
it { is_expected.to respond_to(:name_with_namespace) }
it { is_expected.to respond_to(:owner) }
it { is_expected.to respond_to(:path_with_namespace) }
end
describe '#name_with_namespace' do
let(:project) { build_stubbed(:empty_project) }
it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
it { expect(project.human_name).to eq project.name_with_namespace }
end
describe '#to_reference' do
let(:project) { create(:empty_project) }
......
......@@ -73,47 +73,42 @@ describe ProjectTeam, models: true do
end
end
describe :max_invited_level do
let(:group) { create(:group) }
let(:project) { create(:empty_project) }
before do
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::DEVELOPER
)
group.add_user(master, Gitlab::Access::MASTER)
group.add_user(reporter, Gitlab::Access::REPORTER)
end
it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
end
describe :max_member_access do
let(:group) { create(:group) }
let(:project) { create(:empty_project) }
before do
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::DEVELOPER
)
group.add_user(master, Gitlab::Access::MASTER)
group.add_user(reporter, Gitlab::Access::REPORTER)
describe '#find_member' do
context 'personal project' do
let(:project) { create(:empty_project) }
let(:requester) { create(:user) }
before do
project.team << [master, :master]
project.team << [reporter, :reporter]
project.team << [guest, :guest]
project.request_access(requester)
end
it { expect(project.team.find_member(master.id)).to be_a(ProjectMember) }
it { expect(project.team.find_member(reporter.id)).to be_a(ProjectMember) }
it { expect(project.team.find_member(guest.id)).to be_a(ProjectMember) }
it { expect(project.team.find_member(nonmember.id)).to be_nil }
it { expect(project.team.find_member(requester.id)).to be_nil }
end
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
it "does not have an access" do
project.namespace.update(share_with_group_lock: true)
expect(project.team.max_member_access(master.id)).to be_nil
expect(project.team.max_member_access(reporter.id)).to be_nil
context 'group project' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
let(:requester) { create(:user) }
before do
group.add_master(master)
group.add_reporter(reporter)
group.add_guest(guest)
group.request_access(requester)
end
it { expect(project.team.find_member(master.id)).to be_a(GroupMember) }
it { expect(project.team.find_member(reporter.id)).to be_a(GroupMember) }
it { expect(project.team.find_member(guest.id)).to be_a(GroupMember) }
it { expect(project.team.find_member(nonmember.id)).to be_nil }
it { expect(project.team.find_member(requester.id)).to be_nil }
end
end
......@@ -138,4 +133,69 @@ describe ProjectTeam, models: true do
expect(project.team.human_max_access(user.id)).to eq 'Owner'
end
end
describe '#max_member_access' do
let(:requester) { create(:user) }
context 'personal project' do
let(:project) { create(:empty_project) }
context 'when project is not shared with group' do
before do
project.team << [master, :master]
project.team << [reporter, :reporter]
project.team << [guest, :guest]
project.request_access(requester)
end
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
it { expect(project.team.max_member_access(requester.id)).to be_nil }
end
context 'when project is shared with group' do
before do
group = create(:group)
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::DEVELOPER)
group.add_master(master)
group.add_reporter(reporter)
end
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
it { expect(project.team.max_member_access(requester.id)).to be_nil }
context 'but share_with_group_lock is true' do
before { project.namespace.update(share_with_group_lock: true) }
it { expect(project.team.max_member_access(master.id)).to be_nil }
it { expect(project.team.max_member_access(reporter.id)).to be_nil }
end
end
end
context 'group project' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
before do
group.add_master(master)
group.add_reporter(reporter)
group.add_guest(guest)
group.request_access(requester)
end
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
it { expect(project.team.max_member_access(requester.id)).to be_nil }
end
end
end
......@@ -228,6 +228,14 @@ describe TodoService, services: true do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end
end
describe '#mark_todo' do
it 'creates a todo from a issue' do
service.mark_todo(unassigned_issue, author)
should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
end
end
end
describe 'Merge Requests' do
......@@ -361,6 +369,14 @@ describe TodoService, services: true do
expect(second_todo.reload).not_to be_done
end
end
describe '#mark_todo' do
it 'creates a todo from a merge request' do
service.mark_todo(mr_unassigned, author)
should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED)
end
end
end
def should_create_todo(attributes = {})
......
......@@ -2,6 +2,7 @@ require "spec_helper"
describe StuckCiBuildsWorker do
let!(:build) { create :ci_build }
let(:worker) { described_class.new }
subject do
build.reload
......@@ -16,13 +17,13 @@ describe StuckCiBuildsWorker do
it 'gets dropped if it was updated over 2 days ago' do
build.update!(updated_at: 2.days.ago)
StuckCiBuildsWorker.new.perform
worker.perform
is_expected.to eq('failed')
end
it "is still #{status}" do
build.update!(updated_at: 1.minute.ago)
StuckCiBuildsWorker.new.perform
worker.perform
is_expected.to eq(status)
end
end
......@@ -36,9 +37,21 @@ describe StuckCiBuildsWorker do
it "is still #{status}" do
build.update!(updated_at: 2.days.ago)
StuckCiBuildsWorker.new.perform
worker.perform
is_expected.to eq(status)
end
end
end
context "for deleted project" do
before do
build.update!(status: :running, updated_at: 2.days.ago)
build.project.update(pending_delete: true)
end
it "does not drop build" do
expect_any_instance_of(Ci::Build).not_to receive(:drop)
worker.perform
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment