Commit c4ba4f82 authored by Manoj M J's avatar Manoj M J Committed by Imre Farkas

Add controller actions for deletion/restore of groups

This change adds the controller actions
required to soft-delete and restore a
group from the UI
parent db1ee8e8
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { visitUrl } from '../../lib/utils/url_utility'; import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
...@@ -17,6 +17,7 @@ export default { ...@@ -17,6 +17,7 @@ export default {
tooltip, tooltip,
}, },
components: { components: {
GlBadge,
GlLoadingIcon, GlLoadingIcon,
identicon, identicon,
itemCaret, itemCaret,
...@@ -62,6 +63,9 @@ export default { ...@@ -62,6 +63,9 @@ export default {
isGroup() { isGroup() {
return this.group.type === 'group'; return this.group.type === 'group';
}, },
isGroupPendingRemoval() {
return this.group.type === 'group' && this.group.pendingRemoval;
},
visibilityIcon() { visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.group.visibility]; return VISIBILITY_TYPE_ICON[this.group.visibility];
}, },
...@@ -139,6 +143,9 @@ export default { ...@@ -139,6 +143,9 @@ export default {
<span v-html="group.description"> </span> <span v-html="group.description"> </span>
</div> </div>
</div> </div>
<div v-if="isGroupPendingRemoval">
<gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
</div>
<div <div
class="metadata align-items-md-center d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between" class="metadata align-items-md-center d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between"
> >
......
...@@ -93,7 +93,7 @@ export default class GroupsStore { ...@@ -93,7 +93,7 @@ export default class GroupsStore {
memberCount: rawGroupItem.number_users_with_delimiter, memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count, starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at, updatedAt: rawGroupItem.updated_at,
pendingRemoval: rawGroupItem.marked_for_deletion_at, pendingRemoval: rawGroupItem.marked_for_deletion,
}; };
} }
......
...@@ -6,8 +6,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -6,8 +6,7 @@ class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index def index
@groups = Group.with_statistics.with_route @groups = groups.sort_by_attribute(@sort = params[:sort])
@groups = @groups.sort_by_attribute(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page]) @groups = @groups.page(params[:page])
end end
...@@ -75,6 +74,10 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -75,6 +74,10 @@ class Admin::GroupsController < Admin::ApplicationController
private private
def groups
Group.with_statistics.with_route
end
def group def group
@group ||= Group.find_by_full_path(params[:id]) @group ||= Group.find_by_full_path(params[:id])
end end
......
...@@ -73,3 +73,5 @@ module LoadedInGroupList ...@@ -73,3 +73,5 @@ module LoadedInGroupList
@member_count ||= try(:preloaded_member_count) || users.count @member_count ||= try(:preloaded_member_count) || users.count
end end
end end
LoadedInGroupList::ClassMethods.prepend_if_ee('EE::LoadedInGroupList::ClassMethods')
...@@ -467,6 +467,10 @@ class Group < Namespace ...@@ -467,6 +467,10 @@ class Group < Namespace
import_export_upload&.export_file import_export_upload&.export_file
end end
def adjourned_deletion?
false
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
= storage_counter(group.storage_size) = storage_counter(group.storage_size)
= render_if_exists 'admin/namespace_plan_badge', namespace: group = render_if_exists 'admin/namespace_plan_badge', namespace: group
= render_if_exists 'admin/groups/marked_for_deletion_badge', group: group
%span %span
= icon('bookmark') = icon('bookmark')
......
...@@ -39,12 +39,5 @@ ...@@ -39,12 +39,5 @@
%li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.") %li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
= f.submit s_('GroupSettings|Transfer group'), class: 'btn btn-warning' = f.submit s_('GroupSettings|Transfer group'), class: 'btn btn-warning'
.sub-section = render 'groups/settings/remove', group: @group
%h4.danger-title= _('Remove group') = render_if_exists 'groups/settings/restore', group: @group
= form_tag(@group, method: :delete) do
%p
= _('Removing group will cause all child projects and resources to be removed.')
%br
%strong= _('Removed group can not be restored!')
= button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) }
.sub-section
%h4.danger-title= _('Remove group')
= form_tag(group, method: :delete) do
%p
= _('Removing group will cause all child projects and resources to be removed.')
%br
%strong= _('Removed group can not be restored!')
= button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) }
- if group.adjourned_deletion?
= render_if_exists 'groups/settings/adjourned_deletion', group: group
- else
= render 'groups/settings/permanent_deletion', group: group
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
= render 'groups/home_panel' = render 'groups/home_panel'
= render_if_exists 'groups/self_or_ancestor_marked_for_deletion_notice', group: @group
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
.top-area.group-nav-container.justify-content-between .top-area.group-nav-container.justify-content-between
.scrolling-tabs-container.inner-page-scroll-tabs .scrolling-tabs-container.inner-page-scroll-tabs
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
= render "archived_notice", project: @project = render "archived_notice", project: @project
= render_if_exists "projects/marked_for_deletion_notice", project: @project = render_if_exists "projects/marked_for_deletion_notice", project: @project
= render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project
- view_path = @project.default_view - view_path = @project.default_view
......
---
title: Add UI for 'soft-delete for groups' feature
merge_request: 19483
author:
type: added
...@@ -633,7 +633,7 @@ Only available to group owners and administrators. ...@@ -633,7 +633,7 @@ Only available to group owners and administrators.
This endpoint either: This endpoint either:
- Removes group, and queues a background job to delete all projects in the group as well. - Removes group, and queues a background job to delete all projects in the group as well.
- Since GitLab 12.8, on [Premium](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). - Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
``` ```
DELETE /groups/:id DELETE /groups/:id
......
...@@ -1767,7 +1767,7 @@ Example response: ...@@ -1767,7 +1767,7 @@ Example response:
This endpoint either: This endpoint either:
- Removes a project including all associated resources (issues, merge requests etc). - Removes a project including all associated resources (issues, merge requests etc).
- From GitLab 12.6 on Premium or higher tiers, marks a project for deletion. Actual - From [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/32935) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for deletion. Actual
deletion happens after number of days specified in deletion happens after number of days specified in
[instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
......
...@@ -314,6 +314,30 @@ If you want to retain ownership over the original namespace and ...@@ -314,6 +314,30 @@ If you want to retain ownership over the original namespace and
protect the URL redirects, then instead of changing a group's path or renaming a protect the URL redirects, then instead of changing a group's path or renaming a
username, you can create a new group and transfer projects to it. username, you can create a new group and transfer projects to it.
### Remove a group
To remove a group and its contents:
1. Navigate to your group's **{settings}** **Settings > General** page.
1. Expand the **Path, transfer, remove** section.
1. In the Remove group section, click the **Remove group** button.
1. Confirm the action when asked to.
This action either:
- Removes the group, and also queues a background job to delete all projects in that group.
- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
### Restore a group **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/33257) in GitLab 12.8.
To restore a group that is marked for deletion:
1. Navigate to your group's **{settings}** **Settings > General** page.
1. Expand the **Path, transfer, remove** section.
1. In the Restore group section, click the **Restore group** button.
#### Enforce 2FA to group members #### Enforce 2FA to group members
Add a security layer to your group by Add a security layer to your group by
......
...@@ -177,6 +177,31 @@ namespace if needed. ...@@ -177,6 +177,31 @@ namespace if needed.
[permissions]: ../../permissions.md#project-members-permissions [permissions]: ../../permissions.md#project-members-permissions
#### Remove a project
NOTE: **Note:**
Only project owners and admins have [permissions]((../../permissions.md#project-members-permissions) to remove a project.
To remove a project:
1. Navigate to your project, and select **{settings}** **Settings > General > Advanced**.
1. In the Remove project section, click the **Remove project** button.
1. Confirm the action when asked to.
This action either:
- Removes a project including all associated resources (issues, merge requests etc).
- Since [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/32935), on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
### Restore a project **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32935) in GitLab 12.6.
To restore a project that is marked for deletion:
1. Navigate to your project, and select **{settings}** **Settings > General > Advanced**.
1. In the Restore project section, click the **Restore project** button.
## Operations settings ## Operations settings
### Error Tracking ### Error Tracking
......
...@@ -24,6 +24,10 @@ module EE ...@@ -24,6 +24,10 @@ module EE
gitlab_subscription_attributes: [:hosted_plan_id] gitlab_subscription_attributes: [:hosted_plan_id]
] ]
end end
def groups
super.with_deletion_schedule
end
end end
end end
end end
...@@ -6,7 +6,10 @@ module EE ...@@ -6,7 +6,10 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
prepended do prepended do
alias_method :ee_authorize_admin_group!, :authorize_admin_group!
before_action :set_allowed_domain, only: [:edit] before_action :set_allowed_domain, only: [:edit]
before_action :ee_authorize_admin_group!, only: [:restore]
end end
override :render_show_html override :render_show_html
...@@ -22,6 +25,34 @@ module EE ...@@ -22,6 +25,34 @@ module EE
super + group_params_ee super + group_params_ee
end end
override :destroy
def destroy
return super unless group.adjourned_deletion?
result = ::Groups::MarkForDeletionService.new(group, current_user).execute
if result[:status] == :success
redirect_to group_path(group),
status: :found,
notice: "'#{group.name}' has been scheduled for removal on #{permanent_deletion_date(Time.now.utc)}."
else
redirect_to edit_group_path(group), status: :found, alert: result[:message]
end
end
def restore
return render_404 unless group.marked_for_deletion?
result = ::Groups::RestoreService.new(group, current_user).execute
if result[:status] == :success
redirect_to edit_group_path(group),
notice: "Group '#{group.name}' has been successfully restored."
else
redirect_to edit_group_path(group), alert: result[:message]
end
end
private private
def group_params_ee def group_params_ee
......
...@@ -60,6 +60,24 @@ module EE ...@@ -60,6 +60,24 @@ module EE
{ group_id: group } { group_id: group }
end end
override :remove_group_message
def remove_group_message(group)
return super unless group.feature_available?(:adjourned_deletion_for_projects_and_groups)
date = permanent_deletion_date(Time.now.utc)
_("The contents of this group, its subgroups and projects will be permanently removed after %{deletion_adjourned_period} days on %{date}. After this point, your data cannot be recovered.") %
{ date: date, deletion_adjourned_period: deletion_adjourned_period }
end
def permanent_deletion_date(date)
(date + deletion_adjourned_period.days).strftime('%F')
end
def deletion_adjourned_period
::Gitlab::CurrentSettings.deletion_adjourned_period
end
private private
def get_group_sidebar_links def get_group_sidebar_links
......
# frozen_string_literal: true
module EE
module LoadedInGroupList
extend ActiveSupport::Concern
class_methods do
def with_selects_for_list(archived: nil)
super.preload(:deletion_schedule)
end
end
end
end
...@@ -274,15 +274,21 @@ module EE ...@@ -274,15 +274,21 @@ module EE
end end
def marked_for_deletion? def marked_for_deletion?
return false unless feature_available?(:adjourned_deletion_for_projects_and_groups) marked_for_deletion_on.present? &&
feature_available?(:adjourned_deletion_for_projects_and_groups)
end
def self_or_ancestor_marked_for_deletion
return unless feature_available?(:adjourned_deletion_for_projects_and_groups)
marked_for_deletion_on.present? self_and_ancestors(hierarchy_order: :asc)
.joins(:deletion_schedule).first
end end
override :adjourned_deletion?
def adjourned_deletion? def adjourned_deletion?
return false unless feature_available?(:adjourned_deletion_for_projects_and_groups) feature_available?(:adjourned_deletion_for_projects_and_groups) &&
::Gitlab::CurrentSettings.deletion_adjourned_period > 0
::Gitlab::CurrentSettings.deletion_adjourned_period > 0
end end
private private
......
...@@ -702,9 +702,15 @@ module EE ...@@ -702,9 +702,15 @@ module EE
end end
def marked_for_deletion? def marked_for_deletion?
return false unless feature_available?(:marking_project_for_deletion) marked_for_deletion_at.present? &&
feature_available?(:marking_project_for_deletion)
end
def ancestor_marked_for_deletion
return unless feature_available?(:adjourned_deletion_for_projects_and_groups)
marked_for_deletion_at.present? ancestors(hierarchy_order: :asc)
.joins(:deletion_schedule).first
end end
def has_packages?(package_type) def has_packages?(package_type)
......
...@@ -5,9 +5,10 @@ module EE ...@@ -5,9 +5,10 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
# Project only attributes # For both group and project
expose :marked_for_deletion_at, expose :marked_for_deletion do |instance|
if: lambda { |_instance, _options| project? } instance.marked_for_deletion?
end
end end
end end
end end
- if group.marked_for_deletion?
%span.badge.badge-warning
= _('pending removal')
- return unless (group_pending_deletion = group.self_or_ancestor_marked_for_deletion)
.text-warning.center.prepend-top-20
%p
= sprite_icon('warning-solid', size: 12)
- if group.marked_for_deletion?
= _("This group, its subgroups and projects has been scheduled for removal on %{date}.") % { date: permanent_deletion_date(group_pending_deletion.marked_for_deletion_on) }
- else
= _("This group, its subgroups and projects will be removed on %{date} since its parent group '%{parent_group_name}'' has been scheduled for removal.") % { date: permanent_deletion_date(group_pending_deletion.marked_for_deletion_on), parent_group_name: group_pending_deletion.name }
- return if group.marked_for_deletion?
- date = permanent_deletion_date(Time.now.utc)
.sub-section
%h4.danger-title= _('Remove group')
= form_tag(group, method: :delete) do
%p
= _("Upon performing this action, the contents of this group, its subgroup and projects will be permanently removed after %{deletion_adjourned_period} days on <strong>%{date}</strong>. Until that time:").html_safe % { date: date, deletion_adjourned_period: deletion_adjourned_period }
%ul
%li= _("The group will be placed in 'pending removal' state")
%li= _("The group can be fully restored")
= button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) }
- return unless group.marked_for_deletion?
- date = permanent_deletion_date(group.marked_for_deletion_on)
.sub-section
%h4.warning-title= _('Restore group')
%p
%strong= _('This group has been scheduled for permanent removal on %{date}') %{ date: date }
%p
= _("Restoring the group will prevent the group, its subgroups and projects from being removed on this date.")
= form_tag(group_restore_path(group), method: :post) do
= button_to _('Restore group'), '#', class: "btn btn-warning"
- return if project.marked_for_deletion?
- return unless (ancestor_marked_for_deletion = project.ancestor_marked_for_deletion)
.text-warning.center.prepend-top-20
%p
= sprite_icon('warning-solid', size: 12)
= _("This project will be removed on %{date} since its parent group '%{parent_group_name}' has been scheduled for removal.") % { date: permanent_deletion_date(ancestor_marked_for_deletion.marked_for_deletion_on), parent_group_name: ancestor_marked_for_deletion.name }
...@@ -140,6 +140,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -140,6 +140,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :dependency_proxy, only: [:show, :update] resource :dependency_proxy, only: [:show, :update]
resources :packages, only: [:index] resources :packages, only: [:index]
post '/restore' => '/groups#restore', as: :restore
end end
end end
......
...@@ -67,4 +67,171 @@ describe GroupsController do ...@@ -67,4 +67,171 @@ describe GroupsController do
end end
end end
end end
describe 'POST #restore' do
let(:group) do
create(:group_with_deletion_schedule,
marked_for_deletion_on: 1.day.ago,
deleting_user: user)
end
subject { post :restore, params: { group_id: group.to_param } }
before do
group.add_owner(user)
end
context 'when authenticated user can admin the group' do
before do
sign_in(user)
end
context 'adjourned deletion feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'success' do
it 'restores the group' do
expect { subject }.to change { group.reload.marked_for_deletion? }.from(true).to(false)
end
it 'renders success notice upon restoring' do
subject
expect(response).to redirect_to(edit_group_path(group))
expect(flash[:notice]).to include "Group '#{group.name}' has been successfully restored."
end
end
context 'failure' do
before do
allow(::Groups::RestoreService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: 'error' })
end
it 'does not restore the group' do
expect { subject }.not_to change { group.reload.marked_for_deletion? }.from(true)
end
it 'redirects to group edit page' do
subject
expect(response).to redirect_to(edit_group_path(group))
expect(flash[:alert]).to include 'error'
end
end
end
context 'adjourned deletion feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
context 'when authenticated user cannot admin the group' do
before do
sign_in(create(:user))
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'DELETE #destroy' do
subject { delete :destroy, params: { id: group.to_param } }
before do
group.add_owner(user)
end
context 'when authenticated user can admin the group' do
before do
sign_in(user)
end
context 'adjourned deletion feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'success' do
it 'marks the group for adjourned deletion' do
expect { subject }.to change { group.reload.marked_for_deletion? }.from(false).to(true)
end
it 'does not immediately delete the group' do
Sidekiq::Testing.fake! do
expect { subject }.not_to change(GroupDestroyWorker.jobs, :size)
end
end
it 'redirects to group path with notice about adjourned deletion' do
subject
expect(response).to redirect_to(group_path(group))
expect(flash[:notice]).to include "'#{group.name}' has been scheduled for removal on"
end
end
context 'failure' do
before do
allow(::Groups::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: 'error' })
end
it 'does not mark the group for deletion' do
expect { subject }.not_to change { group.reload.marked_for_deletion? }.from(false)
end
it 'redirects to group edit page' do
subject
expect(response).to redirect_to(edit_group_path(group))
expect(flash[:alert]).to include 'error'
end
end
end
context 'adjourned deletion feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
it 'immediately schedules a group destroy' do
Sidekiq::Testing.fake! do
expect { subject }.to change(GroupDestroyWorker.jobs, :size).by(1)
end
end
it 'redirects to root page with alert about immediate deletion' do
subject
expect(response).to redirect_to(root_path)
expect(flash[:alert]).to include "Group '#{group.name}' was scheduled for deletion."
end
end
end
context 'when authenticated user cannot admin the group' do
before do
sign_in(create(:user))
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
end end
...@@ -50,4 +50,45 @@ describe GroupsHelper do ...@@ -50,4 +50,45 @@ describe GroupsHelper do
expect(helper.group_sidebar_links).not_to include(:contribution_analytics, :epics) expect(helper.group_sidebar_links).not_to include(:contribution_analytics, :epics)
end end
end end
describe '#permanent_deletion_date' do
let(:date) { 2.days.from_now }
subject { helper.permanent_deletion_date(date) }
before do
stub_application_setting(deletion_adjourned_period: 5)
end
it 'returns the sum of the date passed as argument and the deletion_adjourned_period set in application setting' do
expected_date = date + 5.days
expect(subject).to eq(expected_date.strftime('%F'))
end
end
describe '#remove_group_message' do
subject { helper.remove_group_message(group) }
context 'adjourned deletion feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
it 'returns the message related to adjourned deletion' do
expect(subject).to include("The contents of this group, its subgroups and projects will be permanently removed after")
end
end
context 'adjourned deletion feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
it 'returns the message related to permanent deletion' do
expect(subject).to include("You are going to remove #{group.name}")
expect(subject).to include("Removed groups CANNOT be restored!")
end
end
end
end end
...@@ -636,6 +636,63 @@ describe Group do ...@@ -636,6 +636,63 @@ describe Group do
end end
end end
describe '#self_or_ancestor_marked_for_deletion' do
context 'adjourned deletion feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
create(:group_deletion_schedule, group: group, marked_for_deletion_on: 1.day.ago)
end
it 'returns nil' do
expect(group.self_or_ancestor_marked_for_deletion).to be_nil
end
end
context 'adjourned deletion feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'the group has been marked for deletion' do
before do
create(:group_deletion_schedule, group: group, marked_for_deletion_on: 1.day.ago)
end
it 'returns the group' do
expect(group.self_or_ancestor_marked_for_deletion).to eq(group)
end
end
context 'the parent group has been marked for deletion' do
let(:parent_group) { create(:group_with_deletion_schedule, marked_for_deletion_on: 1.day.ago) }
let(:group) { create(:group, parent: parent_group) }
it 'returns the parent group' do
expect(group.self_or_ancestor_marked_for_deletion).to eq(parent_group)
end
end
context 'no group has been marked for deletion' do
let(:parent_group) { create(:group) }
let(:group) { create(:group, parent: parent_group) }
it 'returns nil' do
expect(group.self_or_ancestor_marked_for_deletion).to be_nil
end
end
context 'ordering' do
let(:group_a) { create(:group_with_deletion_schedule, marked_for_deletion_on: 1.day.ago) }
let(:subgroup_a) { create(:group_with_deletion_schedule, marked_for_deletion_on: 1.day.ago, parent: group_a) }
let(:group) { create(:group, parent: subgroup_a) }
it 'returns the first group that is marked for deletion, up its ancestry chain' do
expect(group.self_or_ancestor_marked_for_deletion).to eq(subgroup_a)
end
end
end
end
describe '#marked_for_deletion?' do describe '#marked_for_deletion?' do
subject { group.marked_for_deletion? } subject { group.marked_for_deletion? }
......
...@@ -2257,6 +2257,63 @@ describe Project do ...@@ -2257,6 +2257,63 @@ describe Project do
end end
end end
describe '#ancestor_marked_for_deletion' do
context 'adjourned deletion feature is not available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
context 'the parent namespace has been marked for deletion' do
let(:parent_group) do
create(:group_with_deletion_schedule, marked_for_deletion_on: 1.day.ago)
end
let(:project) { create(:project, namespace: parent_group) }
it 'returns nil' do
expect(project.ancestor_marked_for_deletion).to be_nil
end
end
end
context 'adjourned deletion feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'the parent namespace has been marked for deletion' do
let(:parent_group) do
create(:group_with_deletion_schedule, marked_for_deletion_on: 1.day.ago)
end
let(:project) { create(:project, namespace: parent_group) }
it 'returns the parent namespace' do
expect(project.ancestor_marked_for_deletion).to eq(parent_group)
end
end
context "project or its parent group has not been marked for deletion" do
let(:parent_group) { create(:group) }
let(:project) { create(:project, namespace: parent_group) }
it 'returns nil' do
expect(project.ancestor_marked_for_deletion).to be_nil
end
end
context 'ordering' do
let(:group_a) { create(:group_with_deletion_schedule, marked_for_deletion_on: 1.day.ago) }
let(:subgroup_a) { create(:group_with_deletion_schedule, marked_for_deletion_on: 1.day.ago, parent: group_a) }
let(:project) { create(:project, namespace: subgroup_a) }
it 'returns the first group that is marked for deletion, up its ancestry chain' do
expect(project.ancestor_marked_for_deletion).to eq(subgroup_a)
end
end
end
end
describe '#adjourned_deletion?' do describe '#adjourned_deletion?' do
context 'when marking for deletion feature is available' do context 'when marking for deletion feature is available' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -15956,9 +15956,15 @@ msgstr "" ...@@ -15956,9 +15956,15 @@ msgstr ""
msgid "Restart Terminal" msgid "Restart Terminal"
msgstr "" msgstr ""
msgid "Restore group"
msgstr ""
msgid "Restore project" msgid "Restore project"
msgstr "" msgstr ""
msgid "Restoring the group will prevent the group, its subgroups and projects from being removed on this date."
msgstr ""
msgid "Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it." msgid "Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it."
msgstr "" msgstr ""
...@@ -18558,6 +18564,9 @@ msgstr "" ...@@ -18558,6 +18564,9 @@ msgstr ""
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr "" msgstr ""
msgid "The contents of this group, its subgroups and projects will be permanently removed after %{deletion_adjourned_period} days on %{date}. After this point, your data cannot be recovered."
msgstr ""
msgid "The current issue" msgid "The current issue"
msgstr "" msgstr ""
...@@ -18620,12 +18629,18 @@ msgstr "" ...@@ -18620,12 +18629,18 @@ msgstr ""
msgid "The group and its projects can only be viewed by members." msgid "The group and its projects can only be viewed by members."
msgstr "" msgstr ""
msgid "The group can be fully restored"
msgstr ""
msgid "The group has already been shared with this group" msgid "The group has already been shared with this group"
msgstr "" msgstr ""
msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}." msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}."
msgstr "" msgstr ""
msgid "The group will be placed in 'pending removal' state"
msgstr ""
msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr "" msgstr ""
...@@ -19109,9 +19124,18 @@ msgstr "" ...@@ -19109,9 +19124,18 @@ msgstr ""
msgid "This group does not provide any group Runners yet." msgid "This group does not provide any group Runners yet."
msgstr "" msgstr ""
msgid "This group has been scheduled for permanent removal on %{date}"
msgstr ""
msgid "This group, including all subgroups, projects and git repositories, will only be reachable from the specified IP address range. Multiple addresses are supported with comma delimiters.<br>Example: <code>192.168.0.0/24,192.168.1.0/24</code>. %{read_more_link}." msgid "This group, including all subgroups, projects and git repositories, will only be reachable from the specified IP address range. Multiple addresses are supported with comma delimiters.<br>Example: <code>192.168.0.0/24,192.168.1.0/24</code>. %{read_more_link}."
msgstr "" msgstr ""
msgid "This group, its subgroups and projects has been scheduled for removal on %{date}."
msgstr ""
msgid "This group, its subgroups and projects will be removed on %{date} since its parent group '%{parent_group_name}'' has been scheduled for removal."
msgstr ""
msgid "This is a \"Ghost User\", created to hold all issues authored by users that have since been deleted. This user cannot be removed." msgid "This is a \"Ghost User\", created to hold all issues authored by users that have since been deleted. This user cannot be removed."
msgstr "" msgstr ""
...@@ -19283,6 +19307,9 @@ msgstr "" ...@@ -19283,6 +19307,9 @@ msgstr ""
msgid "This project will be removed on %{date}" msgid "This project will be removed on %{date}"
msgstr "" msgstr ""
msgid "This project will be removed on %{date} since its parent group '%{parent_group_name}' has been scheduled for removal."
msgstr ""
msgid "This repository" msgid "This repository"
msgstr "" msgstr ""
...@@ -20295,6 +20322,9 @@ msgstr "" ...@@ -20295,6 +20322,9 @@ msgstr ""
msgid "Uploads" msgid "Uploads"
msgstr "" msgstr ""
msgid "Upon performing this action, the contents of this group, its subgroup and projects will be permanently removed after %{deletion_adjourned_period} days on <strong>%{date}</strong>. Until that time:"
msgstr ""
msgid "Upstream" msgid "Upstream"
msgstr "" msgstr ""
......
...@@ -155,6 +155,35 @@ describe('GroupItemComponent', () => { ...@@ -155,6 +155,35 @@ describe('GroupItemComponent', () => {
}); });
describe('template', () => { describe('template', () => {
let group = null;
describe('for a group pending deletion', () => {
beforeEach(() => {
group = { ...mockParentGroupItem, pendingRemoval: true };
vm = createComponent(group);
});
it('renders the group pending removal badge', () => {
const badgeEl = vm.$el.querySelector('.badge-warning');
expect(badgeEl).toBeDefined();
expect(badgeEl).toContainText('pending removal');
});
});
describe('for a group not scheduled for deletion', () => {
beforeEach(() => {
group = { ...mockParentGroupItem, pendingRemoval: false };
vm = createComponent(group);
});
it('does not render the group pending removal badge', () => {
const groupTextContainer = vm.$el.querySelector('.group-text-container');
expect(groupTextContainer).not.toContainText('pending removal');
});
});
it('should render component template correctly', () => { it('should render component template correctly', () => {
const visibilityIconEl = vm.$el.querySelector('.item-visibility'); const visibilityIconEl = vm.$el.querySelector('.item-visibility');
......
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