From 88e05f80719f1e540891362b3ac5a7341aec53da Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Wed, 21 Dec 2016 21:45:31 -0600 Subject: [PATCH 1/5] Minimum changes in models to support issue relationships Adds 'blocks' and 'blocked by' relationships to issues. There should be more relationships but this is a starting point. When an issue specifies that it blocks or is blocked by another issue an inverse relationship is also created. This means we get 2 database records for each relationship but it was the easiest and least expensive way to implement the feature. --- app/models/issue.rb | 3 ++ app/models/related_issue.rb | 41 +++++++++++++++++++ app/models/related_issues/blocked_issue.rb | 0 .../20161221052938_create_related_issues.rb | 11 +++++ ...222214701_add_indices_to_related_issues.rb | 16 ++++++++ db/schema.rb | 11 ++++- spec/models/related_issue_spec.rb | 5 +++ 7 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 app/models/related_issue.rb create mode 100644 app/models/related_issues/blocked_issue.rb create mode 100644 db/migrate/20161221052938_create_related_issues.rb create mode 100644 db/migrate/20161222214701_add_indices_to_related_issues.rb create mode 100644 spec/models/related_issue_spec.rb diff --git a/app/models/issue.rb b/app/models/issue.rb index 738c96e4db3d..5b561958e618 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -23,6 +23,9 @@ class Issue < ActiveRecord::Base has_many :events, as: :target, dependent: :destroy has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all + has_many :related_issues + has_many :blocked_issues, -> { where(related_issues: { relationship_type: 0 }) }, through: :related_issues, source: :related_issue + has_many :blocked_by_issues, -> { where(related_issues: { relationship_type: 1 }) }, through: :related_issues, source: :related_issue validates :project, presence: true diff --git a/app/models/related_issue.rb b/app/models/related_issue.rb new file mode 100644 index 000000000000..4ba52aeacfbc --- /dev/null +++ b/app/models/related_issue.rb @@ -0,0 +1,41 @@ +class RelatedIssue < ActiveRecord::Base + belongs_to :issue + belongs_to :related_issue, class_name: 'Issue' + + after_create :create_inverse, unless: :has_inverse? + after_destroy :destroy_inverses, if: :has_inverse? + + enum relationship_type: { + blocks: 0, + blocked_by: 1, + } + + INVERSE_RELATIONSHIPS = { + relationship_types[:blocks] => relationship_types[:blocked_by], + relationship_types[:blocked_by] => relationship_types[:blocks] + } + + def create_inverse + self.class.create(inverse_options) + end + + def destroy_inverses + inverses.destroy_all + end + + def has_inverse? + self.class.exists?(inverse_options) + end + + def inverses + self.class.where(inverse_options) + end + + def inverse_relationship_type + INVERSE_RELATIONSHIPS[self.class.relationship_types[relationship_type]] + end + + def inverse_options + { issue: related_issue, related_issue: issue, relationship_type: inverse_relationship_type } + end +end diff --git a/app/models/related_issues/blocked_issue.rb b/app/models/related_issues/blocked_issue.rb new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/db/migrate/20161221052938_create_related_issues.rb b/db/migrate/20161221052938_create_related_issues.rb new file mode 100644 index 000000000000..5ee8c65a31bd --- /dev/null +++ b/db/migrate/20161221052938_create_related_issues.rb @@ -0,0 +1,11 @@ +class CreateRelatedIssues < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :related_issues do |t| + t.integer :issue_id + t.integer :related_issue_id + t.integer :relationship_type + end + end +end diff --git a/db/migrate/20161222214701_add_indices_to_related_issues.rb b/db/migrate/20161222214701_add_indices_to_related_issues.rb new file mode 100644 index 000000000000..e32ad34c0a67 --- /dev/null +++ b/db/migrate/20161222214701_add_indices_to_related_issues.rb @@ -0,0 +1,16 @@ +class AddIndicesToRelatedIssues < ActiveRecord::Migration + DOWNTIME = false + + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_concurrent_index :related_issues, :issue_id + add_concurrent_index :related_issues, :related_issue_id + end + + def down + remove_index :related_issues, :issue_id if index_exists?(:related_issues, :issue_id) + remove_index :related_issues, :related_issue_id if index_exists?(:related_issues, :related_issue_id) + end +end diff --git a/db/schema.rb b/db/schema.rb index 14801b581e6e..d1950126267a 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: 20161213172958) do +ActiveRecord::Schema.define(version: 20161222214701) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -984,6 +984,15 @@ add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree + create_table "related_issues", force: :cascade do |t| + t.integer "issue_id" + t.integer "related_issue_id" + t.integer "relationship_type" + end + + add_index "related_issues", ["issue_id"], name: "index_related_issues_on_issue_id", using: :btree + add_index "related_issues", ["related_issue_id"], name: "index_related_issues_on_related_issue_id", using: :btree + create_table "releases", force: :cascade do |t| t.string "tag" t.text "description" diff --git a/spec/models/related_issue_spec.rb b/spec/models/related_issue_spec.rb new file mode 100644 index 000000000000..8ca0a751ecfe --- /dev/null +++ b/spec/models/related_issue_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe RelatedIssue, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end -- GitLab From 25a4a7d1242dac5f75b9096f306a24ca63bd2cba Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 22 Dec 2016 16:04:49 -0600 Subject: [PATCH 2/5] Fixes --- app/models/issue.rb | 6 ++++-- app/models/related_issues/blocked_issue.rb | 0 2 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 app/models/related_issues/blocked_issue.rb diff --git a/app/models/issue.rb b/app/models/issue.rb index 5b561958e618..8069f0807491 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -24,8 +24,10 @@ class Issue < ActiveRecord::Base has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all has_many :related_issues - has_many :blocked_issues, -> { where(related_issues: { relationship_type: 0 }) }, through: :related_issues, source: :related_issue - has_many :blocked_by_issues, -> { where(related_issues: { relationship_type: 1 }) }, through: :related_issues, source: :related_issue + has_many :blocked_issues, -> { where(related_issues: { relationship_type: RelatedIssue.relationship_types[:blocks] }) }, + through: :related_issues, source: :related_issue + has_many :blocked_by_issues, -> { where(related_issues: { relationship_type: RelatedIssue.relationship_types[:blocked_by] }) }, + through: :related_issues, source: :related_issue validates :project, presence: true diff --git a/app/models/related_issues/blocked_issue.rb b/app/models/related_issues/blocked_issue.rb deleted file mode 100644 index e69de29bb2d1..000000000000 -- GitLab From 871f7336c6507fb435823a943eee75bf1de1147a Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 22 Dec 2016 16:17:34 -0600 Subject: [PATCH 3/5] Clean up and add validations --- app/models/related_issue.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/related_issue.rb b/app/models/related_issue.rb index 4ba52aeacfbc..53421d50e89b 100644 --- a/app/models/related_issue.rb +++ b/app/models/related_issue.rb @@ -2,8 +2,12 @@ class RelatedIssue < ActiveRecord::Base belongs_to :issue belongs_to :related_issue, class_name: 'Issue' + validates :issue, presence: true, uniqueness: { scope: :related_issue } + validates :related_issue, presence: true, uniqueness: { scope: :issue } + validates_presence_of :relationship_type + after_create :create_inverse, unless: :has_inverse? - after_destroy :destroy_inverses, if: :has_inverse? + after_destroy :destroy_inverse, if: :has_inverse? enum relationship_type: { blocks: 0, @@ -19,8 +23,8 @@ def create_inverse self.class.create(inverse_options) end - def destroy_inverses - inverses.destroy_all + def destroy_inverse + inverse.destroy end def has_inverse? @@ -28,7 +32,7 @@ def has_inverse? end def inverses - self.class.where(inverse_options) + self.class.find_by(inverse_options) end def inverse_relationship_type -- GitLab From 9cc8d8a2485144b2457d71b1e7def7e5afd5f23f Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 22 Dec 2016 16:58:18 -0600 Subject: [PATCH 4/5] specs --- app/models/related_issue.rb | 18 ++++++----- spec/models/related_issue_spec.rb | 54 +++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/app/models/related_issue.rb b/app/models/related_issue.rb index 53421d50e89b..da56722e7281 100644 --- a/app/models/related_issue.rb +++ b/app/models/related_issue.rb @@ -19,19 +19,13 @@ class RelatedIssue < ActiveRecord::Base relationship_types[:blocked_by] => relationship_types[:blocks] } - def create_inverse - self.class.create(inverse_options) - end - - def destroy_inverse - inverse.destroy - end + private def has_inverse? self.class.exists?(inverse_options) end - def inverses + def inverse self.class.find_by(inverse_options) end @@ -39,6 +33,14 @@ def inverse_relationship_type INVERSE_RELATIONSHIPS[self.class.relationship_types[relationship_type]] end + def create_inverse + self.class.create(inverse_options) + end + + def destroy_inverse + inverse.destroy + end + def inverse_options { issue: related_issue, related_issue: issue, relationship_type: inverse_relationship_type } end diff --git a/spec/models/related_issue_spec.rb b/spec/models/related_issue_spec.rb index 8ca0a751ecfe..97879adaa252 100644 --- a/spec/models/related_issue_spec.rb +++ b/spec/models/related_issue_spec.rb @@ -1,5 +1,53 @@ -require 'rails_helper' +require 'spec_helper' -RSpec.describe RelatedIssue, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe RelatedIssue, models: true do + + describe 'validations' do + it { is_expected.to belong_to :issue } + it { is_expected.to belong_to :related_issue } + + it { is_expected.to validate_presence_of :issue } + it { is_expected.to validate_presence_of :related_issue } + it { is_expected.to validate_presence_of :relationship_type } + + # No workie :( Why? + it { is_expected.to validate_uniqueness_of(:issue_id).scoped_to(:related_issue_id) } + it { is_expected.to validate_uniqueness_of(:related_issue_id).scoped_to(:issue_id) } + end + + + describe 'callbacks' do + let!(:issue) { create(:issue) } + let!(:other_issue) { create(:issue) } + + let!(:related_issue) do + described_class.create( + issue: issue, + related_issue: other_issue, + relationship_type: described_class.relationship_types[:blocks] + ) + end + + it 'creates an inverse' do + expect( + described_class.find_by( + issue: other_issue, + related_issue: issue, + relationship_type: described_class.relationship_types[:blocked_by] + ) + ).to be_present + end + + it 'destroys the inverse' do + related_issue.destroy + + expect( + described_class.find_by( + issue: other_issue, + related_issue: issue, + relationship_type: described_class.relationship_types[:blocked_by] + ) + ).to be_nil + end + end end -- GitLab From 80c17e150118e957030d1318ea5631f69e3561c7 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 22 Dec 2016 23:56:26 -0600 Subject: [PATCH 5/5] Minimum related issues UI --- app/assets/javascripts/issue.js | 13 ++++++++++ app/assets/stylesheets/pages/issues.scss | 9 ++++--- app/controllers/projects/issues_controller.rb | 19 ++++++++++++++- app/models/issue.rb | 2 +- .../projects/issues/_related_issues.html.haml | 24 +++++++++++++++++++ app/views/projects/issues/show.html.haml | 3 +++ config/routes/project.rb | 1 + 7 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 app/views/projects/issues/_related_issues.html.haml diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 63b70d4be17b..e36116121e03 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -20,6 +20,7 @@ this.initMergeRequests(); this.initRelatedBranches(); this.initCanCreateBranch(); + this.initRelatedIssues(); } Issue.prototype.initTaskList = function() { @@ -153,6 +154,18 @@ }); }; + Issue.prototype.initRelatedIssues = function() { + var $container; + $container = $('#related-issues'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load related issues', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + }; + return Issue; })(); diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 8734a3b15983..6c0435fc1847 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -44,17 +44,20 @@ ul.related-merge-requests > li { } .merge-requests-title, -.related-branches-title { +.related-branches-title, +.related-issues-title { font-size: 16px; font-weight: 600; } -.merge-request-id { +.merge-request-id, +.related-issue-id { display: inline-block; width: 3em; } -.merge-request-status { +.merge-request-status, +.related-issue-status { font-size: 13px; padding: 0 5px; color: $white-light; diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4f66e01e0f7a..69eb1a2e069e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -9,7 +9,8 @@ class Projects::IssuesController < Projects::ApplicationController before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, - :related_branches, :can_create_branch] + :related_branches, :can_create_branch, + :related_issues] # Allow read any issue before_action :authorize_read_issue!, only: [:show] @@ -161,6 +162,22 @@ def can_create_branch end end + def related_issues + @related_issues = {} + RelatedIssue.relationship_types.keys.each do |relationship_type| + @related_issues[relationship_type] ||= [] + @related_issues[relationship_type] = @issue.send("#{relationship_type}_issues") + end + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_related_issues') + } + end + end + end + protected def issue diff --git a/app/models/issue.rb b/app/models/issue.rb index 8069f0807491..5689cdeedd0e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -24,7 +24,7 @@ class Issue < ActiveRecord::Base has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all has_many :related_issues - has_many :blocked_issues, -> { where(related_issues: { relationship_type: RelatedIssue.relationship_types[:blocks] }) }, + has_many :blocks_issues, -> { where(related_issues: { relationship_type: RelatedIssue.relationship_types[:blocks] }) }, through: :related_issues, source: :related_issue has_many :blocked_by_issues, -> { where(related_issues: { relationship_type: RelatedIssue.relationship_types[:blocked_by] }) }, through: :related_issues, source: :related_issue diff --git a/app/views/projects/issues/_related_issues.html.haml b/app/views/projects/issues/_related_issues.html.haml new file mode 100644 index 000000000000..822fa6f548fb --- /dev/null +++ b/app/views/projects/issues/_related_issues.html.haml @@ -0,0 +1,24 @@ +- if @related_issues.any? + - @related_issues.each do |type, issues| + %h2.related-issues-title + = type.humanize.titleize + = pluralize(issues.count, 'issue').titleize + %ul.unstyled-list.related-s + - issues.each do |issue| + %li + %span.related-issue-id + = issue.to_reference + %span.related-issue-info + %strong + = link_to_gfm issue.title, issue_path(issue), class: 'row_title' + - unless @issue.project.id == issue.project.id + in + - project = issue.project + = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) + + - if issue.closed? + %span.related-issue-status.prepend-left-10.closed + Closed + - else + %span.related-issue-status.prepend-left-10.open + Open diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 981bf640a6b9..8e14f2e713bc 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -72,6 +72,9 @@ #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } } // This element is filled in using JavaScript. + #related-issues{ data: { url: related_issues_namespace_project_issue_url(@project.namespace, @project, @issue) } } + // This element is filled in using JavaScript. + .content-block.content-block-small = render 'new_branch' = render 'award_emoji/awards_block', awardable: @issue, inline: true diff --git a/config/routes/project.rb b/config/routes/project.rb index 80cc47ab9a82..9c68a16d6bb6 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -232,6 +232,7 @@ get :referenced_merge_requests get :related_branches get :can_create_branch + get :related_issues end collection do post :bulk_update -- GitLab