Commit 09348ff8 authored by Jan Provaznik's avatar Jan Provaznik

Add resource label event model and service

This model and service will be used for tracking label changes
on issuable resources. Currently it's not used yet (the reason is
that we don't want to record label events and system notes for labels
at the same time), it will be enabled as part of #48483
parent 63e4129d
# == LabelEventable concern
#
# Contains functionality related to objects that support adding/removing events.
#
# Used by Issue and MergeRequest.
#
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,
......
class ResourceLabelEvent < ActiveRecord::Base
ISSUABLE_COLUMNS = %i(issue_id merge_request_id).freeze
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 :issuable_id_is_present
enum action: {
add: 1,
remove: 2
}
def issuable
issue || merge_request
end
private
def issuable_columns
ISSUABLE_COLUMNS
end
def issuable_id_is_present
ids = issuable_columns.find_all {|attr| self[attr]}
if ids.size != 1
errors.add(:base, "Exactly one of #{issuable_columns.join(', ')} is required")
end
end
end
module ResourceLabelEventService
extend self
def change_labels(resource, user, 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)
if resource.is_a?(Issue)
:issue_id
elsif resource.is_a?(MergeRequest)
:merge_request_id
else
raise ArgumentError, "Unknown resource type #{resource.class.name}"
end
end
end
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 :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"
...@@ -2357,6 +2357,20 @@ ActiveRecord::Schema.define(version: 20180722103201) do ...@@ -2357,6 +2357,20 @@ 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 "label_id"
t.integer "user_id"
t.datetime_with_timezone "created_at", null: false
end
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
...@@ -3017,6 +3031,10 @@ ActiveRecord::Schema.define(version: 20180722103201) do ...@@ -3017,6 +3031,10 @@ 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", "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
......
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
......
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
require 'spec_helper'
describe ResourceLabelEventService do
set(:project) { create(:project) }
set(:author) { create(:user) }
let(:resource) { create(:issue, project: project) }
describe '.change_labels' do
subject { described_class.change_labels(resource, author, added, removed) }
let(:labels) { create_list(:label, 2, project: project) }
let(:added) { [labels[0]] }
let(:removed) { [labels[1]] }
it 'creates an event for each label in 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
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