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) ...@@ -9,6 +9,8 @@ v 8.9.0 (unreleased)
- Allow enabling wiki page events from Webhook management UI - Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0 - Bump rouge to 1.11.0
- Fix issue with arrow keys not working in search autocomplete dropdown - 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 - Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository - Fix wiki page events' webhook to point to the wiki repository
- Don't show tags for revert and cherry-pick operations - Don't show tags for revert and cherry-pick operations
...@@ -25,6 +27,7 @@ v 8.9.0 (unreleased) ...@@ -25,6 +27,7 @@ v 8.9.0 (unreleased)
- Redesign navigation for project pages - Redesign navigation for project pages
- Fix groups API to list only user's accessible projects - Fix groups API to list only user's accessible projects
- Redesign account and email confirmation emails - 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 - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8 - Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0 - Use gitlab-shell v3.0.0
...@@ -57,6 +60,7 @@ v 8.9.0 (unreleased) ...@@ -57,6 +60,7 @@ v 8.9.0 (unreleased)
- Use Knapsack only in CI environment - Use Knapsack only in CI environment
- Cache project build count in sidebar nav - Cache project build count in sidebar nav
- Add milestone expire date to the right sidebar - 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 - 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 - Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects - Improve error handling importing projects
...@@ -80,6 +84,13 @@ v 8.9.0 (unreleased) ...@@ -80,6 +84,13 @@ v 8.9.0 (unreleased)
- Measure CPU time for instrumented methods - Measure CPU time for instrumented methods
- Instrument private methods and private instance methods by default instead just public 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 - 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) v 8.8.5 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds - Ensure branch cleanup regardless of whether the GitHub import process succeeds
......
...@@ -50,7 +50,7 @@ GEM ...@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.4) allocations (1.0.5)
arel (6.0.3) arel (6.0.3)
asana (0.4.0) asana (0.4.0)
faraday (~> 0.9) faraday (~> 0.9)
......
...@@ -56,13 +56,6 @@ issuable_created = false ...@@ -56,13 +56,6 @@ issuable_created = false
Issuable.filterResults $('.filter-form') Issuable.filterResults $('.filter-form')
$('.js-label-select').trigger('update.label') $('.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) => filterResults: (form) =>
formData = form.serialize() formData = form.serialize()
...@@ -71,58 +64,16 @@ issuable_created = false ...@@ -71,58 +64,16 @@ issuable_created = false
issuesUrl = formAction issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData 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: -> initChecks: ->
$('.check_all_issues').on 'click', -> $('.check_all_issues').off('click').on('click', ->
$('.selected_issue').prop('checked', @checked) $('.selected_issue').prop('checked', @checked)
Issuable.checkChanged() Issuable.checkChanged()
)
$('.selected_issue').on 'change', Issuable.checkChanged $('.selected_issue').off('change').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
checkChanged: -> checkChanged: ->
checked_issues = $('.selected_issue:checked') checked_issues = $('.selected_issue:checked')
......
...@@ -9,6 +9,9 @@ class @IssuableBulkActions ...@@ -9,6 +9,9 @@ class @IssuableBulkActions
@bindEvents() @bindEvents()
# Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
getElement: (selector) -> getElement: (selector) ->
@container.find selector @container.find selector
......
class @LayoutNav hideEndFade = ($scrollingTabs) ->
$ -> $scrollingTabs.each ->
$this = $(@)
$this
.find('.fade-right')
.toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
$ ->
$('.fade-left').addClass('end-scroll') $('.fade-left').addClass('end-scroll')
hideEndFade($('.scrolling-tabs'))
$(window)
.off 'resize.nav'
.on 'resize.nav', ->
hideEndFade($('.scrolling-tabs'))
$('.scrolling-tabs').on 'scroll', (event) -> $('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this) $this = $(this)
$el = $(event.target)
currentPosition = $this.scrollLeft() currentPosition = $this.scrollLeft()
size = bp.getBreakpointSize() maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
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) $this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) $this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
...@@ -115,12 +115,14 @@ class @Notes ...@@ -115,12 +115,14 @@ class @Notes
, @pollingInterval , @pollingInterval
refresh: => refresh: =>
return if @refreshing is true
@refreshing = true
if not document.hidden and document.URL.indexOf(@noteable_url) is 0 if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent() @getContent()
getContent: -> getContent: ->
return if @refreshing
@refreshing = true
$.ajax $.ajax
url: @notes_url url: @notes_url
data: "last_fetched_at=" + @last_fetched_at data: "last_fetched_at=" + @last_fetched_at
......
...@@ -43,6 +43,55 @@ class @Sidebar ...@@ -43,6 +43,55 @@ class @Sidebar
$('.right-sidebar') $('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' }) .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) -> sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
...@@ -117,5 +166,3 @@ class @Sidebar ...@@ -117,5 +166,3 @@ class @Sidebar
getBlock: (name) -> getBlock: (name) ->
@sidebar.find(".block.#{name}") @sidebar.find(".block.#{name}")
...@@ -280,11 +280,10 @@ ...@@ -280,11 +280,10 @@
} }
.dropdown { .dropdown {
margin-left: 7px; position: absolute;
top: 7px;
@media (max-width: $screen-xs-min) { right: 15px;
margin-left: 0; z-index: 2;
}
li.active { li.active {
font-weight: bold; font-weight: bold;
......
...@@ -83,6 +83,12 @@ ...@@ -83,6 +83,12 @@
margin-top: 10px; margin-top: 10px;
} }
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
}
a { a {
width: $sidebar_width; width: $sidebar_width;
padding: 7px 15px 7px 23px; padding: 7px 15px 7px 23px;
......
...@@ -39,3 +39,20 @@ ...@@ -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 @@ ...@@ -34,6 +34,10 @@
color: inherit; color: inherit;
} }
.issuable-header-text {
margin-top: 7px;
}
.block { .block {
@include clearfix; @include clearfix;
padding: $gl-padding 0; padding: $gl-padding 0;
...@@ -60,10 +64,6 @@ ...@@ -60,10 +64,6 @@
margin-top: 0; margin-top: 0;
} }
.issuable-count {
margin-top: 7px;
}
.gutter-toggle { .gutter-toggle {
margin-left: 20px; margin-left: 20px;
padding-left: 10px; padding-left: 10px;
...@@ -250,7 +250,7 @@ ...@@ -250,7 +250,7 @@
} }
} }
.issuable-pager { .issuable-header-btn {
background: $gray-normal; background: $gray-normal;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
&:hover { &:hover {
...@@ -263,7 +263,7 @@ ...@@ -263,7 +263,7 @@
} }
} }
a:not(.issuable-pager) { a {
&:hover { &:hover {
color: $md-link-color; color: $md-link-color;
text-decoration: none; text-decoration: none;
......
...@@ -229,13 +229,20 @@ ...@@ -229,13 +229,20 @@
right: 16px; right: 16px;
bottom: 0; bottom: 0;
.btn { @media (max-width: $screen-lg-min) {
padding: 3px 10px; top: 0;
background-color: $background-color;
} }
@media (max-width: 1304px) { .access-request-button {
top: 0; position: absolute;
right: 0;
bottom: 61px;
@media (max-width: $screen-lg-min) {
position: relative;
bottom: 0;
margin-right: 10px;
}
} }
} }
...@@ -286,10 +293,6 @@ ...@@ -286,10 +293,6 @@
color: #555; color: #555;
} }
.project_member_row form {
margin: 0;
}
.transfer-project .select2-container { .transfer-project .select2-container {
min-width: 200px; 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 class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
# Authorize # Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave] before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index def index
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members @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? if params[:search].present?
users = @group.users.search(params[:search]).to_a users = @group.users.search(params[:search]).to_a
...@@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
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 protected
def member_params def member_params
params.require(:group_member).permit(:access_level, :user_id) params.require(:group_member).permit(:access_level, :user_id)
end end
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end end
class Projects::ProjectMembersController < Projects::ApplicationController class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
# Authorize # Authorize
before_action :authorize_admin_project_member!, except: [:leave, :index] before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index def index
@project_members = @project.project_members @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? if params[:search].present?
users = @project.users.search(params[:search]).to_a users = @project.users.search(params[:search]).to_a
...@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC') @project_members = @project_members.order('access_level DESC')
@group = @project.group @group = @project.group
if @group if @group
@group_members = @group.group_members @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? if params[:search].present?
users = @group.users.search(params[:search]).to_a users = @group.users.search(params[:search]).to_a
...@@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
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 def apply_import
source_project = Project.find(params[:source_project_id]) source_project = Project.find(params[:source_project_id])
...@@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def member_params def member_params
params.require(:project_member).permit(:user_id, :access_level) params.require(:project_member).permit(:user_id, :access_level)
end end
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
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 ...@@ -36,7 +36,7 @@ class TodosFinder
private private
def action_id? 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 end
def action_id def action_id
......
...@@ -13,10 +13,23 @@ ...@@ -13,10 +13,23 @@
# merge_request_path(merge_request) # merge_request_path(merge_request)
# #
module GitlabRoutingHelper module GitlabRoutingHelper
# Project
def project_path(project, *args) def project_path(project, *args)
namespace_project_path(project.namespace, project, *args) namespace_project_path(project.namespace, project, *args)
end 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) def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref) namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end end
...@@ -41,10 +54,6 @@ module GitlabRoutingHelper ...@@ -41,10 +54,6 @@ module GitlabRoutingHelper
activity_namespace_project_path(project.namespace, project, *args) activity_namespace_project_path(project.namespace, project, *args)
end end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def runners_path(project, *args) def runners_path(project, *args)
namespace_project_runners_path(project.namespace, project, *args) namespace_project_runners_path(project.namespace, project, *args)
end end
...@@ -65,14 +74,6 @@ module GitlabRoutingHelper ...@@ -65,14 +74,6 @@ module GitlabRoutingHelper
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end 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) def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args) namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end end
...@@ -92,4 +93,56 @@ module GitlabRoutingHelper ...@@ -92,4 +93,56 @@ module GitlabRoutingHelper
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity) toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end end
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 end
module GroupsHelper 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) def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group) can?(current_user, :change_visibility_level, group)
end end
......
...@@ -67,6 +67,12 @@ module IssuablesHelper ...@@ -67,6 +67,12 @@ module IssuablesHelper
end end
end end
def has_todo(issuable)
unless current_user.nil?
current_user.todos.find_by(target_id: issuable.id, state: :pending)
end
end
private private
def sidebar_gutter_collapsed? 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 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) def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name') title = content_tag(:span, project.name, class: 'project-name')
...@@ -115,14 +107,6 @@ module ProjectsHelper ...@@ -115,14 +107,6 @@ module ProjectsHelper
end end
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) def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil? return 'LICENSE' if project.repository.license_key.nil?
...@@ -286,10 +270,6 @@ module ProjectsHelper ...@@ -286,10 +270,6 @@ module ProjectsHelper
end end
end end
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
def new_readme_path def new_readme_path
ref = @repository.root_ref if @repository ref = @repository.root_ref if @repository
ref ||= 'master' ref ||= 'master'
......
...@@ -12,6 +12,7 @@ module TodosHelper ...@@ -12,6 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your' when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'marked this as a Todo for'
end end
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 Emails
module Projects 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) def project_was_moved_email(project_id, user_id, old_path_with_namespace)
@current_user = @user = User.find user_id @current_user = @user = User.find user_id
@project = Project.find project_id @project = Project.find project_id
......
...@@ -6,13 +6,15 @@ class Notify < BaseMailer ...@@ -6,13 +6,15 @@ class Notify < BaseMailer
include Emails::Notes include Emails::Notes
include Emails::Projects include Emails::Projects
include Emails::Profile include Emails::Profile
include Emails::Groups
include Emails::Builds include Emails::Builds
include Emails::Members
add_template_helper MergeRequestsHelper add_template_helper MergeRequestsHelper
add_template_helper DiffHelper add_template_helper DiffHelper
add_template_helper BlobHelper add_template_helper BlobHelper
add_template_helper EmailsHelper add_template_helper EmailsHelper
add_template_helper MembersHelper
add_template_helper GitlabRoutingHelper
def test_email(recipient_email, subject, body) def test_email(recipient_email, subject, body)
mail(to: recipient_email, mail(to: recipient_email,
......
...@@ -187,6 +187,8 @@ class Ability ...@@ -187,6 +187,8 @@ class Ability
project_report_rules project_report_rules
elsif team.guest?(user) elsif team.guest?(user)
project_guest_rules project_guest_rules
else
[]
end end
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' ...@@ -3,11 +3,12 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace class Group < Namespace
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include AccessRequestable
include Referable include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members 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 :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, dependent: :destroy, as: :source
...@@ -58,6 +59,10 @@ class Group < Namespace ...@@ -58,6 +59,10 @@ class Group < Namespace
"#{self.class.reference_prefix}#{name}" "#{self.class.reference_prefix}#{name}"
end end
def web_url
Gitlab::Routing.url_helpers.group_url(self)
end
def human_name def human_name
name name
end end
......
...@@ -26,20 +26,28 @@ class Member < ActiveRecord::Base ...@@ -26,20 +26,28 @@ class Member < ActiveRecord::Base
allow_nil: true allow_nil: true
} }
scope :invite, -> { where(user_id: nil) } scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") } 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 :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) } scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) } scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) } scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) } 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? } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite? after_create :send_invite, if: :invite?
after_create :create_notification_setting, unless: :invite? after_create :send_request, if: :request?
after_create :post_create_hook, unless: :invite? after_create :create_notification_setting, unless: :pending?
after_update :post_update_hook, unless: :invite? after_create :post_create_hook, unless: :pending?
after_destroy :post_destroy_hook, unless: :invite? 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 delegate :name, :username, :email, to: :user, prefix: true
...@@ -96,10 +104,31 @@ class Member < ActiveRecord::Base ...@@ -96,10 +104,31 @@ class Member < ActiveRecord::Base
end end
end end
def real_source_type
source_type
end
def invite? def invite?
self.invite_token.present? self.invite_token.present?
end 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) def accept_invite!(new_user)
return false unless invite? return false unless invite?
...@@ -157,6 +186,10 @@ class Member < ActiveRecord::Base ...@@ -157,6 +186,10 @@ class Member < ActiveRecord::Base
# override in subclass # override in subclass
end end
def send_request
# override in subclass
end
def post_create_hook def post_create_hook
system_hook_service.execute_hooks_for(self, :create) system_hook_service.execute_hooks_for(self, :create)
end end
...@@ -177,6 +210,14 @@ class Member < ActiveRecord::Base ...@@ -177,6 +210,14 @@ class Member < ActiveRecord::Base
# override in subclass # override in subclass
end end
def after_accept_request
post_create_hook
end
def post_decline_request
# override in subclass
end
def system_hook_service def system_hook_service
SystemHooksService.new SystemHooksService.new
end end
......
...@@ -8,9 +8,6 @@ class GroupMember < Member ...@@ -8,9 +8,6 @@ class GroupMember < Member
validates_format_of :source_type, with: /\ANamespace\z/ validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) } 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 def self.access_level_roles
Gitlab::Access.options_with_owner Gitlab::Access.options_with_owner
end end
...@@ -23,6 +20,11 @@ class GroupMember < Member ...@@ -23,6 +20,11 @@ class GroupMember < Member
access_level access_level
end end
# Because source_type is `Namespace`...
def real_source_type
'Group'
end
private private
def send_invite def send_invite
...@@ -31,6 +33,12 @@ class GroupMember < Member ...@@ -31,6 +33,12 @@ class GroupMember < Member
super super
end end
def send_request
notification_service.new_group_access_request(self)
super
end
def post_create_hook def post_create_hook
notification_service.new_group_member(self) notification_service.new_group_member(self)
...@@ -56,4 +64,10 @@ class GroupMember < Member ...@@ -56,4 +64,10 @@ class GroupMember < Member
super super
end end
def post_decline_request
notification_service.decline_group_access_request(self)
super
end
end end
...@@ -11,8 +11,6 @@ class ProjectMember < Member ...@@ -11,8 +11,6 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) } 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 before_destroy :delete_member_todos
...@@ -84,7 +82,7 @@ class ProjectMember < Member ...@@ -84,7 +82,7 @@ class ProjectMember < Member
Gitlab::Access.sym_options Gitlab::Access.sym_options
end end
def access_roles def access_level_roles
Gitlab::Access.options Gitlab::Access.options
end end
end end
...@@ -113,6 +111,12 @@ class ProjectMember < Member ...@@ -113,6 +111,12 @@ class ProjectMember < Member
super super
end end
def send_request
notification_service.new_project_access_request(self)
super
end
def post_create_hook def post_create_hook
unless owner? unless owner?
event_service.join_project(self.project, self.user) event_service.join_project(self.project, self.user)
...@@ -148,6 +152,12 @@ class ProjectMember < Member ...@@ -148,6 +152,12 @@ class ProjectMember < Member
super super
end end
def post_decline_request
notification_service.decline_project_access_request(self)
super
end
def event_service def event_service
EventCreateService.new EventCreateService.new
end end
......
...@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include AccessRequestable
include Referable include Referable
include Sortable include Sortable
include AfterCommitQueue include AfterCommitQueue
...@@ -103,7 +104,8 @@ class Project < ActiveRecord::Base ...@@ -103,7 +104,8 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy has_many :protected_branches, dependent: :destroy
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember' has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
has_many :users, through: :project_members 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_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy has_many :users_star_projects, dependent: :destroy
...@@ -680,16 +682,6 @@ class Project < ActiveRecord::Base ...@@ -680,16 +682,6 @@ class Project < ActiveRecord::Base
end end
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 def name_with_namespace
@name_with_namespace ||= begin @name_with_namespace ||= begin
if namespace if namespace
...@@ -699,6 +691,7 @@ class Project < ActiveRecord::Base ...@@ -699,6 +691,7 @@ class Project < ActiveRecord::Base
end end
end end
end end
alias_method :human_name, :name_with_namespace
def path_with_namespace def path_with_namespace
if namespace if namespace
......
...@@ -21,23 +21,13 @@ class ProjectTeam ...@@ -21,23 +21,13 @@ class ProjectTeam
end end
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) 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 # If user is not in project members
# we should check for group membership # we should check for group membership
if group && !member 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 end
member member
...@@ -61,13 +51,10 @@ class ProjectTeam ...@@ -61,13 +51,10 @@ class ProjectTeam
ProjectMember.truncate_team(project) ProjectMember.truncate_team(project)
end end
def users
members
end
def members def members
@members ||= fetch_members @members ||= fetch_members
end end
alias_method :users, :members
def guests def guests
@guests ||= fetch_members(:guests) @guests ||= fetch_members(:guests)
...@@ -150,7 +137,7 @@ class ProjectTeam ...@@ -150,7 +137,7 @@ class ProjectTeam
def max_member_access(user_id) def max_member_access(user_id)
access = [] access = []
project.project_members.each do |member| project.members.non_request.each do |member|
if member.user_id == user_id if member.user_id == user_id
access << member.access_field if member.access_field access << member.access_field if member.access_field
break break
...@@ -158,7 +145,7 @@ class ProjectTeam ...@@ -158,7 +145,7 @@ class ProjectTeam
end end
if group if group
group.group_members.each do |member| group.members.non_request.each do |member|
if member.user_id == user_id if member.user_id == user_id
access << member.access_field if member.access_field access << member.access_field if member.access_field
break break
...@@ -173,6 +160,7 @@ class ProjectTeam ...@@ -173,6 +160,7 @@ class ProjectTeam
access.compact.max access.compact.max
end end
private
def max_invited_level(user_id) def max_invited_level(user_id)
project.project_group_links.map do |group_link| project.project_group_links.map do |group_link|
...@@ -189,17 +177,15 @@ class ProjectTeam ...@@ -189,17 +177,15 @@ class ProjectTeam
end.compact.max end.compact.max
end end
private
def fetch_members(level = nil) def fetch_members(level = nil)
project_members = project.project_members project_members = project.members.non_request
group_members = group ? group.group_members : [] group_members = group ? group.members.non_request : []
invited_members = [] invited_members = []
if project.invited_groups.any? && project.allowed_to_share_with_group? if project.invited_groups.any? && project.allowed_to_share_with_group?
project.project_group_links.each do |group_link| project.project_group_links.each do |group_link|
invited_group = group_link.group invited_group = group_link.group
im = invited_group.group_members im = invited_group.members.non_request
if level if level
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
......
...@@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base
ASSIGNED = 1 ASSIGNED = 1
MENTIONED = 2 MENTIONED = 2
BUILD_FAILED = 3 BUILD_FAILED = 3
MARKED = 4
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :note belongs_to :note
......
...@@ -56,8 +56,7 @@ class User < ActiveRecord::Base ...@@ -56,8 +56,7 @@ class User < ActiveRecord::Base
# Groups # Groups
has_many :members, dependent: :destroy has_many :members, dependent: :destroy
has_many :project_members, source: 'ProjectMember' has_many :group_members, dependent: :destroy, source: 'GroupMember'
has_many :group_members, source: 'GroupMember'
has_many :groups, through: :group_members has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group 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 has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
...@@ -65,13 +64,13 @@ class User < ActiveRecord::Base ...@@ -65,13 +64,13 @@ class User < ActiveRecord::Base
# Projects # Projects
has_many :groups_projects, through: :groups, source: :projects has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, 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 :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project has_many :starred_projects, through: :users_star_projects, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet" 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 :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, 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 has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
......
...@@ -173,16 +173,26 @@ class NotificationService ...@@ -173,16 +173,26 @@ class NotificationService
end end
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) 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 end
def accept_project_invite(project_member) 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 end
def decline_project_invite(project_member) 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.project.id,
project_member.invite_email, project_member.invite_email,
project_member.access_level, project_member.access_level,
...@@ -191,23 +201,33 @@ class NotificationService ...@@ -191,23 +201,33 @@ class NotificationService
end end
def new_project_member(project_member) 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 end
def update_project_member(project_member) 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 end
def invite_group_member(group_member, token) 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 end
def accept_group_invite(group_member) 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 end
def decline_group_invite(group_member) 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.group.id,
group_member.invite_email, group_member.invite_email,
group_member.access_level, group_member.access_level,
...@@ -216,11 +236,11 @@ class NotificationService ...@@ -216,11 +236,11 @@ class NotificationService
end end
def new_group_member(group_member) 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 end
def update_group_member(group_member) 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 end
def project_was_moved(project, old_path_with_namespace) def project_was_moved(project, old_path_with_namespace)
......
...@@ -139,10 +139,16 @@ class TodoService ...@@ -139,10 +139,16 @@ class TodoService
pending_todos(user, attributes).update_all(state: :done) pending_todos(user, attributes).update_all(state: :done)
end 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 private
def create_todos(users, attributes) def create_todos(users, attributes)
Array(users).each do |user| Array(users).map do |user|
next if pending_todos(user, attributes).exists? next if pending_todos(user, attributes).exists?
Todo.create(attributes.merge(user_id: user.id)) Todo.create(attributes.merge(user_id: user.id))
end end
......
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
%span.pull-right.light %span.pull-right.light
= member.human_access = member.human_access
- if can?(current_user, :destroy_group_member, member) - 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 %i.fa.fa-minus.fa-inverse
.panel-footer .panel-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab' = paginate @members, param_name: 'members_page', theme: 'gitlab'
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
%ul.well-list %ul.well-list
- @group_members.each do |member| - @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 .panel-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab' = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
...@@ -172,7 +172,7 @@ ...@@ -172,7 +172,7 @@
%span.light Owner %span.light Owner
- else - else
%span.light= project_member.human_access %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 %i.fa.fa-times
.panel-footer .panel-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.pull-right .pull-right
%span.light= group_member.human_access %span.light= group_member.human_access
- unless group_member.owner? - 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 %i.fa.fa-times.fa-inverse
- else - else
.nothing-here-block This user has no groups. .nothing-here-block This user has no groups.
...@@ -38,6 +38,5 @@ ...@@ -38,6 +38,5 @@
%span.light= member.human_access %span.light= member.human_access
- if member.respond_to? :project - 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 %i.fa.fa-times
...@@ -6,12 +6,13 @@ ...@@ -6,12 +6,13 @@
.panel-heading .panel-heading
Add new user to group Add new user to group
.panel-body .panel-body
- if should_user_see_group_roles?(current_user, @group)
%p.light %p.light
Members of group have access to all group projects. Members of group have access to all group projects.
.new-group-member-holder .new-group-member-holder
= render "new_group_member" = render "new_group_member"
= render 'shared/members/requests', membership_source: @group, members: @members.request
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
%strong #{@group.name} %strong #{@group.name}
...@@ -25,9 +26,8 @@ ...@@ -25,9 +26,8 @@
= button_tag class: 'btn', title: 'Search' do = button_tag class: 'btn', title: 'Search' do
= icon("search") = icon("search")
%ul.content-list %ul.content-list
- @members.each do |member| = render partial: 'shared/members/member', collection: @members.non_request, as: :member
= render 'groups/group_members/group_member', member: member, show_controls: true = paginate @members.non_request, theme: 'gitlab'
= paginate @members, theme: 'gitlab'
:javascript :javascript
$('form.member-search-form').on('submit', function(event) { $('form.member-search-form').on('submit', function(event) {
......
:plain :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 @@ ...@@ -19,6 +19,9 @@
.cover-desc.description .cover-desc.description
= markdown(@group.description, pipeline: :description) = markdown(@group.description, pipeline: :description)
- if current_user
= render 'shared/members/access_request_buttons', source: @group
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
%ul.nav-links %ul.nav-links
......
...@@ -27,8 +27,7 @@ ...@@ -27,8 +27,7 @@
%li %li
= link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw') = icon('bell fw')
- unless todos_pending_count == 0 %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
%span.badge.todos-pending-count
= todos_pending_count = todos_pending_count
- if current_user.can_create_project? - if current_user.can_create_project?
%li %li
......
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = 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 = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
.icon-container
= navbar_icon('project') = navbar_icon('project')
%span %span
Projects Projects
= nav_link(controller: :todos) do = nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do = link_to dashboard_todos_path, title: 'Todos' do
.icon-container
= icon('bell fw') = icon('bell fw')
%span %span
Todos Todos
%span.count= number_with_delimiter(todos_pending_count) %span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do = nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
.icon-container
= navbar_icon('activity') = navbar_icon('activity')
%span %span
Activity Activity
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do = link_to dashboard_groups_path, title: 'Groups' do
.icon-container
= navbar_icon('group') = navbar_icon('group')
%span %span
Groups Groups
= nav_link(controller: 'dashboard/milestones') do = nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do = link_to dashboard_milestones_path, title: 'Milestones' do
.icon-container
= navbar_icon('milestones') = navbar_icon('milestones')
%span %span
Milestones Milestones
= nav_link(path: 'dashboard#issues') do = nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
.icon-container
= navbar_icon('issues') = navbar_icon('issues')
%span %span
Issues Issues
%span.count= number_with_delimiter(current_user.assigned_issues.opened.count) %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do = nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.icon-container
= navbar_icon('mr') = navbar_icon('mr')
%span %span
Merge Requests Merge Requests
%span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do = nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do = link_to dashboard_snippets_path, title: 'Snippets' do
.icon-container
= icon('clipboard fw') = icon('clipboard fw')
%span %span
Snippets Snippets
= nav_link(controller: :help) do = nav_link(controller: :help) do
= link_to help_path, title: 'Help' do = link_to help_path, title: 'Help' do
.icon-container
= icon('question-circle fw') = icon('question-circle fw')
%span %span
Help Help
= nav_link(html_options: {class: profile_tab_class}) do = nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
.icon-container
= icon('user fw') = icon('user fw')
%span %span
Profile Settings Profile Settings
...@@ -14,7 +14,3 @@ ...@@ -14,7 +14,3 @@
%li %li
= link_to edit_group_path(@group) do = link_to edit_group_path(@group) do
Edit Group 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 - if current_user
- access = user_max_access_in_project(current_user.id, @project)
- can_edit = can?(current_user, :admin_project, @project)
.controls .controls
.dropdown.project-settings-dropdown .dropdown.project-settings-dropdown
%a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
= icon('cog') = icon('cog')
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %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 = render 'layouts/nav/project_settings', access: access, can_edit: can_edit
- if can_edit || access - if can_edit || access
%li.divider %li.divider
- if can_edit - if can_edit
...@@ -16,8 +18,8 @@ ...@@ -16,8 +18,8 @@
Edit Project Edit Project
- if access - if access
%li %li
= link_to leave_namespace_project_project_members_path(@project.namespace, @project), = link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project Leave Project
%div{ class: nav_control_class } %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 %p
You have been invited You have been invited
- if inviter = @group_member.created_by - if member.created_by
by by
= link_to inviter.name, user_url(inviter) = link_to member.created_by.name, user_url(member.created_by)
to join group to join the
= link_to @group.name, group_url(@group) = link_to member_source.human_name, member_source.web_url
as #{@group_member.human_access}. #{member_source.model_name.singular} as #{member.human_access}.
%p %p
= link_to 'Accept invitation', invite_url(@token) = link_to 'Accept invitation', invite_url(@token)
or or
= link_to 'decline', decline_invite_url(@token) = 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) %> Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_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,7 +29,10 @@ ...@@ -29,7 +29,10 @@
.project-clone-holder .project-clone-holder
= render "shared/clone_panel" = render "shared/clone_panel"
.project-repo-buttons.btn-group.project-right-buttons .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/download"
= render 'projects/buttons/dropdown' = render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications' = render 'projects/buttons/notifications'
......
- if @notification_setting - 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| = 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 = f.hidden_field :level
.dropdown .dropdown.hidden-sm
%button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } } %button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } }
= icon('bell') = icon('bell')
= notification_title(@notification_setting.level) = notification_title(@notification_setting.level)
......
...@@ -6,11 +6,14 @@ ...@@ -6,11 +6,14 @@
(#{members.count}) (#{members.count})
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
.controls .controls
= link_to group_group_members_path(@group), class: 'btn' do = link_to 'Manage group members',
Manage group members group_group_members_path(@group),
class: 'btn'
%ul.content-list %ul.content-list
- members.limit(20).each do |member| = render partial: 'shared/members/member',
= render 'groups/group_members/group_member', member: member, show_controls: false collection: members.limit(20),
- if members.count > 20 as: :member,
locals: { show_controls: false }
- if members.size > 20
%li %li
and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)} and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.form-group .form-group
= f.label :access_level, "Project Access", class: 'control-label' = f.label :access_level, "Project Access", class: 'control-label'
.col-sm-10 .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 .help-block
Read more about role permissions Read more about role permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" %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 @@ ...@@ -14,8 +14,10 @@
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
Edit group members Edit group members
%ul.content-list %ul.content-list
- shared_group.group_members.order('access_level DESC').limit(20).each do |member| = render partial: 'shared/members/member',
= render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false 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 - if shared_group_users_count > 20
%li %li
and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)} 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 @@ ...@@ -11,8 +11,7 @@
= button_tag class: 'btn', title: 'Search' do = button_tag class: 'btn', title: 'Search' do
= icon("search") = icon("search")
%ul.content-list %ul.content-list
- members.each do |project_member| = render partial: 'shared/members/member', collection: members, as: :member
= render 'project_member', member: project_member
:javascript :javascript
$('form.member-search-form').on('submit', function (event) { $('form.member-search-form').on('submit', function (event) {
......
...@@ -13,7 +13,9 @@ ...@@ -13,7 +13,9 @@
Users with access to this project are listed below. Users with access to this project are listed below.
= render "new_project_member" = 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 - if @group
= render "group_members", members: @group_members = render "group_members", members: @group_members
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= link_to edit_group_path(group), class: "btn" do = link_to edit_group_path(group), class: "btn" do
= icon('cogs') = 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') = icon('sign-out')
.stats .stats
......
- todo = has_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class } %aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar .issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header .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 = 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| = 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 .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_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)} %li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
%span{class: ("list-item-name" if show_controls)} %span{ class: ("list-item-name" if show_controls) }
- if member.user - if user
= image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
%strong %strong
= link_to user.name, user_path(user) = link_to user.name, user_path(user)
%span.cgray= user.username %span.cgray= user.username
- if user == current_user - if user == current_user
%span.label.label-success It's you %span.label.label-success It's you
- if user.blocked? - if user.blocked?
%label.label.label-danger %label.label.label-danger
%strong Blocked %strong Blocked
- if member.request?
%span.cgray
– Requested
= time_ago_with_tooltip(member.requested_at)
- else - else
= image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
%strong %strong= member.invite_email
= member.invite_email
%span.cgray %span.cgray
invited – Invited
- if member.created_by - if member.created_by
by by
= link_to member.created_by.name, user_path(member.created_by) = link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at) = time_ago_with_tooltip(member.created_at)
- if show_controls && can?(current_user, :admin_group_member, @group) - if show_controls && can?(current_user, action_member_permission(:admin, member), member.source)
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
Resend invite 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 %span.pull-right
%strong.member-access-level= member.human_access %strong= member.human_access
- if show_controls - if show_controls
- if can?(current_user, :update_group_member, member) - if can?(current_user, action_member_permission(:update, member), member)
= button_tag class: "btn-xs btn btn-grouped inline js-toggle-button", = button_tag icon('pencil'),
title: 'Edit access level', type: 'button' do type: 'button',
= icon('pencil') 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; &nbsp;
- if current_user == user - 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 = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
= icon("sign-out") method: :delete,
Leave data: { confirm: leave_confirmation_message(member.source) },
class: 'btn-xs btn btn-remove'
- else - 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 = link_to icon('trash'), member,
= icon('trash') 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 .edit-member.hide.js-toggle-content
%br %br
= form_for [@group, member], remote: true do |f| = form_for member, remote: true do |f|
.prepend-top-10 .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 .prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm' = 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 ...@@ -6,7 +6,7 @@ class StuckCiBuildsWorker
def perform def perform
Rails.logger.info 'Cleaning stuck builds' 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| builds.find_each(batch_size: 50).each do |build|
Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}" Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
build.drop build.drop
......
...@@ -30,6 +30,11 @@ Rails.application.routes.draw do ...@@ -30,6 +30,11 @@ Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: '/rails/letter_opener' mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
end end
concern :access_requestable do
post :request_access, on: :collection
post :approve_access_request, on: :member
end
namespace :ci do namespace :ci do
# CI API # CI API
Ci::API::API.logger Rails.logger Ci::API::API.logger Rails.logger
...@@ -409,7 +414,7 @@ Rails.application.routes.draw do ...@@ -409,7 +414,7 @@ Rails.application.routes.draw do
end end
scope module: :groups do 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 post :resend_invite, on: :member
delete :leave, on: :collection delete :leave, on: :collection
end end
...@@ -766,7 +771,7 @@ Rails.application.routes.draw do ...@@ -766,7 +771,7 @@ Rails.application.routes.draw do
end end
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 collection do
delete :leave delete :leave
...@@ -790,6 +795,8 @@ Rails.application.routes.draw do ...@@ -790,6 +795,8 @@ Rails.application.routes.draw do
end end
end end
resources :todos, only: [:create, :update], constraints: { id: /\d+/ }
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
collection do collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } 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 ...@@ -537,6 +537,7 @@ ActiveRecord::Schema.define(version: 20160610301627) do
t.string "invite_email" t.string "invite_email"
t.string "invite_token" t.string "invite_token"
t.datetime "invite_accepted_at" t.datetime "invite_accepted_at"
t.datetime "requested_at"
end end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
......
...@@ -147,7 +147,8 @@ bt ...@@ -147,7 +147,8 @@ bt
To output a backtrace from all threads at once: 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 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 ...@@ -62,6 +62,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
end end
step 'I should see the "Can not leave message"' do 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
end end
...@@ -53,7 +53,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -53,7 +53,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
page.within '.content-list' do page.within '.content-list' do
expect(page).to have_content('sjobs@apple.com') 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') expect(page).to have_content('Reporter')
end end
end end
...@@ -116,23 +116,19 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -116,23 +116,19 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member member = mary_jane_member
page.within "#group_member_#{member.id}" do page.within "#group_member_#{member.id}" do
find(".js-toggle-button").click click_button "Edit access level"
page.within "#edit_group_member_#{member.id}" do
select 'Developer', from: 'group_member_access_level' select 'Developer', from: 'group_member_access_level'
click_on 'Save' click_on 'Save'
end end
end end
end
step 'I should see "Mary Jane" as "Developer"' do step 'I should see "Mary Jane" as "Developer"' do
member = mary_jane_member member = mary_jane_member
page.within "#group_member_#{member.id}" do page.within "#group_member_#{member.id}" do
page.within '.member-access-level' do
expect(page).to have_content "Developer" expect(page).to have_content "Developer"
end end
end end
end
private private
......
...@@ -26,8 +26,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -26,8 +26,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
step 'I should see "Mike" in team list as "Reporter"' do 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('Mike')
expect(page).to have_content('Reporter')
end end
end end
...@@ -40,16 +43,20 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -40,16 +43,20 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do 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('sjobs@apple.com')
expect(page).to have_content('invited') expect(page).to have_content('Invited')
expect(page).to have_content('Reporter') expect(page).to have_content('Reporter')
end end
end end
step 'I should see "Dmitriy" in team list as "Developer"' do 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('Dmitriy')
expect(page).to have_content('Developer')
end end
end end
...@@ -65,15 +72,14 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -65,15 +72,14 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
step 'I should see "Dmitriy" in team list as "Reporter"' do 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('Dmitriy')
expect(page).to have_content('Reporter')
end end
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 step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy") user = User.find_by(name: "Dmitriy")
expect(page).not_to have_content(user.name) expect(page).not_to have_content(user.name)
...@@ -120,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -120,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy') user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id) project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do page.within "#project_member_#{project_member.id}" do
click_link('Remove user from team') click_link('Remove user from project')
end end
end end
......
...@@ -88,10 +88,7 @@ module API ...@@ -88,10 +88,7 @@ module API
class Group < Grape::Entity class Group < Grape::Entity
expose :id, :name, :path, :description, :visibility_level expose :id, :name, :path, :description, :visibility_level
expose :avatar_url expose :avatar_url
expose :web_url
expose :web_url do |group, options|
Gitlab::Routing.url_helpers.group_url(group)
end
end end
class GroupDetail < Group class GroupDetail < Group
......
...@@ -46,7 +46,7 @@ module API ...@@ -46,7 +46,7 @@ module API
required_attributes! [:user_id, :access_level] required_attributes! [:user_id, :access_level]
# either the user is already a team member or a new one # 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? if project_member.nil?
project_member = user_project.project_members.new( project_member = user_project.project_members.new(
user_id: params[:user_id], user_id: params[:user_id],
......
...@@ -66,7 +66,11 @@ module Gitlab ...@@ -66,7 +66,11 @@ module Gitlab
def sample_objects def sample_objects
sample = Allocations.to_hash sample = Allocations.to_hash
counts = sample.each_with_object({}) do |(klass, count), hash| counts = sample.each_with_object({}) do |(klass, count), hash|
hash[klass.name] = count name = klass.name
next unless name
hash[name] = count
end end
# Symbols aren't allocated so we'll need to add those manually. # Symbols aren't allocated so we'll need to add those manually.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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