diff options
-rw-r--r-- | app/models/concerns/label_eventable.rb | 16 | ||||
-rw-r--r-- | app/models/issue.rb | 1 | ||||
-rw-r--r-- | app/models/merge_request.rb | 1 | ||||
-rw-r--r-- | app/models/resource_label_event.rb | 35 | ||||
-rw-r--r-- | app/services/resource_events/change_labels_service.rb | 43 | ||||
-rw-r--r-- | changelogs/unreleased/jprovazn-resource-events.yml | 5 | ||||
-rw-r--r-- | db/migrate/20180726172057_create_resource_label_events.rb | 18 | ||||
-rw-r--r-- | db/schema.rb | 20 | ||||
-rw-r--r-- | spec/factories/resource_label_events.rb | 10 | ||||
-rw-r--r-- | spec/lib/gitlab/import_export/all_models.yml | 2 | ||||
-rw-r--r-- | spec/models/resource_label_event_spec.rb | 48 | ||||
-rw-r--r-- | spec/services/resource_events/change_labels_service_spec.rb | 53 |
12 files changed, 251 insertions, 1 deletions
diff --git a/app/models/concerns/label_eventable.rb b/app/models/concerns/label_eventable.rb new file mode 100644 index 00000000000..d22d93448e4 --- /dev/null +++ b/app/models/concerns/label_eventable.rb @@ -0,0 +1,16 @@ +# 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 diff --git a/app/models/issue.rb b/app/models/issue.rb index 4715d942c8d..e4ed06f9a69 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -12,6 +12,7 @@ class Issue < ActiveRecord::Base include TimeTrackable include ThrottledTouch include IgnorableColumn + include LabelEventable ignore_column :assignee_id, :branch_name, :deleted_at diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b4090fd8baf..124ff6b3f91 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -10,6 +10,7 @@ class MergeRequest < ActiveRecord::Base include EachBatch include ThrottledTouch include Gitlab::Utils::StrongMemoize + include LabelEventable ignore_column :locked_at, :ref_fetched, diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb new file mode 100644 index 00000000000..42c255fcd1e --- /dev/null +++ b/app/models/resource_label_event.rb @@ -0,0 +1,35 @@ +# 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 + 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 diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb new file mode 100644 index 00000000000..8edb0ddb3ed --- /dev/null +++ b/app/services/resource_events/change_labels_service.rb @@ -0,0 +1,43 @@ +# 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 + 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 diff --git a/changelogs/unreleased/jprovazn-resource-events.yml b/changelogs/unreleased/jprovazn-resource-events.yml new file mode 100644 index 00000000000..05643150f16 --- /dev/null +++ b/changelogs/unreleased/jprovazn-resource-events.yml @@ -0,0 +1,5 @@ +--- +title: Add new model for tracking label events. +merge_request: +author: +type: added diff --git a/db/migrate/20180726172057_create_resource_label_events.rb b/db/migrate/20180726172057_create_resource_label_events.rb new file mode 100644 index 00000000000..2ef7078d898 --- /dev/null +++ b/db/migrate/20180726172057_create_resource_label_events.rb @@ -0,0 +1,18 @@ +# 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 :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 diff --git a/db/schema.rb b/db/schema.rb index a5f7b8149c6..97e7e28df09 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # 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 enable_extension "plpgsql" @@ -1787,6 +1787,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", ["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| t.integer "source_id", null: false t.string "source_type", null: false @@ -2337,6 +2351,10 @@ ActiveRecord::Schema.define(version: 20180722103201) do add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", 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 "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade add_foreign_key "subscriptions", "projects", on_delete: :cascade diff --git a/spec/factories/resource_label_events.rb b/spec/factories/resource_label_events.rb new file mode 100644 index 00000000000..a67ad78c098 --- /dev/null +++ b/spec/factories/resource_label_events.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :resource_label_event do + user { issue.project.creator } + action :add + label + issue + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index db5aab0cd76..c175dc1e4dd 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -7,6 +7,7 @@ issues: - updated_by - milestone - notes +- resource_label_events - label_links - labels - last_edited_by @@ -76,6 +77,7 @@ merge_requests: - updated_by - milestone - notes +- resource_label_events - label_links - labels - last_edited_by diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb new file mode 100644 index 00000000000..4756caa1b97 --- /dev/null +++ b/spec/models/resource_label_event_spec.rb @@ -0,0 +1,48 @@ +# 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 diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb new file mode 100644 index 00000000000..41b0fb3eea3 --- /dev/null +++ b/spec/services/resource_events/change_labels_service_spec.rb @@ -0,0 +1,53 @@ +# 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 |