summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG2
-rw-r--r--app/assets/javascripts/application.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js12
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js (renamed from app/assets/javascripts/blob/edit_blob.js)0
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es62
-rw-r--r--app/assets/javascripts/boards/components/board.js.es64
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es64
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es65
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/lib/ace.js2
-rw-r--r--app/assets/javascripts/member_expiration_date.js32
-rw-r--r--app/assets/javascripts/project_members.js3
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js12
-rw-r--r--app/assets/stylesheets/pages/boards.scss29
-rw-r--r--app/assets/stylesheets/pages/projects.scss26
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/project_members_controller.rb9
-rw-r--r--app/helpers/blob_helper.rb8
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/concerns/expirable.rb15
-rw-r--r--app/models/group.rb24
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/project_member.rb10
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_group_link.rb4
-rw-r--r--app/models/project_team.rb13
-rw-r--r--app/services/members/authorized_destroy_service.rb19
-rw-r--r--app/services/members/destroy_service.rb7
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml9
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/projects/blob/edit.html.haml9
-rw-r--r--app/views/projects/blob/new.html.haml9
-rw-r--r--app/views/projects/boards/components/_board.html.haml1
-rw-r--r--app/views/projects/boards/components/_card.html.haml1
-rw-r--r--app/views/projects/group_links/index.html.haml11
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml9
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/shared/members/_member.html.haml20
-rw-r--r--app/views/shared/snippets/_form.html.haml9
-rw-r--r--app/workers/remove_expired_group_links_worker.rb7
-rw-r--r--app/workers/remove_expired_members_worker.rb13
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--db/migrate/20160801163421_add_expires_at_to_member.rb29
-rw-r--r--db/migrate/20160818205718_add_expires_at_to_project_group_links.rb29
-rw-r--r--db/schema.rb6
-rw-r--r--doc/api/members.md5
-rw-r--r--doc/workflow/share_projects_with_other_groups.md18
-rw-r--r--features/steps/group/members.rb4
-rw-r--r--features/steps/project/source/browse_files.rb8
-rw-r--r--features/steps/project/team_management.rb4
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/members.rb9
-rw-r--r--lib/gitlab/diff/position.rb18
-rw-r--r--spec/features/projects/group_links_spec.rb32
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb45
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb42
-rw-r--r--spec/mailers/notify_spec.rb14
-rw-r--r--spec/models/member_spec.rb14
-rw-r--r--spec/requests/api/members_spec.rb6
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb24
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb58
65 files changed, 620 insertions, 131 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 62f1953b720..d78c51fbbc3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -8,6 +8,7 @@ v 8.11.0 (unreleased)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
- Add delimiter to project stars and forks count (ClemMakesApps)
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
+ - Fix adding line comments on the initial commit to a repo !5900
- Fix the title of the toggle dropdown button. !5515 (herminiotorres)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948
@@ -134,6 +135,7 @@ v 8.11.0 (unreleased)
- Change requests_profiles resource constraint to catch virtually any file
- Bump gitlab_git to lazy load compare commits
- Reduce number of queries made for merge_requests/:id/diffs
+ - Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski)
- Sensible state specific default sort order for issues and merge requests !5453 (tomb0y)
- Fix bug where destroying a namespace would not always destroy projects
- Fix RequestProfiler::Middleware error when code is reloaded in development
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index c77983896a3..fc354dfd677 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -26,8 +26,6 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
-/*= require ace-rails-ap */
-/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
/*= require mousetrap */
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
new file mode 100644
index 00000000000..2afef43f3d6
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var url = $(".js-edit-blob-form").data("relative-url-root");
+ url += $(".js-edit-blob-form").data("assets-prefix");
+
+ var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
+ new NewCommitForm($('.js-edit-blob-form'));
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 649c79daee8..649c79daee8 100644
--- a/app/assets/javascripts/blob/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 2c65d4427be..a612cf0f1ae 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -38,7 +38,7 @@ $(() => {
ready () {
Store.disabled = this.disabled;
gl.boardService.all()
- .then((resp) => {
+ .then((resp) => {
resp.json().forEach((board) => {
const list = Store.addList(board);
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 5ef6a1b2277..d7f4107cb02 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -72,10 +72,6 @@
}
});
- if (bp.getBreakpointSize() === 'xs') {
- options.handle = '.js-board-drag-handle';
- }
-
this.sortable = Sortable.create(this.$el.parentNode, options);
},
beforeDestroy () {
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index dceacb25452..a6644e9eb8c 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -74,10 +74,6 @@
}
});
- if (bp.getBreakpointSize() === 'xs') {
- options.handle = '.js-card-drag-handle';
- }
-
this.sortable = Sortable.create(this.$els.list, options);
// Scroll event on list to load more
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index 8e8d13ede5a..44addb3ea98 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -13,6 +13,8 @@
document.body.classList.remove('is-dragging');
};
+ gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
let defaultSortOptions = {
forceFallback: true,
@@ -20,7 +22,8 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.has-tooltip',
- scrollSensitivity: 100,
+ delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 32e3aa62358..ba64d2bcf0b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -129,10 +129,12 @@
new NotificationsDropdown();
break;
case 'groups:group_members:index':
+ new gl.MemberExpirationDate();
new GroupMembers();
new UsersSelect();
break;
case 'projects:project_members:index':
+ new gl.MemberExpirationDate();
new ProjectMembers();
new UsersSelect();
break;
@@ -174,6 +176,7 @@
new BuildArtifacts();
break;
case 'projects:group_links:index':
+ new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show':
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
new file mode 100644
index 00000000000..4cdf99cae72
--- /dev/null
+++ b/app/assets/javascripts/lib/ace.js
@@ -0,0 +1,2 @@
+/*= require ace-rails-ap */
+/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 00000000000..1935af491f7
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,32 @@
+(function() {
+ // Add datepickers to all `js-access-expiration-date` elements. If those elements are
+ // children of an element with the `clearable-input` class, and have a sibling
+ // `js-clear-input` element, then show that element when there is a value in the
+ // datepicker, and make clicking on that element clear the field.
+ //
+ gl.MemberExpirationDate = function() {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+
+ var inputs = $('.js-access-expiration-date');
+
+ inputs.datepicker({
+ dateFormat: 'yy-mm-dd',
+ minDate: 1,
+ onSelect: toggleClearInput
+ });
+
+ inputs.next('.js-clear-input').on('click', function(event) {
+ event.preventDefault();
+
+ var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+ input.datepicker('setDate', null);
+ toggleClearInput.call(input);
+ });
+
+ inputs.on('blur', toggleClearInput);
+
+ inputs.each(toggleClearInput);
+ };
+}).call(this);
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
index f6a796b325a..78f7b48bc7d 100644
--- a/app/assets/javascripts/project_members.js
+++ b/app/assets/javascripts/project_members.js
@@ -5,9 +5,6 @@
return $(this).fadeOut();
});
}
-
return ProjectMembers;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
new file mode 100644
index 00000000000..855e97eb301
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var editor = ace.edit("editor")
+
+ $(".snippet-form-holder form").on('submit', function() {
+ $(".snippet-file-content").val(editor.getValue());
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index bea9ac75715..9ac4d801ac4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -105,8 +105,8 @@
.board {
display: -webkit-flex;
display: flex;
- min-width: calc(100vw - 15px);
- max-width: calc(100vw - 15px);
+ min-width: calc(85vw - 15px);
+ max-width: calc(85vw - 15px);
margin-bottom: 25px;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
@@ -158,14 +158,6 @@
padding: $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
-
- .board-mobile-handle {
- position: relative;
- left: 0;
- top: 1px;
- margin-top: 0;
- margin-right: 5px;
- }
}
.board-search-container {
@@ -268,11 +260,7 @@
list-style: none;
&.user-can-drag {
- padding-left: ($gl-padding * 2);
-
- @media (min-width: $screen-sm-min) {
- padding-left: $gl-padding;
- }
+ padding-left: $gl-padding;
}
&:not(:last-child) {
@@ -293,17 +281,6 @@
}
}
-.board-mobile-handle {
- position: absolute;
- left: 10px;
- top: 50%;
- margin-top: (-15px / 2);
-
- @media (min-width: $screen-sm-min) {
- display: none;
- }
-}
-
.card-title {
margin: 0;
font-size: 1em;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 27dc2b2a1fa..eaf2d3270b3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -719,3 +719,29 @@ pre.light-well {
width: 300px;
}
}
+
+.clearable-input {
+ position: relative;
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ position: absolute;
+ right: 7px;
+ top: 7px;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ &.has-value {
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+}
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4ce18321649..cdfa8d91a28 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9fc41a12536..272164cd0cc 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ current_user: current_user,
+ expires_at: params[:expires_at]
+ )
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
@@ -63,7 +68,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
def member_params
- params.require(:group_member).permit(:access_level, :user_id)
+ params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 606552fa853..d0c4550733c 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
- group: group, group_access: params[:link_group_access]
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
)
redirect_to namespace_project_group_links_path(project.namespace, project)
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3435a118964..42a7e5a2c30 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @project.team.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ expires_at: params[:expires_at],
+ current_user: current_user
+ )
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
@@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
def member_params
- params.require(:project_member).permit(:user_id, :access_level)
+ params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 9ea03720c1e..e13b7cdd707 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -217,4 +217,12 @@ module BlobHelper
def gitlab_ci_ymls
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
end
+
+ def blob_editor_paths
+ {
+ 'relative-url-root' => Rails.application.config.relative_url_root,
+ 'assets-prefix' => Gitlab::Application.config.assets.prefix,
+ 'blob-language' => @blob && @blob.language.try(:ace_mode)
+ }
+ end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index cc413448ce8..817d063e4a2 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -229,7 +229,7 @@ class Commit
def diff_refs
Gitlab::Diff::DiffRefs.new(
- base_sha: self.parent_id || self.sha,
+ base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
head_sha: self.sha
)
end
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
new file mode 100644
index 00000000000..be93435453b
--- /dev/null
+++ b/app/models/concerns/expirable.rb
@@ -0,0 +1,15 @@
+module Expirable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :expired, -> { where('expires_at <= ?', Time.current) }
+ end
+
+ def expires?
+ expires_at.present?
+ end
+
+ def expires_soon?
+ expires_at < 7.days.from_now
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index 37631b99701..c48869ae465 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,34 +95,40 @@ class Group < Namespace
end
end
- def add_users(user_ids, access_level, current_user = nil)
+ def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
- Member.add_user(self.group_members, user_id, access_level, current_user)
+ Member.add_user(
+ self.group_members,
+ user_id,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
- def add_user(user, access_level, current_user = nil)
- add_users([user], access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ add_users([user], access_level, current_user: current_user, expires_at: expires_at)
end
def add_guest(user, current_user = nil)
- add_user(user, Gitlab::Access::GUEST, current_user)
+ add_user(user, Gitlab::Access::GUEST, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, Gitlab::Access::REPORTER, current_user)
+ add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, Gitlab::Access::DEVELOPER, current_user)
+ add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
end
def add_master(user, current_user = nil)
- add_user(user, Gitlab::Access::MASTER, current_user)
+ add_user(user, Gitlab::Access::MASTER, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, Gitlab::Access::OWNER, current_user)
+ add_user(user, Gitlab::Access::OWNER, current_user: current_user)
end
def has_owner?(user)
diff --git a/app/models/member.rb b/app/models/member.rb
index 24ab1276ee9..64e0d33fb20 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
class Member < ActiveRecord::Base
include Sortable
include Importable
+ include Expirable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -73,7 +74,7 @@ class Member < ActiveRecord::Base
user
end
- def add_user(members, user_id, access_level, current_user = nil)
+ def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited
@@ -87,6 +88,7 @@ class Member < ActiveRecord::Base
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
+ member.expires_at = expires_at
member.save
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 18e97c969d7..ec2d40eb11c 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -34,7 +34,7 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_to_projects(project_ids, user_ids, access, current_user = nil)
+ def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
@@ -50,7 +50,13 @@ class ProjectMember < Member
project = Project.find(project_id)
users.each do |user|
- Member.add_user(project.project_members, user, access_level, current_user)
+ Member.add_user(
+ project.project_members,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 043da030468..f9c48a546e6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1003,8 +1003,8 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
- def add_user(user, access_level, current_user = nil)
- team.add_user(user, access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
end
def default_branch
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index e52a6bd7c84..7613cbdea93 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -1,4 +1,6 @@
class ProjectGroupLink < ActiveRecord::Base
+ include Expirable
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
self.class.access_options.key(self.group_access)
end
- private
+ private
def different_group
if self.group && self.project && self.project.group == self.group
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d0a714cd6fc..ab6ea2aae36 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -15,9 +15,9 @@ class ProjectTeam
users, access, current_user = *args
if users.respond_to?(:each)
- add_users(users, access, current_user)
+ add_users(users, access, current_user: current_user)
else
- add_user(users, access, current_user)
+ add_user(users, access, current_user: current_user)
end
end
@@ -33,17 +33,18 @@ class ProjectTeam
member
end
- def add_users(users, access, current_user = nil)
+ def add_users(users, access, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects(
[project.id],
users,
access,
- current_user
+ current_user: current_user,
+ expires_at: expires_at
)
end
- def add_user(user, access, current_user = nil)
- add_users([user], access, current_user)
+ def add_user(user, access, current_user: nil, expires_at: nil)
+ add_users([user], access, current_user: current_user, expires_at: expires_at)
end
# Remove all users from project team
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
new file mode 100644
index 00000000000..ca9db59cac7
--- /dev/null
+++ b/app/services/members/authorized_destroy_service.rb
@@ -0,0 +1,19 @@
+module Members
+ class AuthorizedDestroyService < BaseService
+ attr_accessor :member, :user
+
+ def initialize(member, user = nil)
+ @member, @user = member, user
+ end
+
+ def execute
+ return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+
+ member.destroy
+
+ if member.request? && member.user != user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 9e3f6af628d..9a2bf82ef51 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -11,12 +11,7 @@ module Members
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
-
- member.destroy
-
- if member.request? && member.user != current_user
- notification_service.decline_access_request(member)
- end
+ AuthorizedDestroyService.new(member, current_user).execute
end
end
end
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 9bb9f962177..2fb3190ab11 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this group and all of its projects.
+
.form-actions
= f.submit 'Add users to group', class: "btn btn-create"
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index da71de4cd1e..742f9d7a433 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 7b0621f9401..680e95ac6b5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,4 +1,7 @@
- page_title "Edit", @blob.path, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
- if @conflict
.alert.alert-danger
@@ -16,14 +19,10 @@
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
- new NewCommitForm($('.js-edit-blob-form'))
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index c952bc7e5db..b6ed9518c48 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,17 +1,16 @@
- page_title "New File", @path.presence, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
%h3.page-title
New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
- new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index f8ebf397ee2..de53a298f84 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -11,7 +11,6 @@
.board-inner
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
- = icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)")
{{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" }
{{ list.issues.length }}
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index b20c23f6b8e..e8b60b54d80 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -9,7 +9,6 @@
"track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled }",
":index" => "index" }
- = icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled")
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 2b904544f28..ca700cb3a3b 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -17,6 +17,13 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret
+ .form-group
+ = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, all users in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
@@ -35,6 +42,10 @@
= group.name
%br
up to #{group_link.human_access}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
%span.sr-only disable sharing
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 978c4dfc5ec..fa8cbf71733 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this project.
+
.form-actions
= f.submit 'Add users to project', class: "btn btn-create"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9031f01b496..9d063b3081f 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Members"
-.project-members-page.prepend-top-default
+.project-members-page.js-project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 45f8ef89060..833954bc039 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index fc6e206d082..5f20e4bd42a 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -16,7 +16,7 @@
= button_tag icon('pencil'),
type: 'button',
class: 'btn inline js-toggle-button',
- title: 'Edit access level'
+ title: 'Edit'
- if member.request?
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
@@ -59,6 +59,10 @@
= time_ago_with_tooltip(member.requested_at)
- else
Joined #{time_ago_with_tooltip(member.created_at)}
+ - if member.expires?
+ ·
+ %span{ class: ('text-warning' if member.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
@@ -73,8 +77,16 @@
- if show_roles
.edit-member.hide.js-toggle-content
%br
- = form_for member, remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
+ = form_for member, remote: true, html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
+ .col-sm-10
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
+ .form-group
+ = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
+ %i.clear-icon.js-clear-input
.prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 47ec09f62c6..0c788032020 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,3 +1,7 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('snippet/snippet_bundle.js')
+
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
= form_errors(@snippet)
@@ -31,8 +35,3 @@
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
-:javascript
- var editor = ace.edit("editor");
- $(".snippet-form-holder form").submit(function(){
- $(".snippet-file-content").val(editor.getValue());
- });
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
new file mode 100644
index 00000000000..246c8b6650a
--- /dev/null
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -0,0 +1,7 @@
+class RemoveExpiredGroupLinksWorker
+ include Sidekiq::Worker
+
+ def perform
+ ProjectGroupLink.expired.destroy_all
+ end
+end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
new file mode 100644
index 00000000000..cf765af97ce
--- /dev/null
+++ b/app/workers/remove_expired_members_worker.rb
@@ -0,0 +1,13 @@
+class RemoveExpiredMembersWorker
+ include Sidekiq::Worker
+
+ def perform
+ Member.expired.find_each do |member|
+ begin
+ Members::AuthorizedDestroyService.new(member).execute
+ rescue => ex
+ logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
+ end
+ end
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index 6b80f8ddafa..4792f6670a8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -88,6 +88,8 @@ module Gitlab
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
+ config.assets.precompile << "blob_edit/blob_edit_bundle.js"
+ config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index deac3b0f0f9..7a9376def02 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -293,6 +293,12 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor
Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
+Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
+Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
#
# GitLab Shell
diff --git a/db/migrate/20160801163421_add_expires_at_to_member.rb b/db/migrate/20160801163421_add_expires_at_to_member.rb
new file mode 100644
index 00000000000..8db0fc60c4b
--- /dev/null
+++ b/db/migrate/20160801163421_add_expires_at_to_member.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddExpiresAtToMember < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :members, :expires_at, :date
+ end
+end
diff --git a/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
new file mode 100644
index 00000000000..0ed538b0df8
--- /dev/null
+++ b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddExpiresAtToProjectGroupLinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :project_group_links, :expires_at, :date
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 82d4590f6b5..748c4adc889 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160817154936) do
+ActiveRecord::Schema.define(version: 20160818205718) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -568,6 +568,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
+ t.date "expires_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
@@ -783,6 +784,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
+ t.date "expires_at"
end
create_table "project_import_data", force: :cascade do |t|
@@ -1149,4 +1151,4 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "u2f_registrations", "users"
-end
+end \ No newline at end of file
diff --git a/doc/api/members.md b/doc/api/members.md
index d002e6eaf89..fd6d728dad2 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -86,7 +86,8 @@ Example response:
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
- "access_level": 30
+ "access_level": 30,
+ "expires_at": null
}
```
@@ -106,6 +107,7 @@ POST /projects/:id/members
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the new member |
| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
@@ -141,6 +143,7 @@ PUT /projects/:id/members/:user_id
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the member |
| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
index 4c59f59c587..8e50cb03e63 100644
--- a/doc/workflow/share_projects_with_other_groups.md
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -1,22 +1,24 @@
# Share Projects with other Groups
-In GitLab Enterprise Edition you can share projects with other groups.
-This makes it possible to add a group of users to a project with a single action.
+You can share projects with other groups. This makes it possible to add a group of users
+to a project with a single action.
## Groups as collections of users
-In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
-In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+Groups are used primarily to [create collections of projects](groups.md), but you can also
+take advantage of the fact that groups define collections of _users_, namely the group
+members.
## Sharing a project with a group of users
-The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
-But what if 'Project Acme' already belongs to another group, say 'Open Source'?
-This is where the (Enterprise Edition only) group sharing feature can be of use.
+The primary mechanism to give a group of users, say 'Engineering', access to a project,
+say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
+Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
-![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index dfa2fa75def..e9b45823c67 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -116,8 +116,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- click_button "Edit access level"
- select 'Developer', from: 'group_member_access_level'
+ click_button 'Edit'
+ select 'Developer', from: "member_access_level_#{member.id}"
click_on 'Save'
end
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 841d191d55b..bb79424ee08 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see its content with new lines preserved at end of file' do
- expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n"
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n"
end
step 'I click link "Raw"' do
@@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I can edit code' do
set_new_content
- expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content
end
step 'I edit code' do
@@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I edit code with new lines at end of file' do
- execute_script('blob.editor.setValue("Sample\n\n\n")')
+ execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
end
step 'I fill the new file name' do
@@ -378,7 +378,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
private
def set_new_content
- execute_script("blob.editor.setValue('#{new_gitignore_content}')")
+ execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
end
# Content of the gitignore file on the seed repository.
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index f32576d2cb1..e920f5a706b 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,8 +65,8 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- click_button "Edit access level"
- select "Reporter", from: "project_member_access_level"
+ click_button 'Edit'
+ select "Reporter", from: "member_access_level_#{project_member.id}"
click_button "Save"
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 67420772335..54ce2dcfa57 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -97,6 +97,10 @@ module API
member = options[:member] || options[:members].find { |m| m.user_id == user.id }
member.access_level
end
+ expose :expires_at do |user, options|
+ member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member.expires_at
+ end
end
class AccessRequester < UserBasic
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 2fae83f60b2..94c16710d9a 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -49,6 +49,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the new member
# access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# POST /groups/:id/members
@@ -72,7 +73,7 @@ module API
conflict!('Member already exists') if source_type == 'group' && member
unless member
- source.add_user(params[:user_id], params[:access_level], current_user)
+ source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
member = source.members.find_by(user_id: params[:user_id])
end
@@ -81,7 +82,7 @@ module API
else
# Since `source.add_user` doesn't return a member object, we have to
# build a new one and populate its errors in order to render them.
- member = source.members.build(attributes_for_keys([:user_id, :access_level]))
+ member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
member.valid? # populate the errors
# This is to ensure back-compatibility but 400 behavior should be used
@@ -97,6 +98,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the member
# access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# PUT /groups/:id/members/:user_id
@@ -107,8 +109,9 @@ module API
required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
+ attrs = attributes_for_keys [:access_level, :expires_at]
- if member.update_attributes(access_level: params[:access_level])
+ if member.update_attributes(attrs)
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 2fdcf8d7838..ecf62dead35 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -139,13 +139,19 @@ module Gitlab
private
def find_diff_file(repository)
- diffs = Gitlab::Git::Compare.new(
- repository.raw_repository,
- start_sha,
- head_sha
- ).diffs(paths: paths)
+ # We're at the initial commit, so just get that as we can't compare to anything.
+ if Gitlab::Git.blank_ref?(start_sha)
+ compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+ else
+ compare = Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ )
+ end
+
+ diff = compare.diffs(paths: paths).first
- diff = diffs.first
return unless diff
Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
new file mode 100644
index 00000000000..1a71a03fbd9
--- /dev/null
+++ b/spec/features/projects/group_links_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+feature 'Project group links', feature: true, js: true do
+ include Select2Helper
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:group) { create(:group) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ context 'setting an expiration date for a group link' do
+ before do
+ visit namespace_project_group_links_path(project.namespace, project)
+
+ select2 group.id, from: '#link_group_id'
+ fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+ page.find('body').click
+ click_on 'Share'
+ end
+
+ it 'shows the expiration time with a warning class' do
+ page.within('.enabled-groups') do
+ expect(page).to have_content('expires in 4 days')
+ expect(page).to have_selector('.text-warning')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
new file mode 100644
index 00000000000..430c384ac2e
--- /dev/null
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
+ include Select2Helper
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:new_member) { create(:user) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'expiration date is displayed in the members list' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.users-project-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
+ fill_in 'expires_at', with: '2016-08-10'
+ click_on 'Add users to project'
+ end
+
+ page.within '.project_member:first-child' do
+ expect(page).to have_content('Expires in 4 days')
+ end
+ end
+ end
+
+ scenario 'change expiration date' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.project_member:first-child' do
+ click_on 'Edit'
+ fill_in 'Access expiration date', with: '2016-08-09'
+ click_on 'Save'
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 10537bea008..6e8fff6f516 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -339,6 +339,48 @@ describe Gitlab::Diff::Position, lib: true do
end
end
+ describe "position for a file in the initial commit" do
+ let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
+
+ subject do
+ described_class.new(
+ old_path: "README.md",
+ new_path: "README.md",
+ old_line: nil,
+ new_line: 1,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+testme")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
describe "#to_json" do
let(:hash) do
{
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index fa241867858..eae9c060c38 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -493,7 +493,12 @@ describe Notify do
end
def invite_to_project(project:, email:, inviter:)
- ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ project.project_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
project.project_members.invite.last
end
@@ -740,7 +745,12 @@ describe Notify do
end
def invite_to_group(group:, email:, inviter:)
- GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ group.group_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
group.group_members.invite.last
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2277f4e13bf..fef90d9b5cb 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -65,11 +65,21 @@ describe Member, models: true do
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
- ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto1@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
accepted_invite_user = build(:user)
- ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto2@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index a56ee30f7b1..1e365bf353a 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -122,12 +122,13 @@ describe API::Members, api: true do
it 'creates a new member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
- user_id: stranger.id, access_level: Member::DEVELOPER
+ user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
expect(response).to have_http_status(201)
end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
end
end
@@ -183,11 +184,12 @@ describe API::Members, api: true do
context 'when authenticated as a master/owner' do
it 'updates the member' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
- access_level: Member::MASTER
+ access_level: Member::MASTER, expires_at: '2016-08-05'
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
end
end
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
new file mode 100644
index 00000000000..689bc3d27b4
--- /dev/null
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe RemoveExpiredGroupLinksWorker do
+ describe '#perform' do
+ let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
+ let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
+ let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
+
+ it 'removes expired group links' do
+ expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
+ expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
+ end
+
+ it 'leaves group links that expire in the future' do
+ subject.perform
+ expect(project_group_link_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves group links that do not expire at all' do
+ subject.perform
+ expect(non_expiring_project_group_link.reload).to be_present
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
new file mode 100644
index 00000000000..402aa1e714e
--- /dev/null
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe RemoveExpiredMembersWorker do
+ let(:worker) { RemoveExpiredMembersWorker.new }
+
+ describe '#perform' do
+ context 'project members' do
+ let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_project_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(project_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_project_member.reload).to be_present
+ end
+ end
+
+ context 'group members' do
+ let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_group_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(group_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_group_member.reload).to be_present
+ end
+ end
+
+ context 'when the last group owner expires' do
+ let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
+
+ it 'does not delete the owner' do
+ worker.perform
+ expect(expired_group_owner.reload).to be_present
+ end
+ end
+ end
+end