Commit 908902d1 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'requirements-model' into 'master'

Add Requirement model

See merge request gitlab-org/gitlab!26097
parents 9a81718f 886e4652
......@@ -21,7 +21,7 @@ class InternalId < ApplicationRecord
belongs_to :project
belongs_to :namespace
enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 }
enum usage: ::InternalIdEnums.usage_resources
validates :usage, presence: true
......
# frozen_string_literal: true
module InternalIdEnums
def self.usage_resources
# when adding new resource, make sure it doesn't conflict with EE usage_resources
{ issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 }
end
end
InternalIdEnums.prepend_if_ee('EE::InternalIdEnums')
---
title: Add migration for Requirement model
merge_request: 26097
author:
type: added
# frozen_string_literal: true
class CreateRequirements < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :requirements do |t|
t.timestamps_with_timezone null: false
t.integer :project_id, null: false
t.integer :author_id
t.integer :iid, null: false
t.integer :cached_markdown_version
t.integer :state, limit: 2, default: 1, null: false
t.string :title, limit: 255, null: false
t.text :title_html
t.index :project_id
t.index :author_id
t.index :title, name: "index_requirements_on_title_trigram", using: :gin, opclass: :gin_trgm_ops
t.index :state
t.index :created_at
t.index :updated_at
t.index %w(project_id iid), name: 'index_requirements_on_project_id_and_iid', where: 'project_id IS NOT NULL', unique: true, using: :btree
end
end
end
# frozen_string_literal: true
class RequirementsAddProjectFk < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key(:requirements, :projects, column: :project_id, on_delete: :cascade) # rubocop: disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key(:requirements, column: :project_id)
end
end
end
# frozen_string_literal: true
class RequirementsAddAuthorFk < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key(:requirements, :users, column: :author_id, on_delete: :nullify) # rubocop: disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key(:requirements, column: :author_id)
end
end
end
......@@ -3723,6 +3723,25 @@ ActiveRecord::Schema.define(version: 2020_03_04_160823) do
t.index ["project_id", "programming_language_id"], name: "index_repository_languages_on_project_and_languages_id", unique: true
end
create_table "requirements", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id", null: false
t.integer "author_id"
t.integer "iid", null: false
t.integer "cached_markdown_version"
t.integer "state", limit: 2, default: 1, null: false
t.string "title", limit: 255, null: false
t.text "title_html"
t.index ["author_id"], name: "index_requirements_on_author_id"
t.index ["created_at"], name: "index_requirements_on_created_at"
t.index ["project_id", "iid"], name: "index_requirements_on_project_id_and_iid", unique: true, where: "(project_id IS NOT NULL)"
t.index ["project_id"], name: "index_requirements_on_project_id"
t.index ["state"], name: "index_requirements_on_state"
t.index ["title"], name: "index_requirements_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["updated_at"], name: "index_requirements_on_updated_at"
end
create_table "resource_label_events", force: :cascade do |t|
t.integer "action", null: false
t.integer "issue_id"
......@@ -5001,6 +5020,8 @@ ActiveRecord::Schema.define(version: 2020_03_04_160823) do
add_foreign_key "releases", "users", column: "author_id", name: "fk_8e4456f90f", on_delete: :nullify
add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade
add_foreign_key "repository_languages", "projects", on_delete: :cascade
add_foreign_key "requirements", "projects", on_delete: :cascade
add_foreign_key "requirements", "users", column: "author_id", on_delete: :nullify
add_foreign_key "resource_label_events", "epics", on_delete: :cascade
add_foreign_key "resource_label_events", "issues", on_delete: :cascade
add_foreign_key "resource_label_events", "labels", on_delete: :nullify
......
# frozen_string_literal: true
module EE
module InternalIdEnums
extend ActiveSupport::Concern
class_methods do
extend ::Gitlab::Utils::Override
override :usage_resources
def usage_resources
super.merge(requirements: 1000)
end
end
end
end
......@@ -59,6 +59,7 @@ module EE
has_many :audit_events, as: :entity
has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design'
has_many :path_locks
has_many :requirements
# the rationale behind vulnerabilities and vulnerability_findings can be found here:
# https://gitlab.com/gitlab-org/gitlab/issues/10252#terminology
......
......@@ -32,6 +32,7 @@ module EE
has_many :reviews, foreign_key: :author_id, inverse_of: :author
has_many :epics, foreign_key: :author_id
has_many :requirements, foreign_key: :author_id
has_many :assigned_epics, foreign_key: :assignee_id, class_name: "Epic"
has_many :path_locks, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :vulnerability_feedback, foreign_key: :author_id, class_name: 'Vulnerabilities::Feedback'
......
# frozen_string_literal: true
class Requirement < ApplicationRecord
include CacheMarkdownField
include StripAttribute
include AtomicInternalId
cache_markdown_field :title, pipeline: :single_line
strip_attributes :title
belongs_to :author, class_name: 'User'
belongs_to :project
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.requirements&.maximum(:iid) }
validates :author, :project, :title, presence: true
validates :title, length: { maximum: Issuable::TITLE_LENGTH_MAX }
validates :title_html, length: { maximum: Issuable::TITLE_HTML_LENGTH_MAX }, allow_blank: true
enum state: { opened: 1, archived: 2 }
# In the next iteration we will support also group-level requirements
# so it's better to use resource_parent instead of project directly
def resource_parent
project
end
end
......@@ -7,6 +7,7 @@ module EE
def migrate_records
migrate_epics
migrate_requirements
migrate_vulnerabilities_feedback
migrate_reviews
super
......@@ -19,6 +20,10 @@ module EE
end
# rubocop: enable CodeReuse/ActiveRecord
def migrate_requirements
user.requirements.update_all(author_id: ghost_user.id)
end
def migrate_vulnerabilities_feedback
user.vulnerability_feedback.update_all(author_id: ghost_user.id)
user.commented_vulnerability_feedback.update_all(comment_author_id: ghost_user.id)
......
# frozen_string_literal: true
FactoryBot.define do
factory :requirement do
project
author
title { generate(:title) }
title_html { "<h2>#{title}</h2>" }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Requirement do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
describe 'associations' do
subject { build(:requirement) }
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
subject { build(:requirement) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(::Issuable::TITLE_LENGTH_MAX) }
it { is_expected.to validate_length_of(:title_html).is_at_most(::Issuable::TITLE_HTML_LENGTH_MAX) }
end
end
......@@ -4,8 +4,8 @@ require 'spec_helper'
describe Users::MigrateToGhostUserService do
context 'epics' do
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
context 'deleted user is present as both author and edited_user' do
include_examples "migrating a deleted user's associated records to the ghost user", Epic, [:author, :last_edited_by] do
......@@ -23,8 +23,8 @@ describe Users::MigrateToGhostUserService do
end
context 'vulnerability_feedback author' do
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
include_examples "migrating a deleted user's associated records to the ghost user", Vulnerabilities::Feedback, [:author] do
let(:created_record) { create(:vulnerability_feedback, author: user) }
......@@ -32,8 +32,8 @@ describe Users::MigrateToGhostUserService do
end
context 'vulnerability_feedback comment author' do
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
include_examples "migrating a deleted user's associated records to the ghost user", Vulnerabilities::Feedback, [:comment_author] do
let(:created_record) { create(:vulnerability_feedback, comment_author: user) }
......@@ -41,11 +41,20 @@ describe Users::MigrateToGhostUserService do
end
context 'reviews' do
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
include_examples "migrating a deleted user's associated records to the ghost user", Review, [:author] do
let(:created_record) { create(:review, author: user) }
end
end
context 'requirements' do
let!(:user) { create(:user) }
let(:service) { described_class.new(user) }
include_examples "migrating a deleted user's associated records to the ghost user", Requirement, [:author] do
let(:created_record) { create(:requirement, author: user) }
end
end
end
......@@ -467,6 +467,7 @@ project:
- resource_groups
- autoclose_referenced_issues
- status_page_setting
- requirements
award_emoji:
- awardable
- user
......
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