Commit 3d2af926 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'ee-jprovazn-resource-events' into 'master'

[EE] Add resource label event model and service

See merge request gitlab-org/gitlab-ee!6697
parents 8f5f1f3e 4877e993
# frozen_string_literal: true
# == LabelEventable concern
#
# Contains functionality related to objects that support adding/removing labels.
#
# This concern is not used yet, it will be used for:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
module LabelEventable
extend ActiveSupport::Concern
included do
has_many :resource_label_events
end
end
...@@ -16,6 +16,7 @@ class Issue < ActiveRecord::Base ...@@ -16,6 +16,7 @@ class Issue < ActiveRecord::Base
include TimeTrackable include TimeTrackable
include ThrottledTouch include ThrottledTouch
include IgnorableColumn include IgnorableColumn
include LabelEventable
ignore_column :assignee_id, :branch_name, :deleted_at ignore_column :assignee_id, :branch_name, :deleted_at
......
...@@ -11,6 +11,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -11,6 +11,7 @@ class MergeRequest < ActiveRecord::Base
include EachBatch include EachBatch
include ThrottledTouch include ThrottledTouch
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include LabelEventable
ignore_column :locked_at, ignore_column :locked_at,
:ref_fetched, :ref_fetched,
......
# frozen_string_literal: true
# This model is not used yet, it will be used for:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
class ResourceLabelEvent < ActiveRecord::Base
prepend EE::ResourceLabelEvent
belongs_to :user
belongs_to :issue
belongs_to :merge_request
belongs_to :label
validates :user, presence: true, on: :create
validates :label, presence: true, on: :create
validate :exactly_one_issuable
enum action: {
add: 1,
remove: 2
}
def self.issuable_columns
%i(issue_id merge_request_id).freeze
end
def issuable
issue || merge_request
end
private
def exactly_one_issuable
if self.class.issuable_columns.count { |attr| self[attr] } != 1
errors.add(:base, "Exactly one of #{self.class.issuable_columns.join(', ')} is required")
end
end
end
# frozen_string_literal: true
# This service is not used yet, it will be used for:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
module ResourceEvents
class ChangeLabelsService
prepend EE::ResourceEvents::ChangeLabelsService
attr_reader :resource, :user
def initialize(resource, user)
@resource, @user = resource, user
end
def execute(added_labels: [], removed_labels: [])
label_hash = {
resource_column(resource) => resource.id,
user_id: user.id,
created_at: Time.now
}
labels = added_labels.map do |label|
label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['add'])
end
labels += removed_labels.map do |label|
label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove'])
end
Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels)
end
private
def resource_column(resource)
case resource
when Issue
:issue_id
when MergeRequest
:merge_request_id
else
raise ArgumentError, "Unknown resource type #{resource.class.name}"
end
end
end
end
---
title: Add new model for tracking label events.
merge_request:
author:
type: added
# frozen_string_literal: true
class CreateResourceLabelEvents < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :resource_label_events, id: :bigserial do |t|
t.integer :action, null: false
t.references :issue, null: true, index: true, foreign_key: { on_delete: :cascade }
t.references :merge_request, null: true, index: true, foreign_key: { on_delete: :cascade }
t.references :epic, null: true, index: true, foreign_key: { on_delete: :cascade }
t.references :label, index: true, foreign_key: { on_delete: :nullify }
t.references :user, index: true, foreign_key: { on_delete: :nullify }
t.datetime_with_timezone :created_at, null: false
end
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: 20180722103201) do ActiveRecord::Schema.define(version: 20180726172057) 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"
...@@ -2359,6 +2359,22 @@ ActiveRecord::Schema.define(version: 20180722103201) do ...@@ -2359,6 +2359,22 @@ ActiveRecord::Schema.define(version: 20180722103201) do
add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree
add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
create_table "resource_label_events", id: :bigserial, force: :cascade do |t|
t.integer "action", null: false
t.integer "issue_id"
t.integer "merge_request_id"
t.integer "epic_id"
t.integer "label_id"
t.integer "user_id"
t.datetime_with_timezone "created_at", null: false
end
add_index "resource_label_events", ["epic_id"], name: "index_resource_label_events_on_epic_id", using: :btree
add_index "resource_label_events", ["issue_id"], name: "index_resource_label_events_on_issue_id", using: :btree
add_index "resource_label_events", ["label_id"], name: "index_resource_label_events_on_label_id", using: :btree
add_index "resource_label_events", ["merge_request_id"], name: "index_resource_label_events_on_merge_request_id", using: :btree
add_index "resource_label_events", ["user_id"], name: "index_resource_label_events_on_user_id", using: :btree
create_table "routes", force: :cascade do |t| create_table "routes", force: :cascade do |t|
t.integer "source_id", null: false t.integer "source_id", null: false
t.string "source_type", null: false t.string "source_type", null: false
...@@ -3034,6 +3050,11 @@ ActiveRecord::Schema.define(version: 20180722103201) do ...@@ -3034,6 +3050,11 @@ ActiveRecord::Schema.define(version: 20180722103201) do
add_foreign_key "push_rules", "projects", name: "fk_83b29894de", on_delete: :cascade add_foreign_key "push_rules", "projects", name: "fk_83b29894de", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade
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
add_foreign_key "resource_label_events", "merge_requests", on_delete: :cascade
add_foreign_key "resource_label_events", "users", on_delete: :nullify
add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade add_foreign_key "slack_integrations", "services", on_delete: :cascade
......
...@@ -9,6 +9,7 @@ module EE ...@@ -9,6 +9,7 @@ module EE
include Noteable include Noteable
include Referable include Referable
include Awardable include Awardable
include LabelEventable
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
belongs_to :group belongs_to :group
......
# frozen_string_literal: true
module EE
module ResourceLabelEvent
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
belongs_to :epic
end
class_methods do
def issuable_columns
%i(epic_id).freeze + super
end
end
override :issuable
def issuable
epic || super
end
end
end
# frozen_string_literal: true
module EE
module ResourceEvents
module ChangeLabelsService
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :resource_column
def resource_column(resource)
resource.is_a?(Epic) ? :epic_id : super
end
end
end
end
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ResourceLabelEvent, type: :model do
subject { build(:resource_label_event) }
let(:epic) { create(:epic) }
describe 'validations' do
describe 'Issuable validation' do
it 'is valid if only epic_id is set' do
subject.attributes = { epic: epic, issue: nil, merge_request: nil }
expect(subject).to be_valid
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :resource_label_event do
user { issue.project.creator }
action :add
label
issue
end
end
...@@ -7,6 +7,7 @@ issues: ...@@ -7,6 +7,7 @@ issues:
- updated_by - updated_by
- milestone - milestone
- notes - notes
- resource_label_events
- label_links - label_links
- labels - labels
- last_edited_by - last_edited_by
...@@ -79,6 +80,7 @@ merge_requests: ...@@ -79,6 +80,7 @@ merge_requests:
- updated_by - updated_by
- milestone - milestone
- notes - notes
- resource_label_events
- label_links - label_links
- labels - labels
- last_edited_by - last_edited_by
......
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ResourceLabelEvent, type: :model do
subject { build(:resource_label_event) }
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:issue) }
it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:label) }
end
describe 'validations' do
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:label) }
it { is_expected.to validate_presence_of(:user) }
describe 'Issuable validation' do
it 'is invalid if issue_id and merge_request_id are missing' do
subject.attributes = { issue: nil, merge_request: nil }
expect(subject).to be_invalid
end
it 'is invalid if issue_id and merge_request_id are set' do
subject.attributes = { issue: issue, merge_request: merge_request }
expect(subject).to be_invalid
end
it 'is valid if only issue_id is set' do
subject.attributes = { issue: issue, merge_request: nil }
expect(subject).to be_valid
end
it 'is valid if only merge_request_id is set' do
subject.attributes = { merge_request: merge_request, issue: nil }
expect(subject).to be_valid
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ResourceEvents::ChangeLabelsService do
set(:project) { create(:project) }
set(:author) { create(:user) }
let(:resource) { create(:issue, project: project) }
describe '.change_labels' do
subject { described_class.new(resource, author).execute(added_labels: added, removed_labels: removed) }
let(:labels) { create_list(:label, 2, project: project) }
def expect_label_event(event, label, action)
expect(event.user).to eq(author)
expect(event.label).to eq(label)
expect(event.action).to eq(action)
end
context 'when adding a label' do
let(:added) { [labels[0]] }
let(:removed) { [] }
it 'creates new label event' do
expect { subject }.to change { resource.resource_label_events.count }.from(0).to(1)
expect_label_event(resource.resource_label_events.first, labels[0], 'add')
end
end
context 'when removing a label' do
let(:added) { [] }
let(:removed) { [labels[1]] }
it 'creates new label event' do
expect { subject }.to change { resource.resource_label_events.count }.from(0).to(1)
expect_label_event(resource.resource_label_events.first, labels[1], 'remove')
end
end
context 'when both adding and removing labels' do
let(:added) { [labels[0]] }
let(:removed) { [labels[1]] }
it 'creates all label events in a single query' do
expect(Gitlab::Database).to receive(:bulk_insert).once.and_call_original
expect { subject }.to change { resource.resource_label_events.count }.from(0).to(2)
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