Commit 883b96ab authored by Sean McGivern's avatar Sean McGivern

Allow project group links to be expired

parent d2cd9d96
...@@ -173,6 +173,7 @@ ...@@ -173,6 +173,7 @@
new BuildArtifacts(); new BuildArtifacts();
break; break;
case 'projects:group_links:index': case 'projects:group_links:index':
new MemberExpirationDate();
new GroupsSelect(); new GroupsSelect();
break; break;
case 'search:show': case 'search:show':
......
...@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
return render_404 unless can?(current_user, :read_group, group) return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create( 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) redirect_to namespace_project_group_links_path(project.namespace, project)
......
...@@ -36,8 +36,4 @@ module MembersHelper ...@@ -36,8 +36,4 @@ module MembersHelper
"Are you sure you want to leave the " \ "Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?" "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end end
def member_expires_soon?(member)
member.expires_at < 7.days.from_now
end
end end
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
class Member < ActiveRecord::Base class Member < ActiveRecord::Base
include Sortable include Sortable
include Importable include Importable
include Expirable
include Gitlab::Access include Gitlab::Access
attr_accessor :raw_invite_token attr_accessor :raw_invite_token
...@@ -31,7 +32,6 @@ class Member < ActiveRecord::Base ...@@ -31,7 +32,6 @@ class Member < ActiveRecord::Base
scope :non_invite, -> { where(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) } scope :request, -> { where.not(requested_at: nil) }
scope :has_access, -> { where('access_level > 0') } scope :has_access, -> { where('access_level > 0') }
scope :expired, -> { where('expires_at <= ?', Time.current) }
scope :guests, -> { where(access_level: GUEST) } scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) } scope :reporters, -> { where(access_level: REPORTER) }
...@@ -125,10 +125,6 @@ class Member < ActiveRecord::Base ...@@ -125,10 +125,6 @@ class Member < ActiveRecord::Base
invite? || request? invite? || request?
end end
def expires?
expires_at.present?
end
def accept_request def accept_request
return false unless request? return false unless request?
......
class ProjectGroupLink < ActiveRecord::Base class ProjectGroupLink < ActiveRecord::Base
include Expirable
GUEST = 10 GUEST = 10
REPORTER = 20 REPORTER = 20
DEVELOPER = 30 DEVELOPER = 30
...@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base ...@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
self.class.access_options.key(self.group_access) self.class.access_options.key(self.group_access)
end end
private private
def different_group def different_group
if self.group && self.project && self.project.group == self.group if self.group && self.project && self.project.group == self.group
......
...@@ -17,6 +17,11 @@ ...@@ -17,6 +17,11 @@
.select-wrapper .select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret %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
= submit_tag "Share", class: "btn btn-create" = submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3 .col-lg-9.col-lg-offset-3
%hr %hr
...@@ -35,6 +40,10 @@ ...@@ -35,6 +40,10 @@
= group.name = group.name
%br %br
up to #{group_link.human_access} 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 .pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
%span.sr-only disable sharing %span.sr-only disable sharing
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
Joined #{time_ago_with_tooltip(member.created_at)} Joined #{time_ago_with_tooltip(member.created_at)}
- if member.expires? - if member.expires?
· ·
%span{ class: ('text-warning' if member_expires_soon?(member)) } %span{ class: ('text-warning' if member.expires_soon?) }
Expires in #{distance_of_time_in_words_to_now(member.expires_at)} Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else - else
......
class RemoveExpiredGroupLinksWorker
include Sidekiq::Worker
def perform
ProjectGroupLink.expired.destroy_all
end
end
...@@ -296,6 +296,9 @@ Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesW ...@@ -296,6 +296,9 @@ Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesW
Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({}) 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']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker' Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
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'] = 'RemoveExpiredGroupLinksWorker'
# #
# GitLab Shell # GitLab Shell
......
# 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
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160810142633) do ActiveRecord::Schema.define(version: 20160818205718) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -780,6 +780,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -780,6 +780,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "group_access", default: 30, null: false t.integer "group_access", default: 30, null: false
t.date "expires_at"
end end
create_table "project_import_data", force: :cascade do |t| create_table "project_import_data", force: :cascade do |t|
......
# Share Projects with other Groups # Share Projects with other Groups
In GitLab Enterprise Edition you can share projects with other groups. You can share projects with other groups. This makes it possible to add a group of users
This makes it possible to add a group of users to a project with a single action. to a project with a single action.
## Groups as collections of users ## Groups as collections of users
In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md). Groups are used primarily to [create collections of projects](groups.md), but you can also
In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members. take advantage of the fact that groups define collections of _users_, namely the group
members.
## Sharing a project with a group of users ## 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'. The primary mechanism to give a group of users, say 'Engineering', access to a project,
But what if 'Project Acme' already belongs to another group, say 'Open Source'? say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
This is where the (Enterprise Edition only) group sharing feature can be of use. 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. 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. 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. After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
......
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
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
...@@ -14,12 +14,12 @@ describe RemoveExpiredMembersWorker do ...@@ -14,12 +14,12 @@ describe RemoveExpiredMembersWorker do
expect(Member.find_by(id: expired_project_member.id)).to be_nil expect(Member.find_by(id: expired_project_member.id)).to be_nil
end end
it 'leaves members who expire in the future' do it 'leaves members that expire in the future' do
worker.perform worker.perform
expect(project_member_expiring_in_future.reload).to be_present expect(project_member_expiring_in_future.reload).to be_present
end end
it 'leaves members who do not expire at all' do it 'leaves members that do not expire at all' do
worker.perform worker.perform
expect(non_expiring_project_member.reload).to be_present expect(non_expiring_project_member.reload).to be_present
end end
...@@ -35,12 +35,12 @@ describe RemoveExpiredMembersWorker do ...@@ -35,12 +35,12 @@ describe RemoveExpiredMembersWorker do
expect(Member.find_by(id: expired_group_member.id)).to be_nil expect(Member.find_by(id: expired_group_member.id)).to be_nil
end end
it 'leaves members who expire in the future' do it 'leaves members that expire in the future' do
worker.perform worker.perform
expect(group_member_expiring_in_future.reload).to be_present expect(group_member_expiring_in_future.reload).to be_present
end end
it 'leaves members who do not expire at all' do it 'leaves members that do not expire at all' do
worker.perform worker.perform
expect(non_expiring_group_member.reload).to be_present expect(non_expiring_group_member.reload).to be_present
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment