summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/models/concerns/label_eventable.rb16
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/resource_label_event.rb35
-rw-r--r--app/services/resource_events/change_labels_service.rb43
-rw-r--r--changelogs/unreleased/jprovazn-resource-events.yml5
-rw-r--r--db/migrate/20180726172057_create_resource_label_events.rb18
-rw-r--r--db/schema.rb20
-rw-r--r--spec/factories/resource_label_events.rb10
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/models/resource_label_event_spec.rb48
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb53
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