Commit b420e180 authored by huzaifaiftikhar1's avatar huzaifaiftikhar1

Default to delayed deletion for projects not in personal namespace

Use adjourned deletion if the projects belong to a group
and the instance has 'adjourned_deletion_for_projects_and_groups'
feature available

Changelog: changed
parent 37e63135
......@@ -24,7 +24,12 @@ class Projects::ApplicationController < ApplicationController
return unless params[:project_id] || params[:id]
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
auth_proc = if params[:controller] == "projects" && params[:action] == "restore"
->(project) { !project.pending_delete? }
else
->(project) { !project.pending_delete_or_hidden? }
end
@project = find_routable!(Project, path, request.fullpath, extra_authorization_proc: auth_proc)
end
......
......@@ -526,6 +526,7 @@ class Project < ApplicationRecord
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
scope :not_hidden, -> { where(hidden: false) }
scope :not_aimed_for_deletion, -> { where(marked_for_deletion_at: nil).without_deleted }
scope :with_storage_feature, ->(feature) do
......@@ -2805,6 +2806,10 @@ class Project < ApplicationRecord
end
end
def pending_delete_or_hidden?
pending_delete? || hidden?
end
private
# overridden in EE
......
# frozen_string_literal: true
class AddHiddenToProjects < Gitlab::Database::Migration[1.0]
DOWNTIME = false
INDEX_NAME = 'index_projects_on_hidden'
disable_ddl_transaction!
def up
add_column :projects, :hidden, :boolean, default: false # rubocop: disable Migration/AddColumnsToWideTables
add_concurrent_index :projects, :hidden, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :projects, INDEX_NAME
remove_column :projects, :hidden
end
end
# frozen_string_literal: true
class UpdateApiIndexesForProjects < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
ARCHIVED_INDEX_NAME = 'idx_projects_api_created_at_id_for_archived'
OLD_ARCHIVED_INDEX_NAME = 'index_projects_api_created_at_id_for_archived'
PUBLIC_AND_ARCHIVED_INDEX_NAME = 'idx_projects_api_created_at_id_for_archived_vis20'
OLD_PUBLIC_AND_ARCHIVED_INDEX_NAME = 'index_projects_api_created_at_id_for_archived_vis20'
INTERNAL_PROJECTS_INDEX_NAME = 'idx_projects_api_created_at_id_for_vis10'
OLD_INTERNAL_PROJECTS_INDEX_NAME = 'index_projects_api_created_at_id_for_vis10'
def up
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND pending_delete = false AND hidden = false",
name: ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND visibility_level = 20 AND pending_delete = false AND hidden = false",
name: PUBLIC_AND_ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "visibility_level = 10 AND pending_delete = false AND hidden = false",
name: INTERNAL_PROJECTS_INDEX_NAME
remove_concurrent_index_by_name :projects, OLD_ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, OLD_PUBLIC_AND_ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, OLD_INTERNAL_PROJECTS_INDEX_NAME
end
def down
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND pending_delete = false",
name: OLD_ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND visibility_level = 20 AND pending_delete = false",
name: OLD_PUBLIC_AND_ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "visibility_level = 10 AND pending_delete = false",
name: OLD_INTERNAL_PROJECTS_INDEX_NAME
remove_concurrent_index_by_name :projects, ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, PUBLIC_AND_ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, INTERNAL_PROJECTS_INDEX_NAME
end
end
02f7a38c7bc19b1a266ac1f1d6631f1922fc135518baeb19ee90bebd7c31c6b9
\ No newline at end of file
f63be8bd42cc1856c92f9073fdb39c58c45806b483d38b91db007a8661c49a97
\ No newline at end of file
......@@ -18894,7 +18894,8 @@ CREATE TABLE projects (
marked_for_deletion_by_user_id integer,
autoclose_referenced_issues boolean,
suggestion_commit_message character varying(255),
project_namespace_id bigint
project_namespace_id bigint,
hidden boolean DEFAULT false
);
CREATE SEQUENCE projects_id_seq
......@@ -25398,6 +25399,12 @@ CREATE UNIQUE INDEX idx_project_id_payload_key_self_managed_prometheus_alert_eve
CREATE INDEX idx_project_repository_check_partial ON projects USING btree (repository_storage, created_at) WHERE (last_repository_check_at IS NULL);
CREATE INDEX idx_projects_api_created_at_id_for_archived ON projects USING btree (created_at, id) WHERE ((archived = true) AND (pending_delete = false) AND (hidden = false));
CREATE INDEX idx_projects_api_created_at_id_for_archived_vis20 ON projects USING btree (created_at, id) WHERE ((archived = true) AND (visibility_level = 20) AND (pending_delete = false) AND (hidden = false));
CREATE INDEX idx_projects_api_created_at_id_for_vis10 ON projects USING btree (created_at, id) WHERE ((visibility_level = 10) AND (pending_delete = false) AND (hidden = false));
CREATE INDEX idx_projects_id_created_at_disable_overriding_approvers_false ON projects USING btree (id, created_at) WHERE ((disable_overriding_approvers_per_merge_request = false) OR (disable_overriding_approvers_per_merge_request IS NULL));
CREATE INDEX idx_projects_id_created_at_disable_overriding_approvers_true ON projects USING btree (id, created_at) WHERE (disable_overriding_approvers_per_merge_request = true);
......@@ -27510,12 +27517,6 @@ CREATE INDEX index_projects_aimed_for_deletion ON projects USING btree (marked_f
CREATE INDEX index_projects_api_created_at_id_desc ON projects USING btree (created_at, id DESC);
CREATE INDEX index_projects_api_created_at_id_for_archived ON projects USING btree (created_at, id) WHERE ((archived = true) AND (pending_delete = false));
CREATE INDEX index_projects_api_created_at_id_for_archived_vis20 ON projects USING btree (created_at, id) WHERE ((archived = true) AND (visibility_level = 20) AND (pending_delete = false));
CREATE INDEX index_projects_api_created_at_id_for_vis10 ON projects USING btree (created_at, id) WHERE ((visibility_level = 10) AND (pending_delete = false));
CREATE INDEX index_projects_api_last_activity_at_id_desc ON projects USING btree (last_activity_at, id DESC);
CREATE INDEX index_projects_api_name_id_desc ON projects USING btree (name, id DESC);
......@@ -27546,6 +27547,8 @@ CREATE INDEX index_projects_on_creator_id_import_type_and_created_at_partial ON
CREATE INDEX index_projects_on_description_trigram ON projects USING gin (description gin_trgm_ops);
CREATE INDEX index_projects_on_hidden ON projects USING btree (hidden);
CREATE INDEX index_projects_on_id_and_archived_and_pending_delete ON projects USING btree (id) WHERE ((archived = false) AND (pending_delete = false));
CREATE UNIQUE INDEX index_projects_on_id_partial_for_visibility ON projects USING btree (id) WHERE (visibility_level = ANY (ARRAY[10, 20]));
......@@ -85,7 +85,10 @@ are included when cloning.
Top-level groups created after August 12, 2021 have delayed project deletion enabled by default.
Projects are permanently deleted after a seven-day delay.
You can disable this by changing the [group setting](../group/index.md#enable-delayed-project-deletion).
If you are on:
- Premium tier and above, you can disable this by changing the [group setting](../group/index.md#enable-delayed-project-deletion).
- Free tier, groups have delayed project deletion enabled by default and you cannot disable this setting.
## Alternative SSH port
......
......@@ -38,7 +38,7 @@ module EE
end
def projects_pending_deletion_params
finder_params = { aimed_for_deletion: true }
finder_params = { aimed_for_deletion: true, include_hidden: true }
unless current_user.can_admin_all_resources?
finder_params[:min_access_level] = ::Gitlab::Access::OWNER
......
......@@ -38,17 +38,24 @@ module EE
override :destroy
def destroy
return super unless project.adjourned_deletion?
return super if project.marked_for_deletion? && params[:permanently_delete].present?
return super unless can?(current_user, :delayed_project_deletion)
return super unless project.adjourned_deletion_configured?
return super if project.marked_for_deletion_at? && params[:permanently_delete].present?
return access_denied! unless can?(current_user, :remove_project, project)
result = ::Projects::MarkForDeletionService.new(project, current_user, {}).execute
if result[:status] == :success
date = permanent_deletion_date(project.marked_for_deletion_at)
flash[:notice] = _("Project '%{project_name}' will be deleted on %{date}") % { date: date, project_name: project.full_name }
redirect_to(project_path(project), status: :found)
if project.licensed_feature_available?(:adjourned_deletion_for_projects_and_groups)
redirect_to(project_path(project), status: :found)
else
redirect_to dashboard_projects_path, status: :found
end
else
flash.now[:alert] = result[:message]
......
......@@ -8,6 +8,9 @@ module EE
# Added arguments:
# params:
# plans: string[]
# feature_available: string[]
# aimed_for_deletion: Symbol
# include_hidden: boolean
module ProjectsFinder
extend ::Gitlab::Utils::Override
......@@ -18,6 +21,7 @@ module EE
collection = super(collection)
collection = by_plans(collection)
collection = by_feature_available(collection)
collection = by_hidden(collection)
by_aimed_for_deletion(collection)
end
......@@ -44,5 +48,9 @@ module EE
items
end
end
def by_hidden(items)
params[:include_hidden].present? ? items : items.not_hidden
end
end
end
......@@ -109,7 +109,13 @@ module EE
def marked_for_removal_message(project)
date = permanent_deletion_date(Time.now.utc)
message = _("This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains.")
message = if project.feature_available?(:adjourned_deletion_for_projects_and_groups)
_("This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains.")
else
_("This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}")
end
html_escape(message) % remove_message_data(project).merge(date: date)
end
......
......@@ -723,7 +723,11 @@ module EE
def adjourned_deletion?
feature_available?(:adjourned_deletion_for_projects_and_groups) &&
::Gitlab::CurrentSettings.deletion_adjourned_period > 0 &&
adjourned_deletion_configured?
end
def adjourned_deletion_configured?
::Gitlab::CurrentSettings.deletion_adjourned_period > 0 &&
group_deletion_mode_configured?
end
......
......@@ -60,6 +60,7 @@ module EE
rule { adjourned_project_deletion_available }.policy do
enable :list_removable_projects
enable :delayed_project_deletion
end
rule { export_user_permissions_available & admin }.enable :export_user_permissions
......
......@@ -4,16 +4,12 @@ module Projects
class MarkForDeletionService < BaseService
def execute
return success if project.marked_for_deletion_at?
return error('Cannot mark project for deletion: feature not supported') unless project.feature_available?(:adjourned_deletion_for_projects_and_groups)
return error('Cannot mark project for deletion: feature not supported') unless can?(current_user, :delayed_project_deletion)
result = ::Projects::UpdateService.new(
project,
current_user,
{ archived: true,
name: "#{project.name}-deleted-#{project.id}",
path: "#{project.path}-deleted-#{project.id}",
marked_for_deletion_at: Time.current.utc,
deleting_user: current_user }
project_update_service_params
).execute
log_event if result[:status] == :success
log_error(result[:message]) if result[:status] == :error
......@@ -21,6 +17,8 @@ module Projects
result
end
private
def log_event
log_audit_event
log_info("User #{current_user.id} marked project #{project.full_path} for deletion")
......@@ -34,5 +32,17 @@ module Projects
custom_message: "Project marked for deletion"
).for_project.security_event
end
def project_update_service_params
params = {
archived: true,
name: "#{project.name}-deleted-#{project.id}",
path: "#{project.path}-deleted-#{project.id}",
marked_for_deletion_at: Time.current.utc,
deleting_user: current_user
}
params[:hidden] = true unless project.feature_available?(:adjourned_deletion_for_projects_and_groups)
params
end
end
end
......@@ -13,6 +13,7 @@ module Projects
project,
current_user,
{ archived: false,
hidden: false,
marked_for_deletion_at: nil,
deleting_user: nil,
name: updated_value(project.name),
......
- if project.marked_for_deletion?
- if project.marked_for_deletion_at? && can?(current_user, :delayed_project_deletion)
= gl_badge_tag _('pending deletion'), variant: :warning
- elsif project.archived
= gl_badge_tag _('archived'), variant: :warning
- return unless can?(current_user, :remove_project, project)
- adjourned_deletion = project.adjourned_deletion?
- can_delayed_project_deletion = can?(current_user, :delayed_project_deletion)
- adjourned_deletion = project.adjourned_deletion_configured?
- adjourned_date = adjourned_deletion ? permanent_deletion_date(Time.now.utc).to_s : nil
- restore_help_path = help_page_path('user/project/settings/index', anchor: 'restore-a-project')
- merge_requests_count = Projects::AllMergeRequestsCountService.new(project).count
- issues_count = Projects::AllIssuesCountService.new(project).count
- forks_count = Projects::ForksCountService.new(project).count
- unless project.marked_for_deletion?
- unless project.marked_for_deletion_at?
.sub-section
%h4.danger-title= _('Delete this project')
- if adjourned_deletion
- if adjourned_deletion && can_delayed_project_deletion
= render 'projects/settings/marked_for_removal'
#js-project-adjourned-delete-button{ data: { restore_help_path: restore_help_path, adjourned_removal_date: adjourned_date, form_path: project_path(project), confirm_phrase: delete_confirm_phrase(project), is_fork: project.forked?.to_s, issues_count: number_with_delimiter(issues_count), merge_requests_count: number_with_delimiter(merge_requests_count), forks_count: number_with_delimiter(forks_count), stars_count: number_with_delimiter(project.star_count) } }
- else
%p= permanent_delete_message(project)
- if project.feature_available?(:adjourned_deletion_for_projects_and_groups)
#js-project-adjourned-delete-button{ data: { restore_help_path: restore_help_path, adjourned_removal_date: adjourned_date, form_path: project_path(project), confirm_phrase: delete_confirm_phrase(project), is_fork: project.forked?.to_s, issues_count: number_with_delimiter(issues_count), merge_requests_count: number_with_delimiter(merge_requests_count), forks_count: number_with_delimiter(forks_count), stars_count: number_with_delimiter(project.star_count) } }
- else
#js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: delete_confirm_phrase(project), is_fork: project.forked?.to_s, issues_count: number_with_delimiter(issues_count), merge_requests_count: number_with_delimiter(merge_requests_count), forks_count: number_with_delimiter(forks_count), stars_count: number_with_delimiter(project.star_count) } }
- else
= render 'projects/settings/restore', project: project
......
- return unless @project.feature_available?(:adjourned_deletion_for_projects_and_groups)
- return unless can?(current_user, :delayed_project_deletion)
%p= marked_for_removal_message(@project)
- if project.marked_for_deletion?
- if project.marked_for_deletion_at && can?(current_user, :delayed_project_deletion)
= gl_badge_tag _('pending deletion'), { variant: :warning }, { class: 'gl-display-flex gl-ml-3' }
- elsif project.archived
= gl_badge_tag _('archived'), { variant: :warning }, { class: 'gl-display-flex gl-ml-3' }
......@@ -125,7 +125,8 @@ module EE
override :delete_project
def delete_project(user_project)
return super unless user_project.adjourned_deletion?
return super unless can?(current_user, :delayed_project_deletion)
return super unless user_project.adjourned_deletion_configured?
result = destroy_conditionally!(user_project) do
::Projects::MarkForDeletionService.new(user_project, current_user, {}).execute
......
......@@ -14,7 +14,7 @@ RSpec.describe Dashboard::ProjectsController do
before do
sign_in(user)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
shared_examples 'returns not found' do
......@@ -32,6 +32,7 @@ RSpec.describe Dashboard::ProjectsController do
context 'for admin users', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
let_it_be(:hidden_project) { create(:project, :hidden, :archived, creator: user, marked_for_deletion_at: 2.days.ago) }
let_it_be(:projects) { create_list(:project, 2, :archived, creator: user, marked_for_deletion_at: 3.days.ago) }
it 'returns success' do
......@@ -43,7 +44,13 @@ RSpec.describe Dashboard::ProjectsController do
it 'paginates the records' do
subject
expect(assigns(:projects).count).to eq(1)
expect(assigns(:projects).count).to eq(2)
end
it 'returns projects marked for deletion' do
subject
expect(assigns(:projects)).to contain_exactly(hidden_project, projects.first)
end
end
......@@ -72,7 +79,7 @@ RSpec.describe Dashboard::ProjectsController do
it 'paginates the records' do
subject
expect(assigns(:projects).count).to eq(1)
expect(assigns(:projects).count).to eq(2)
end
context 'for should_check_namespace_plan' do
......
......@@ -679,11 +679,24 @@ RSpec.describe ProjectsController do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.reload.marked_for_deletion?).to be_truthy
expect(project.reload.hidden?).to be_falsey
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(project_path(project))
end
end
shared_examples 'marks free project for deletion' do
it do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.reload.marked_for_deletion?).to be_falsey
expect(project.reload.marked_for_deletion_at).to be_truthy
expect(project.reload.hidden?).to be_truthy
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(dashboard_projects_path)
end
end
context 'feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
......@@ -757,6 +770,18 @@ RSpec.describe ProjectsController do
it_behaves_like 'deletes project right away'
end
context 'when feature is not available for the project' do
before do
allow(group.namespace_settings).to receive(:delayed_project_removal?).and_return(true)
allow(project).to receive(:licensed_feature_available?).and_call_original
allow(project).to receive(:licensed_feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(false)
allow(project).to receive(:feature_available?).and_call_original
allow(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(false)
end
it_behaves_like 'marks free project for deletion'
end
context 'for projects in user namespace' do
let(:project) { create(:project, namespace: user.namespace) }
......
......@@ -3,21 +3,20 @@
require 'spec_helper'
RSpec.describe ProjectsFinder do
describe '#execute' do
describe '#execute', :saas do
let_it_be(:user) { create(:user) }
let_it_be(:ultimate_project) { create_project(:ultimate_plan) }
let_it_be(:ultimate_project2) { create_project(:ultimate_plan) }
let_it_be(:premium_project) { create_project(:premium_plan) }
let_it_be(:no_plan_project) { create_project(nil) }
let(:project_ids_relation) { nil }
let(:finder) { described_class.new(current_user: user, params: params, project_ids_relation: project_ids_relation) }
subject { finder.execute }
describe 'filter by plans', :saas do
describe 'filter by plans' do
let(:params) { { plans: plans } }
let(:project_ids_relation) { nil }
let_it_be(:ultimate_project) { create_project(:ultimate_plan) }
let_it_be(:ultimate_project2) { create_project(:ultimate_plan) }
let_it_be(:premium_project) { create_project(:premium_plan) }
let_it_be(:no_plan_project) { create_project(nil) }
context 'with ultimate plan' do
let(:plans) { ['ultimate'] }
......@@ -48,28 +47,44 @@ RSpec.describe ProjectsFinder do
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project) }
end
end
context 'filter by aimed for deletion' do
let_it_be(:params) { { aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
context 'filter by aimed for deletion' do
let_it_be(:params) { { aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
it { is_expected.to contain_exactly(aimed_for_deletion_project) }
end
context 'filter by not aimed for deletion' do
let_it_be(:params) { { not_aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
it { is_expected.to contain_exactly(aimed_for_deletion_project) }
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project) }
end
context 'filter by hidden' do
let_it_be(:hidden_project) { create(:project, :public, :hidden) }
context 'when include hidden is true' do
let_it_be(:params) { { include_hidden: true } }
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project, hidden_project) }
end
context 'filter by not aimed for deletion' do
let_it_be(:params) { { not_aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
context 'when include hidden is false' do
let_it_be(:params) { { include_hidden: false } }
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project) }
end
end
private
private
def create_project(plan)
create(:project, :public, namespace: create(:namespace_with_plan, plan: plan))
end
def create_project(plan)
create(:project, :public, namespace: create(:namespace_with_plan, plan: plan))
end
end
end
......@@ -331,17 +331,26 @@ RSpec.describe ProjectsHelper do
subject { helper.marked_for_removal_message(project) }
before do
allow(project).to receive(:adjourned_deletion?).and_return(enabled)
allow(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(feature_available)
end
context 'when project has delayed deletion enabled' do
let(:enabled) { true }
context 'when project has delayed deletion feature' do
let(:feature_available) { true }
specify do
deletion_date = helper.permanent_deletion_date(Time.now.utc)
expect(subject).to eq "This action deletes <code>#{project.path_with_namespace}</code> on #{deletion_date} and everything this project contains."
end
end
context 'when project does not have delayed deletion feature' do
let(:feature_available) { false }
specify do
deletion_date = helper.permanent_deletion_date(Time.now.utc)
expect(subject).to eq "This action deletes <code>#{project.path_with_namespace}</code> on #{deletion_date} and everything this project contains. <strong>There is no going back.</strong>"
end
end
end
describe '#scheduled_for_deletion?' do
......
......@@ -3002,6 +3002,43 @@ RSpec.describe Project do
end
end
describe '#adjourned_deletion_configured?' do
subject { project.adjourned_deletion_configured? }
where(:feature_enabled_on_group?, :adjourned_period, :result) do
true | 0 | false
true | 1 | true
false | 0 | false
false | 1 | false
end
with_them do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
before do
stub_application_setting(deletion_adjourned_period: adjourned_period)
allow(group.namespace_settings).to receive(:delayed_project_removal?).and_return(feature_enabled_on_group?)
end
it { is_expected.to be result }
end
context 'when project belongs to user namespace' do
let_it_be(:user) { create(:user) }
let_it_be(:user_project) { create(:project, namespace: user.namespace) }
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
stub_application_setting(deletion_adjourned_period: 7)
end
it 'deletes immediately' do
expect(user_project.adjourned_deletion?).to be_falsey
end
end
end
describe 'calculate template repositories' do
let(:group1) { create(:group) }
let(:group2) { create(:group) }
......
......@@ -67,6 +67,7 @@ RSpec.describe Projects::MarkForDeletionService do
result = described_class.new(project, user).execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Cannot mark project for deletion: feature not supported')
expect(Project.all).to include(project)
expect(project.archived).to eq(false)
......@@ -75,4 +76,40 @@ RSpec.describe Projects::MarkForDeletionService do
end
end
end
describe "#project_update_service_params" do
subject { described_class.new(project, user) }
context 'when delayed deletion feature is not available' do
before do
expect(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(false)
end
it "creates the params for project update service" do
project_update_service_params = subject.send(:project_update_service_params)
expect(project_update_service_params[:marked_for_deletion_at]).not_to be_nil
expect(project_update_service_params[:archived]).to eq(true)
expect(project_update_service_params[:hidden]).to eq(true)
expect(project_update_service_params[:deleting_user]).to eq(user)
expect(project_update_service_params[:name]).to eq("test project xyz-deleted-#{project.id}")
end
end
context 'when delayed deletion feature is available' do
before do
expect(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(true)
end
it "creates the params for project update service" do
project_update_service_params = subject.send(:project_update_service_params)
expect(project_update_service_params[:marked_for_deletion_at]).not_to be_nil
expect(project_update_service_params[:archived]).to eq(true)
expect(project_update_service_params.has_key?(:hidden)).to eq(false)
expect(project_update_service_params[:deleting_user]).to eq(user)
expect(project_update_service_params[:name]).to eq("test project xyz-deleted-#{project.id}")
end
end
end
end
......@@ -13,18 +13,20 @@ RSpec.describe Projects::RestoreService do
marked_for_deletion_at: 1.day.ago,
deleting_user: user,
archived: true,
hidden: true,
pending_delete: pending_delete)
end
context 'restoring project' do
subject { described_class.new(project, user).execute }
it 'marks project as unarchived and not marked for deletion' do
it 'marks project as not hidden, unarchived and not marked for deletion' do
subject
expect(Project.unscoped.all).to include(project)
expect(project.archived).to eq(false)
expect(project.hidden).to eq(false)
expect(project.marked_for_deletion_at).to be_nil
expect(project.deleting_user).to eq(nil)
end
......
......@@ -119,7 +119,7 @@ module API
def find_project(id)
return unless id
projects = Project.without_deleted
projects = Project.without_deleted.not_hidden
if id.is_a?(Integer) || id =~ /^\d+$/
projects.find_by(id: id)
......
......@@ -37038,6 +37038,9 @@ msgstr ""
msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains."
msgstr ""
msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}"
msgstr ""
msgid "This action will %{strongOpen}permanently remove%{strongClose} %{codeOpen}%{group}%{codeClose} %{strongOpen}immediately%{strongClose}."
msgstr ""
......
......@@ -154,6 +154,10 @@ FactoryBot.define do
archived { true }
end
trait :hidden do
hidden { true }
end
trait :last_repository_check_failed do
last_repository_check_failed { true }
end
......
......@@ -109,6 +109,26 @@ RSpec.describe API::Helpers do
end
end
end
context 'when project is pending delete' do
let(:project_pending_delete) { create(:project, pending_delete: true) }
it 'does not return the project pending delete' do
expect(Project).not_to receive(:find_by_full_path)
expect(subject.find_project(project_pending_delete.id)).to be_nil
end
end
context 'when project is hidden' do
let(:hidden_project) { create(:project, :hidden) }
it 'does not return the hidden project' do
expect(Project).not_to receive(:find_by_full_path)
expect(subject.find_project(hidden_project.id)).to be_nil
end
end
end
describe '#find_project!' do
......
......@@ -7997,6 +7997,37 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '.not_hidden' do
it 'lists projects that are not hidden' do
project = create(:project)
hidden_project = create(:project, :hidden)
expect(described_class.not_hidden).to contain_exactly(project)
expect(described_class.not_hidden).not_to include(hidden_project)
end
end
describe '#pending_delete_or_hidden?' do
let_it_be(:project) { create(:project, name: 'test-project') }
where(:pending_delete, :hidden, :expected_result) do
true | false | true
true | true | true
false | true | true
false | false | false
end
with_them do
it 'returns true if project is pending delete or hidden' do
project.pending_delete = pending_delete
project.hidden = hidden
project.save!
expect(project.pending_delete_or_hidden?).to eq(expected_result)
end
end
end
private
def finish_job(export_job)
......
......@@ -9,6 +9,7 @@ itself: # project
- external_webhook_token
- has_external_issue_tracker
- has_external_wiki
- hidden
- import_source
- import_type
- import_url
......
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