Commit 66a8e157 authored by Alex Buijs's avatar Alex Buijs

Add onboarding_progress table and model

With individual columns for actions
parent e460ade7
......@@ -29,6 +29,7 @@ class Namespace < ApplicationRecord
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
has_many :namespace_onboarding_actions
has_one :onboarding_progress
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
......
# frozen_string_literal: true
class OnboardingProgress < ApplicationRecord
belongs_to :namespace, optional: false
validate :namespace_is_root_namespace
ACTIONS = [
:git_pull,
:git_write,
:merge_request_created,
:pipeline_created,
:user_added,
:trial_started,
:subscription_created,
:required_mr_approvals_enabled,
:code_owners_enabled,
:scoped_label_created,
:security_scan_enabled,
:issue_auto_closed,
:repository_imported,
:repository_mirrored
].freeze
class << self
def onboard(namespace)
return unless root_namespace?(namespace)
safe_find_or_create_by(namespace: namespace)
end
def register(namespace, action)
return unless root_namespace?(namespace) && ACTIONS.include?(action)
action_column = column_name(action)
onboarding_progress = find_by(namespace: namespace, action_column => nil)
onboarding_progress&.update!(action_column => Time.current)
end
def completed?(namespace, action)
return unless root_namespace?(namespace) && ACTIONS.include?(action)
action_column = column_name(action)
where(namespace: namespace).where.not(action_column => nil).exists?
end
private
def column_name(action)
:"#{action}_at"
end
def root_namespace?(namespace)
namespace && namespace.root?
end
end
private
def namespace_is_root_namespace
return unless namespace
errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent?
end
end
---
title: Change onboarding actions table to use one record per namespace
merge_request: 50711
author:
type: changed
# frozen_string_literal: true
class CreateOnboardingProgress < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
create_table :onboarding_progresses do |t|
t.references :namespace, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
t.timestamps_with_timezone null: false
t.datetime_with_timezone :git_pull_at
t.datetime_with_timezone :git_write_at
t.datetime_with_timezone :merge_request_created_at
t.datetime_with_timezone :pipeline_created_at
t.datetime_with_timezone :user_added_at
t.datetime_with_timezone :trial_started_at
t.datetime_with_timezone :subscription_created_at
t.datetime_with_timezone :required_mr_approvals_enabled_at
t.datetime_with_timezone :code_owners_enabled_at
t.datetime_with_timezone :scoped_label_created_at
t.datetime_with_timezone :security_scan_enabled_at
t.datetime_with_timezone :issue_auto_closed_at
t.datetime_with_timezone :repository_imported_at
t.datetime_with_timezone :repository_mirrored_at
end
end
end
def down
with_lock_retries do
drop_table :onboarding_progresses
end
end
end
c2766b50914c6b4d8c96fb958cdfb67f0d29e40df45654c35d62792c272e3d5a
\ No newline at end of file
......@@ -14465,6 +14465,36 @@ CREATE SEQUENCE oauth_openid_requests_id_seq
ALTER SEQUENCE oauth_openid_requests_id_seq OWNED BY oauth_openid_requests.id;
CREATE TABLE onboarding_progresses (
id bigint NOT NULL,
namespace_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
git_pull_at timestamp with time zone,
git_write_at timestamp with time zone,
merge_request_created_at timestamp with time zone,
pipeline_created_at timestamp with time zone,
user_added_at timestamp with time zone,
trial_started_at timestamp with time zone,
subscription_created_at timestamp with time zone,
required_mr_approvals_enabled_at timestamp with time zone,
code_owners_enabled_at timestamp with time zone,
scoped_label_created_at timestamp with time zone,
security_scan_enabled_at timestamp with time zone,
issue_auto_closed_at timestamp with time zone,
repository_imported_at timestamp with time zone,
repository_mirrored_at timestamp with time zone
);
CREATE SEQUENCE onboarding_progresses_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE onboarding_progresses_id_seq OWNED BY onboarding_progresses.id;
CREATE TABLE open_project_tracker_data (
id bigint NOT NULL,
service_id integer NOT NULL,
......@@ -18605,6 +18635,8 @@ ALTER TABLE ONLY oauth_applications ALTER COLUMN id SET DEFAULT nextval('oauth_a
ALTER TABLE ONLY oauth_openid_requests ALTER COLUMN id SET DEFAULT nextval('oauth_openid_requests_id_seq'::regclass);
ALTER TABLE ONLY onboarding_progresses ALTER COLUMN id SET DEFAULT nextval('onboarding_progresses_id_seq'::regclass);
ALTER TABLE ONLY open_project_tracker_data ALTER COLUMN id SET DEFAULT nextval('open_project_tracker_data_id_seq'::regclass);
ALTER TABLE ONLY operations_feature_flag_scopes ALTER COLUMN id SET DEFAULT nextval('operations_feature_flag_scopes_id_seq'::regclass);
......@@ -19935,6 +19967,9 @@ ALTER TABLE ONLY oauth_applications
ALTER TABLE ONLY oauth_openid_requests
ADD CONSTRAINT oauth_openid_requests_pkey PRIMARY KEY (id);
ALTER TABLE ONLY onboarding_progresses
ADD CONSTRAINT onboarding_progresses_pkey PRIMARY KEY (id);
ALTER TABLE ONLY open_project_tracker_data
ADD CONSTRAINT open_project_tracker_data_pkey PRIMARY KEY (id);
......@@ -22125,6 +22160,8 @@ CREATE INDEX index_on_users_lower_username ON users USING btree (lower((username
CREATE INDEX index_on_users_name_lower ON users USING btree (lower((name)::text));
CREATE UNIQUE INDEX index_onboarding_progresses_on_namespace_id ON onboarding_progresses USING btree (namespace_id);
CREATE INDEX index_open_project_tracker_data_on_service_id ON open_project_tracker_data USING btree (service_id);
CREATE INDEX index_operations_feature_flags_issues_on_issue_id ON operations_feature_flags_issues USING btree (issue_id);
......@@ -24313,6 +24350,9 @@ ALTER TABLE ONLY geo_repository_updated_events
ALTER TABLE ONLY boards_epic_board_labels
ADD CONSTRAINT fk_rails_2bedeb8799 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
ALTER TABLE ONLY onboarding_progresses
ADD CONSTRAINT fk_rails_2ccfd420cc FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_2d2aba21ef FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
......
# frozen_string_literal: true
FactoryBot.define do
factory :onboarding_progress do
namespace
end
end
......@@ -21,6 +21,7 @@ RSpec.describe Namespace do
it { is_expected.to have_many :custom_emoji }
it { is_expected.to have_many :namespace_onboarding_actions }
it { is_expected.to have_one :package_setting_relation }
it { is_expected.to have_one :onboarding_progress }
end
describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe OnboardingProgress do
let(:namespace) { create(:namespace) }
let(:action) { :subscription_created }
describe 'associations' do
it { is_expected.to belong_to(:namespace).required }
end
describe 'validations' do
describe 'namespace_is_root_namespace' do
subject(:onboarding_progress) { build(:onboarding_progress, namespace: namespace)}
context 'when associated namespace is root' do
it { is_expected.to be_valid }
end
context 'when associated namespace is not root' do
let(:namespace) { build(:group, :nested) }
it 'is invalid' do
expect(onboarding_progress).to be_invalid
expect(onboarding_progress.errors[:namespace]).to include('must be a root namespace')
end
end
end
end
describe '.onboard' do
subject(:onboard) { described_class.onboard(namespace) }
it 'adds a record for the namespace' do
expect { onboard }.to change(described_class, :count).from(0).to(1)
end
context 'when not given a namespace' do
let(:namespace) { nil }
it 'does not add a record for the namespace' do
expect { onboard }.not_to change(described_class, :count).from(0)
end
end
context 'when not given a root namespace' do
let(:namespace) { create(:namespace, parent: build(:namespace)) }
it 'does not add a record for the namespace' do
expect { onboard }.not_to change(described_class, :count).from(0)
end
end
end
describe '.register' do
subject(:register_action) { described_class.register(namespace, action) }
context 'when the namespace was onboarded' do
before do
described_class.onboard(namespace)
end
it 'registers the action for the namespace' do
expect { register_action }.to change { described_class.completed?(namespace, action) }.from(false).to(true)
end
context 'when the action does not exist' do
let(:action) { :foo }
it 'does not register the action for the namespace' do
expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(nil)
end
end
end
context 'when the namespace was not onboarded' do
it 'does not register the action for the namespace' do
expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(false)
end
end
end
describe '.completed?' do
subject { described_class.completed?(namespace, action) }
context 'when the namespace has not yet been onboarded' do
it { is_expected.to eq(false) }
end
context 'when the namespace has been onboarded but not registered the action yet' do
before do
described_class.onboard(namespace)
end
it { is_expected.to eq(false) }
context 'when the action has been registered' do
before do
described_class.register(namespace, action)
end
it { is_expected.to eq(true) }
end
end
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