Commit f227ce6a authored by Clement Ho's avatar Clement Ho

Merge branch 'multiple_assignees_review' into 'multiple-assignees-fe-issues-list'

# Conflicts:
#   app/serializers/issuable_entity.rb
#   app/views/projects/issues/_issue.html.haml
parents 765008ba 974ec3ca
......@@ -15,7 +15,7 @@ export default {
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.removeTokens();
},
beforeDestroy() {
destroyed() {
this.filteredSearch.cleanup();
FilteredSearchContainer.container = document;
this.store.path = '';
......
......@@ -65,8 +65,15 @@ require('./empty_state');
},
filter: {
handler() {
this.page = 1;
this.loadIssues(true);
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
this.loadIssues(true)
.then(() => {
this.filterLoading = false;
});
}
},
deep: true,
},
......@@ -140,14 +147,14 @@ require('./empty_state');
:image="blankStateImage"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
v-if="!loading && showList"></modal-list>
v-if="!loading && showList && !filterLoading"></modal-list>
<empty-state
v-if="showEmptyState"
:image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state>
<section
class="add-issues-list text-center"
v-if="loading">
v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
......
......@@ -15,6 +15,7 @@
searchTerm: '',
loading: false,
loadingNewPage: false,
filterLoading: false,
page: 1,
perPage: 50,
filter: {
......
......@@ -30,9 +30,9 @@
$loading.fadeOut();
$selectbox.hide();
if (data.weight != null) {
$value.html(data.weight);
$value.html(`<strong>${data.weight}</strong>`);
} else {
$value.html('None');
$value.html('<span class="no-value">None</span>');
}
return $sidebarCollapsedValue.html(data.weight);
});
......
......@@ -68,23 +68,19 @@
}
@mixin btn-green {
@include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light);
@include btn-color($green-500, $green-600, $green-600, $green-700, $green-700, $green-800, $white-light);
}
@mixin btn-blue {
@include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light);
}
@mixin btn-blue-medium {
@include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light);
@include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white-light);
}
@mixin btn-orange {
@include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light);
@include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white-light);
}
@mixin btn-red {
@include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light);
@include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light);
}
@mixin btn-gray {
......@@ -145,11 +141,11 @@
&.btn-new,
&.btn-create,
&.btn-save {
@include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal);
@include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700);
}
&.btn-remove {
@include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
}
......@@ -157,11 +153,8 @@
@include btn-gray;
}
&.btn-primary {
@include btn-blue-medium;
}
&.btn-info,
&.btn-primary,
&.btn-register {
@include btn-blue;
}
......@@ -171,11 +164,11 @@
}
&.btn-close {
@include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal);
@include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
}
&.btn-spam {
@include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
&.btn-danger,
......@@ -360,7 +353,7 @@
.btn-inverted {
&-secondary {
@include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal);
@include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
}
}
......
......@@ -199,7 +199,7 @@
text-decoration: none;
.badge {
background-color: darken($row-hover, 5%);
background-color: darken($dropdown-link-hover-bg, 5%);
}
}
......
......@@ -177,34 +177,34 @@ label {
}
.gl-field-error {
color: $red-normal;
color: $red-500;
}
.gl-show-field-errors {
.gl-field-success-outline {
border: 1px solid $green-normal;
border: 1px solid $green-600;
&:focus {
box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal;
box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600;
border: 0 none;
}
}
.gl-field-error-outline {
border: 1px solid $red-normal;
border: 1px solid $red-500;
&:focus {
box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
border: 0 none;
}
}
.gl-field-success-message {
color: $green-normal;
color: $green-600;
}
.gl-field-error-message {
color: $red-normal;
color: $red-500;
}
.gl-field-hint {
......
......@@ -265,7 +265,7 @@ header {
}
.impersonation i {
color: $red-normal;
color: $red-500;
}
}
......
.ci-status-icon-success {
color: $gl-success;
color: $green-500;
svg {
fill: $gl-success;
fill: $green-500;
}
}
......@@ -17,18 +17,18 @@
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
color: $gl-warning;
color: $orange-500;
svg {
fill: $gl-warning;
fill: $orange-500;
}
}
.ci-status-icon-running {
color: $blue-normal;
color: $blue-400;
svg {
fill: $blue-normal;
fill: $blue-400;
}
}
......
......@@ -33,7 +33,7 @@
}
&.status-box-open {
background-color: $green-light;
background-color: $green-500;
}
&.status-box-expired {
......
......@@ -76,28 +76,28 @@ body {
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning {
transition: background-color 0.15s, border-color 0.15s;
background-color: lighten($gl-warning, 4%);
border-color: lighten($gl-warning, 4%);
background-color: $orange-500;
border-color: $orange-500;
}
.alert-warning + .alert-warning {
background-color: $gl-warning;
border-color: $gl-warning;
background-color: $orange-600;
border-color: $orange-600;
}
.alert-warning + .alert-warning + .alert-warning {
background-color: darken($gl-warning, 4%);
border-color: darken($gl-warning, 4%);
background-color: $orange-700;
border-color: $orange-700;
}
.alert-warning + .alert-warning + .alert-warning + .alert-warning {
background-color: darken($gl-warning, 8%);
border-color: darken($gl-warning, 8%);
background-color: $orange-800;
border-color: $orange-800;
}
.alert-warning:only-of-type {
background-color: $gl-warning;
border-color: $gl-warning;
background-color: $orange-500;
border-color: $orange-500;
}
}
......
......@@ -122,7 +122,7 @@ ul.content-list {
}
.member-group-link {
color: $blue-normal;
color: $blue-600;
}
.description {
......
......@@ -31,6 +31,7 @@ $border-radius-small: 3px !default;
//
$text-color: $gl-text-color;
$link-color: $gl-link-color;
$link-hover-color: $gl-link-hover-color;
//== Typography
......@@ -73,7 +74,7 @@ $pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
$pagination-active-color: $blue-dark;
$pagination-active-color: $blue-600;
$pagination-active-bg: $white-light;
$pagination-active-border: $border-color;
......@@ -135,8 +136,8 @@ $well-border: #eee;
//
//##
$code-color: #c7254e;
$code-bg: #f9f2f4;
$code-color: $red-600;
$code-bg: lighten($red-50, 2%);
$kbd-color: $white-light;
$kbd-bg: #333;
......
......@@ -536,9 +536,9 @@
right: -3px;
top: -3px;
width: 17px;
background-color: $blue-light;
background-color: $blue-500;
color: $white-light;
border: 1px solid $border-blue-light;
border: 1px solid $blue-600;
font-size: 9px;
line-height: 15px;
border-radius: 50%;
......
......@@ -239,18 +239,18 @@
margin: 1px;
&-finished {
background-color: lighten($green-light, 25%);
border-color: $green-light;
background-color: $green-100;
border-color: $green-400;
}
&-deploying {
background-color: lighten($green-light, 40%);
border-color: $green-light;
background-color: $green-50;
border-color: $green-400;
}
&-failed {
background-color: lighten($red-light, 20%);
border-color: $red-normal;
background-color: $red-200;
border-color: $red-500;
}
&-ready {
......
......@@ -530,12 +530,12 @@
&.over_estimate {
.meter-fill {
background: $red-light;
background: $red-500;
}
.time-remaining,
.compare-value.spent {
color: $red-light;
color: $red-500;
}
}
}
......
......@@ -69,21 +69,17 @@ ul.related-merge-requests > li {
height: 20px;
border-radius: 3px;
line-height: 18px;
border: 1px solid;
&.merged {
border-color: darken($blue-normal, 10%);
background: $blue-normal;
background: $blue-500;
}
&.closed {
border-color: darken($red-normal, 10%);
background: $red-normal;
background: $red-500;
}
&.open {
border: 1px solid darken($green-normal, 10%);
background: $green-normal;
background: $green-500;
}
}
......@@ -143,7 +139,7 @@ ul.related-merge-requests > li {
}
.export-checkmark {
color: $green-light;
color: $green-400;
}
}
......
......@@ -85,11 +85,11 @@
}
.username .validation-success {
color: $green-normal;
color: $green-600;
}
.username .validation-error {
color: $red-normal;
color: $red-500;
}
}
}
......
......@@ -158,7 +158,7 @@
> p {
float: left;
margin-bottom: 10px;
color: $orange-normal;
color: $orange-600;
@media (min-width: $screen-sm-min) {
padding-left: 55px;
......
......@@ -255,7 +255,7 @@ $colors: (
&.saved {
.editor {
border-top: solid 2px $border-green-extra-light;
border-top: solid 2px $green-200;
}
}
......
......@@ -535,7 +535,7 @@
}
.fa-info-circle {
color: $orange-normal;
color: $orange-500;
padding-right: 5px;
}
}
......
......@@ -63,7 +63,7 @@
}
.remaining-days {
color: $orange-light;
color: $orange-600;
}
.milestone-stats-and-buttons {
......
......@@ -462,17 +462,18 @@ ul.notes {
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $gl-link-color;
color: $blue-500;
margin-left: -55px;
position: absolute;
z-index: 10;
width: 23px;
height: 23px;
border: 1px solid $border-color;
border: 1px solid $blue-500;
transition: transform .1s ease-in-out;
&:hover {
background: $gl-info;
background: $blue-500;
border-color: $blue-600;
color: $white-light;
transform: scale(1.15);
}
......
......@@ -671,51 +671,71 @@
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
border-color: $gl-success;
color: $gl-success;
border-color: $green-500;
color: $green-500;
&:hover,
&:focus,
&:active {
background-color: rgba($gl-success, 0.1);
border-color: $gl-success;
background-color: $green-50;
border-color: $green-600;
color: $green-600;
svg {
fill: $green-600;
}
}
}
&.ci-status-icon-failed {
border-color: $gl-danger;
color: $gl-danger;
border-color: $red-500;
color: $red-500;
&:hover,
&:focus,
&:active {
background-color: rgba($gl-danger, 0.1);
border-color: $gl-danger;
background-color: $red-50;
border-color: $red-600;
color: $red-600;
svg {
fill: $red-600;
}
}
}
&.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings {
border-color: $gl-warning;
color: $gl-warning;
border-color: $orange-500;
color: $orange-500;
&:hover,
&:focus,
&:active {
background-color: rgba($gl-warning, 0.1);
border-color: $gl-warning;
background-color: $orange-50;
border-color: $orange-600;
color: $orange-600;
svg {
fill: $orange-600;
}
}
}
&.ci-status-icon-running {
border-color: $blue-normal;
color: $blue-normal;
border-color: $blue-400;
color: $blue-400;
&:hover,
&:focus,
&:active {
background-color: rgba($blue-normal, 0.1);
border-color: $blue-normal;
background-color: $blue-50;
border-color: $blue-600;
color: $blue-600;
svg {
fill: $blue-600;
}
}
}
......
......@@ -74,7 +74,6 @@
display: inline;
a {
color: $blue-dark;
text-decoration: none;
}
}
......
......@@ -28,6 +28,6 @@ table .sherlock-code {
}
.sherlock-line-samples-table .slow {
color: $red-light;
color: $red-500;
font-weight: bold;
}
......@@ -21,42 +21,41 @@
&.ci-failed,
&.ci-failed_with_warnings {
color: $gl-danger;
border-color: $gl-danger;
color: $red-500;
border-color: $red-500;
&:not(span):hover {
background-color: rgba($gl-danger, .07);
background-color: $red-50;
color: $red-600;
border-color: $red-600;
svg {
fill: $red-600;
}
}
svg {
fill: $gl-danger;
fill: $red-500;
}
}
&.ci-success,
&.ci-success_with_warnings {
color: $gl-success;
border-color: $gl-success;
color: $green-600;
border-color: $green-500;
&:not(span):hover {
background-color: rgba($gl-success, .07);
}
svg {
fill: $gl-success;
}
}
&.ci-info {
color: $gl-info;
border-color: $gl-info;
background-color: $green-50;
color: $green-700;
border-color: $green-600;
&:not(span):hover {
background-color: rgba($gl-info, .07);
svg {
fill: $green-600;
}
}
svg {
fill: $gl-info;
fill: $green-500;
}
}
......@@ -75,28 +74,41 @@
}
&.ci-pending {
color: $gl-warning;
border-color: $gl-warning;
color: $orange-600;
border-color: $orange-500;
&:not(span):hover {
background-color: rgba($gl-warning, .07);
background-color: $orange-50;
color: $orange-700;
border-color: $orange-600;
svg {
fill: $orange-600;
}
}
svg {
fill: $gl-warning;
fill: $orange-500;
}
}
&.ci-info,
&.ci-running {
color: $blue-normal;
border-color: $blue-normal;
color: $blue-500;
border-color: $blue-500;
&:not(span):hover {
background-color: rgba($blue-normal, .07);
background-color: $blue-50;
color: $blue-600;
border-color: $blue-600;
svg {
fill: $blue-600;
}
}
svg {
fill: $blue-normal;
fill: $blue-500;
}
}
......
......@@ -40,7 +40,7 @@ module IssuableCollections
end
def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
issues_finder.execute.preload(:project, :author, :labels, :milestone, project: :namespace)
end
def merge_requests_collection
......
......@@ -4,7 +4,7 @@ class Groups::AnalyticsController < Groups::ApplicationController
layout 'group'
def show
@users = @group.users
@users = @group.users.select(:id, :name, :username)
@start_date = params[:start_date] || Date.today - 1.week
@events = Event.contributions
.where("created_at > ?", @start_date)
......@@ -12,16 +12,21 @@ class Groups::AnalyticsController < Groups::ApplicationController
@stats = {}
@stats[:merge_requests] = @users.map do |user|
@events.merge_requests.created.where(author_id: user).count
end
@stats[:total_events] = count_by_user(@events.totals_by_author)
@stats[:push] = count_by_user(@events.code_push.totals_by_author)
@stats[:merge_requests_created] = count_by_user(@events.merge_requests.created.totals_by_author)
@stats[:merge_requests_merged] = count_by_user(@events.merge_requests.merged.totals_by_author)
@stats[:issues_created] = count_by_user(@events.issues.created.totals_by_author)
@stats[:issues_closed] = count_by_user(@events.issues.closed.totals_by_author)
end
private
@stats[:issues] = @users.map do |user|
@events.issues.closed.where(author_id: user).count
end
def count_by_user(data)
user_ids.map { |id| data.fetch(id, 0) }
end
@stats[:push] = @users.map do |user|
@events.code_push.where(author_id: user).count
end
def user_ids
@user_ids ||= @users.map(&:id)
end
end
......@@ -82,7 +82,7 @@ module Projects
labels: true,
only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
},
user: current_user
......
......@@ -65,7 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController
def new
params[:issue] ||= ActionController::Parameters.new(
assignee_id: ""
assignee_ids: ""
)
build_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
......
......@@ -26,17 +26,31 @@ class IssuesFinder < IssuableFinder
IssuesFinder.not_restricted_by_confidentiality(current_user)
end
def by_assignee(items)
if assignee
items = items.where("issue_assignees.user_id = ?", assignee.id)
elsif no_assignee?
items = items.where("issue_assignees.user_id is NULL")
elsif assignee_id? || assignee_username? # assignee not found
items = items.none
end
items
end
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
issues = Issue.with_assignees
return issues.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return Issue.all if user.admin_or_auditor?
return issues.all if user.admin_or_auditor?
Issue.where('
issues.where('
issues.confidential IS NULL
OR issues.confidential IS FALSE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR issues.assignee_id = :user_id
OR issue_assignees.user_id = :user_id
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
......
......@@ -11,10 +11,12 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
@previous_assignees = []
@previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
......
......@@ -25,7 +25,6 @@ module Issuable
cache_markdown_field :description
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :milestone
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do
......@@ -94,7 +93,6 @@ module Issuable
attr_mentionable :description
participant :author
participant :assignee
participant :notes_with_associations
strip_attributes :title
......@@ -285,7 +283,11 @@ module Issuable
# DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
hook_data[:assignee] = assignee.hook_attrs if assignee
if self.is_a?(Issue)
hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any?
else
hook_data[:assignee] = assignee.hook_attrs if assignee
end
hook_data
end
......@@ -319,14 +321,6 @@ module Issuable
@human_class_name ||= self.class.name.titleize.downcase
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
}
end
def notes_with_associations
# If A has_many Bs, and B has_many Cs, and you do
# `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
......@@ -358,11 +352,6 @@ module Issuable
false
end
def assignee_or_author?(user)
# We're comparing IDs here so we don't need to load any associations.
author_id == user.id || assignee_id == user.id
end
def record_metrics
metrics = self.metrics || create_metrics
metrics.record!
......
......@@ -46,6 +46,7 @@ class Event < ActiveRecord::Base
scope :created, -> { where(action: CREATED) }
scope :closed, -> { where(action: CLOSED) }
scope :merged, -> { where(action: MERGED) }
scope :totals_by_author, -> { group(:author_id).count }
class << self
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
......
......@@ -29,11 +29,14 @@ class Issue < ActiveRecord::Base
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
has_and_belongs_to_many :assignees, class_name: "User", join_table: :issue_assignees
validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
scope :cared, ->(user) { with_assignees.where("issue_assignees.user_id IN(?)", user.id) }
scope :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :with_assignees, -> { joins('LEFT JOIN issue_assignees ON issue_id = issues.id') }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
......@@ -51,6 +54,8 @@ class Issue < ActiveRecord::Base
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
participant :assignees
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
......@@ -127,6 +132,28 @@ class Issue < ActiveRecord::Base
"id DESC")
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee_list
}
end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
def assignee_list
assignees.map(&:name).to_sentence
end
# TODO: This method will help us to find some silent failures.
# We should remove it before merging to master
def assignee_id
raise "assignee_id is deprecated"
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
......@@ -261,7 +288,7 @@ class Issue < ActiveRecord::Base
true
elsif confidential?
author == user ||
assignee == user ||
assignees.include?(user) ||
project.team.member?(user, Gitlab::Access::REPORTER)
else
project.public? ||
......
......@@ -2,7 +2,7 @@ class License < ActiveRecord::Base
include ActionView::Helpers::NumberHelper
validate :valid_license
validate :active_user_count, if: :new_record?, unless: :validate_with_trueup?
validate :check_users_limit, if: :new_record?, unless: :validate_with_trueup?
validate :check_trueup, unless: :persisted?, if: :validate_with_trueup?
validate :not_expired, unless: :persisted?
......@@ -99,6 +99,10 @@ class License < ActiveRecord::Base
restricted_attr(:previous_user_count)
end
def current_active_users_count
@current_active_users_count ||= User.active.count
end
def validate_with_trueup?
[restricted_attr(:trueup_quantity),
restricted_attr(:trueup_from),
......@@ -127,37 +131,41 @@ class License < ActiveRecord::Base
self.errors.add(:base, "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc.")
end
def historical_max(from, to)
def historical_max(from = nil, to = nil)
from ||= starts_at - 1.year
to ||= starts_at
HistoricalData.during(from..to).maximum(:active_user_count) || 0
end
def active_user_count
def check_users_limit
return unless restricted_user_count
historical_user_count = historical_max((starts_at - 1.year), starts_at)
overage = historical_user_count - restricted_user_count
return if historical_user_count <= restricted_user_count
if previous_user_count && (historical_max <= previous_user_count)
return if restricted_user_count >= current_active_users_count
else
return if restricted_user_count >= historical_max
end
add_limit_error(user_count: historical_user_count, restricted_user_count: restricted_user_count, overage: overage)
overage = historical_max - restricted_user_count
add_limit_error(user_count: historical_max, restricted_user_count: restricted_user_count, overage: overage)
end
def check_trueup
trueup_qty = restrictions[:trueup_quantity]
trueup_from = Date.parse(restrictions[:trueup_from]) rescue (starts_at - 1.year)
trueup_to = Date.parse(restrictions[:trueup_to]) rescue starts_at
active_user_count = User.active.count
max_historical = historical_max(trueup_from, trueup_to)
overage = active_user_count - restricted_user_count
overage = current_active_users_count - restricted_user_count
expected_trueup_qty = if previous_user_count
max_historical - previous_user_count
else
max_historical - active_user_count
max_historical - current_active_users_count
end
if trueup_qty >= expected_trueup_qty
if restricted_user_count < active_user_count
add_limit_error(trueup: true, user_count: active_user_count, restricted_user_count: restricted_user_count, overage: overage)
if restricted_user_count < current_active_users_count
add_limit_error(trueup: true, user_count: current_active_users_count, restricted_user_count: restricted_user_count, overage: overage)
end
else
message = "You have applied a True-up for #{trueup_qty} #{"user".pluralize(trueup_qty)} "
......
......@@ -22,6 +22,8 @@ class MergeRequest < ActiveRecord::Base
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
belongs_to :assignee, class_name: "User"
serialize :merge_params, Hash
after_create :ensure_merge_request_diff, unless: :importing?
......@@ -121,6 +123,7 @@ class MergeRequest < ActiveRecord::Base
scope :references_project, -> { references(:target_project) }
participant :approvers_left
participant :assignee
after_save :keep_around_commit
......@@ -182,6 +185,23 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
}
end
# This method is needed for compatibility with issues
def assignees
[assignee]
end
def assignee_or_author?(user)
author_id == user.id || assignee_id == user.id
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
......
class IssuableEntity < Grape::Entity
expose :id
expose :iid
expose :assignee_id
expose :assignees, using: UserEntity
expose :author_id
expose :description
expose :lock_version
......@@ -18,4 +16,4 @@ class IssuableEntity < Grape::Entity
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
end
end
\ No newline at end of file
class IssueEntity < IssuableEntity
expose :branch_name
expose :confidential
expose :assignee_ids
expose :due_date
expose :moved_to_id
expose :project_id
......
class MergeRequestEntity < IssuableEntity
expose :approvals_before_merge
expose :assignee_id
expose :in_progress_merge_commit_sha
expose :locked_at
expose :merge_commit_sha
......
class IssuableBaseService < BaseService
private
def create_assignee_note(issuable)
SystemNoteService.change_assignee(
issuable, issuable.project, current_user, issuable.assignee)
end
def create_milestone_note(issuable)
SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone)
......@@ -53,7 +48,13 @@ class IssuableBaseService < BaseService
params.delete(:add_label_ids)
params.delete(:remove_label_ids)
params.delete(:label_ids)
params.delete(:assignee_id)
if issuable.is_a?(Issue)
params.delete(:assignee_ids)
else
params.delete(:assignee_id)
end
params.delete(:due_date)
end
......@@ -77,7 +78,7 @@ class IssuableBaseService < BaseService
def assignee_can_read?(issuable, assignee_id)
new_assignee = User.find_by_id(assignee_id)
return false unless new_assignee.present?
return false unless new_assignee
ability_name = :"read_#{issuable.to_ability_name}"
resource = issuable.persisted? ? issuable : project
......@@ -207,6 +208,7 @@ class IssuableBaseService < BaseService
filter_params(issuable)
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
old_assignees = issuable.assignees.to_a
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
......@@ -222,7 +224,13 @@ class IssuableBaseService < BaseService
handle_common_system_notes(issuable, old_labels: old_labels)
end
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
handle_changes(
issuable,
old_labels: old_labels,
old_mentioned_users: old_mentioned_users,
old_assignees: old_assignees
)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
......
......@@ -9,11 +9,28 @@ module Issues
private
def create_assignee_note(issue)
SystemNoteService.change_issue_assignees(
issue, issue.project, current_user, issue.assignees)
end
def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope)
end
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
assignee_ids = params[:assignee_ids].split(',').map(&:strip)
if assignee_ids == [ IssuableFinder::NONE ]
params[:assignee_ids] = ""
else
params.delete(:assignee_ids) unless assignee_ids.all?{ |assignee_id| assignee_can_read?(issuable, assignee_id)}
end
end
end
end
......@@ -21,7 +21,7 @@ module Issues
def csv_builder
@csv_builder ||=
CsvBuilder.new(@issues.includes(:author, :assignee), header_to_value_hash)
CsvBuilder.new(@issues.includes(:author), header_to_value_hash)
end
private
......@@ -35,8 +35,8 @@ module Issues
'Description' => 'description',
'Author' => 'author_name',
'Author Username' => -> (issue) { issue.author&.username },
'Assignee' => 'assignee_name',
'Assignee Username' => -> (issue) { issue.assignee&.username },
'Assignee' => -> (issue) { issue.assignees.pluck(:name).join(', ') },
'Assignee Username' => -> (issue) { issue.assignees.pluck(:username).join(', ') },
'Confidential' => -> (issue) { issue.confidential? ? 'Yes' : 'No' },
'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) },
'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) },
......
......@@ -12,7 +12,11 @@ module Issues
spam_check(issue, current_user)
end
def handle_changes(issue, old_labels: [], old_mentioned_users: [])
def handle_changes(issue, options)
old_labels = options[:old_labels] || []
old_mentioned_users = options[:old_mentioned_users] || []
old_assignees = options[:old_assignees] || []
if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
end
......@@ -26,9 +30,9 @@ module Issues
create_milestone_note(issue)
end
if issue.previous_changes.include?('assignee_id')
if issue.previous_changes.include?('assignee_ids')
create_assignee_note(issue)
notification_service.reassigned_issue(issue, current_user)
notification_service.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user)
end
......
......@@ -4,7 +4,7 @@ module MergeRequests
@assignable_issues ||= begin
if current_user == merge_request.author
closes_issues.select do |issue|
!issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue)
!issue.is_a?(ExternalIssue) && !issue.assignees.any? && can?(current_user, :admin_issue, issue)
end
else
[]
......@@ -14,7 +14,7 @@ module MergeRequests
def execute
assignable_issues.each do |issue|
Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue)
Issues::UpdateService.new(issue.project, current_user, assignee_ids: current_user.id.to_s).execute(issue)
end
{
......
......@@ -38,6 +38,11 @@ module MergeRequests
private
def create_assignee_note(merge_request)
SystemNoteService.change_assignee(
merge_request, merge_request.project, current_user, merge_request.assignee)
end
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
def merge_requests_for(source_branch, mr_states: [:opened])
MergeRequest
......
......@@ -31,7 +31,10 @@ module MergeRequests
merge_request
end
def handle_changes(merge_request, old_labels: [], old_mentioned_users: [])
def handle_changes(merge_request, options)
old_labels = options[:old_labels] || []
old_mentioned_users = options[:old_mentioned_users] || []
if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
......
......@@ -3,11 +3,12 @@
#
class NotificationRecipientService
attr_reader :project
def initialize(project)
@project = project
end
# TODO: refactor this: previous_assignee argument can be a user object or an array which is not really nice
def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target)
......@@ -23,9 +24,13 @@ class NotificationRecipientService
# Re-assign is considered as a mention of the new assignee so we add the
# new assignee to the list of recipients after we rejected users with
# the "on mention" notification level
if [:reassign_merge_request, :reassign_issue].include?(custom_action)
case custom_action
when :reassign_merge_request
recipients << previous_assignee if previous_assignee
recipients << target.assignee
when :reassign_issue
recipients.concat(previous_assignee) if previous_assignee.any?
recipients.concat(target.assignees)
end
recipients = reject_muted_users(recipients)
......
......@@ -66,8 +66,23 @@ class NotificationService
# * issue new assignee if their notification level is not Disabled
# * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user)
reassign_resource_email(issue, issue.project, current_user, :reassigned_issue_email)
def reassigned_issue(issue, current_user, previous_assignees = [])
recipients = NotificationRecipientService.new(issue.project).build_recipients(
issue,
current_user,
action: "reassign",
previous_assignee: previous_assignees
)
recipients.each do |recipient|
mailer.send(
:reassigned_issue_email,
recipient.id,
issue.id,
previous_assignees.map(&:id),
current_user.id
).deliver_later
end
end
# When we add labels to an issue we should send an email to:
......@@ -407,10 +422,10 @@ class NotificationService
end
def previous_record(object, attribute)
if object && attribute
if object.previous_changes.include?(attribute)
object.previous_changes[attribute].first
end
return unless object && attribute
if object.previous_changes.include?(attribute)
object.previous_changes[attribute].first
end
end
end
......@@ -49,6 +49,42 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the assignees of an Issue is changed or removed
#
# issue - Issue object
# project - Project owning noteable
# author - User performing the change
# assignees - User being assigned, or nil
#
# Example Note text:
#
# "removed all assignees"
#
# "assigned to @user1 additionally to @user2"
#
# "assigned to @user1, @user2 and @user3 and unassigned from @user4 and @user5"
#
# "assigned to @user1 and @user2"
#
# Returns the created Note object
def change_issue_assignees(issue, project, author, assignees)
# TODO: basic implementation, should be improved before merging the MR
body =
if issue.assignees.any? && assignees.any?
unassigned_users = issue.assignees - assignees
added_users = assignees - issue.assignees
"assigned to #{added_users.map(&:to_reference).to_sentence} and unassigned #{unassigned_users.map(&:to_reference).to_sentence}"
elsif issue.assignees.any?
"removed all assignees"
elsif assignees.any?
"assigned to #{assignees.map(&:to_reference).to_sentence}"
end
create_note(noteable: issue, project: project, author: author, note: body)
end
# Called when one or more labels on a Noteable are added and/or removed
#
# noteable - Noteable object
......
......@@ -264,13 +264,13 @@ class TodoService
end
def create_assignment_todo(issuable, author)
if issuable.assignee
if issuable.assignees.any?
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
create_todos(issuable.assignee, attributes)
create_todos(issuable.assignees, attributes)
end
end
def create_mention_todos(project, target, author, note = nil)
def create_mention_todos(project, target, author, note = nil)
# Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
......
......@@ -60,7 +60,7 @@
.col-md-8
%div
%p.light Merge requests created per group member
%canvas#merge_requests{ height: 250 }
%canvas#merge_requests_created{ height: 250 }
%h3 Issues
......@@ -77,7 +77,7 @@
.col-md-8
%div
%p.light Issues closed per group member
%canvas#issues{ height: 250 }
%canvas#issues_closed{ height: 250 }
.gray-content-block
.oneline
......@@ -109,21 +109,21 @@
Total Contributions
= icon('sort')
%tbody
- @users.each do |user|
- @users.each_with_index do |user, index|
%tr
%td
%strong
= link_to user.name, user
%td= @events.code_push.where(author_id: user).count
%td= @events.issues.created.where(author_id: user).count
%td= @events.issues.closed.where(author_id: user).count
%td= @events.merge_requests.created.where(author_id: user).count
%td= @events.merge_requests.merged.where(author_id: user).count
%td= @events.where(author_id: user).count
%td= @stats[:push][index]
%td= @stats[:issues_created][index]
%td= @stats[:issues_closed][index]
%td= @stats[:merge_requests_created][index]
%td= @stats[:merge_requests_merged][index]
%td= @stats[:total_events][index]
- [:push, :issues, :merge_requests].each do |scope|
- [:push, :issues_closed, :merge_requests_created].each do |scope|
:javascript
var data = {
labels : #{@users.map(&:name).to_json},
......
......@@ -23,10 +23,12 @@ xml.entry do
end
end
if issue.assignee
xml.assignee do
xml.name issue.assignee.name
xml.email issue.assignee_public_email
if issue.assignees.any?
xml.assignees do
issue.assignees.each do |assignee|
xml.name ssignee.name
xml.email assignee.assignee_public_email
end
end
end
end
Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
Reassigned Issue <%= @issue.iid %>
<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
......@@ -4,6 +4,6 @@
- if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present?
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_name}
Assignee: #{@issue.assignee_list}
......@@ -2,6 +2,6 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_name %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
......@@ -7,6 +7,6 @@
- if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present?
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_name}
Assignee: #{@issue.assignee_list}
......@@ -2,6 +2,6 @@ You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_name %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
= render 'reassigned_issuable_email', issuable: @issue
%p
Assignee changed
- if @previous_assignees.any?
from
%strong= @previous_assignees.map(&:name).to_sentence
to
- if @issue.assignees.any?
%strong= @issue.assignee_list
- else
%strong Unassigned
<%= render 'reassigned_issuable_email', issuable: @merge_request %>
Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
to <%= "#{@merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'}" %>
......@@ -13,7 +13,7 @@
%li
CLOSED
- if issue.assignee
- if issue.assignees.any?
%li
= render 'shared/issuable/assignees', project: @project, issue: issue
......@@ -48,4 +48,4 @@
= issue.weight
.pull-right.issue-updated-at
%span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
%span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
\ No newline at end of file
......@@ -22,36 +22,8 @@
= icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
%span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= issuable.assignee.to_reference
- else
%span.assign-yourself.no-value
No assignee
- if can_edit_issuable
\-
%a.js-assign-yourself{ href: '#' }
assign yourself
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
= render "shared/issuable/form/#{issuable.model_name.singular}_assignee", can_edit_issuable: can_edit_issuable, issuable: issuable, f: f
.block.milestone
.sidebar-collapsed-icon
......@@ -162,11 +134,11 @@
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed
.value.hide-collapsed
- if issuable.weight
= issuable.weight
%strong= issuable.weight
- else
.light None
%span.no-value None
.selectbox.hide-collapsed
= weight_dropdown_tag(issuable, title: 'Change weight', data: { field_name: 'weight', issue_update: "#{issuable_json_path(issuable)}", ability_name: "#{issuable.to_ability_name}" }) do
%ul
......
- issue = issuable
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee_list) }
- if issue.assignees.any?
- issue.assignees.each do |assignee|
= link_to_member(@project, assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
- if issue.assignees.any?
- issue.assignees.each do |assignee|
= link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
%span.username
= assignee.to_reference
- else
%span.assign-yourself.no-value
No assignee
- if can_edit_issuable
\-
%a.js-assign-yourself{ href: '#' }
assign yourself
.selectbox.hide-collapsed
= f.hidden_field 'assignee_ids', value: issuable.assignee_ids, id: 'issue_assignee_ids'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
- merge_request = issuable
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) }
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
- unless merge_request.can_be_merged_by?(merge_request.assignee)
%span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= merge_request.assignee.to_reference
- else
%span.assign-yourself.no-value
No assignee
- if can_edit_issuable
\-
%a.js-assign-yourself{ href: '#' }
assign yourself
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: merge_request.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project&.id, author_id: merge_request.author_id, field_name: 'merge_request[assignee_id]', issue_update: issuable_json_path(merge_request), ability_name: 'merge_request', null_user: true } })
......@@ -10,13 +10,24 @@
.row
%div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
.form-group.issue-assignee
= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
= form.hidden_field :assignee_id
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
- if issuable.is_a?(Issue)
= form.label :assignee_ids, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
= form.hidden_field :assignee_ids
= dropdown_tag(user_dropdown_label(issuable.assignee_ids, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_ids, field_name: "#{issuable.class.model_name.param_key}[assignee_ids]", default_label: "Assignee"} })
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_ids.split(', ').include?(current_user.id)}"
- else
= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
= form.hidden_field :assignee_id
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
.form-group.issue-milestone
= form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
......
---
title: Update color palette to a more harmonious and consistent one.
merge_request: 1500
author:
---
title: Remove N+1 queries for Groups::AnalyticsController
merge_request:
author:
---
title: Update color palette to a more harmonious and consistent one.
merge_request: 10154
author:
---
title: Shows loading icon in issue boards modal when changing filters
merge_request:
author:
......@@ -8,7 +8,7 @@ Gitlab::Seeder.quiet do
description: FFaker::Lorem.sentence,
state: ['opened', 'closed'].sample,
milestone: project.milestones.sample,
assignee: project.team.users.sample
assignees: [project.team.users.sample]
}
Issues::CreateService.new(project, project.team.users.sample, issue_params).execute
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateIssueAssigneesTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
create_table :issue_assignees do |t|
t.references :user, foreign_key: { on_delete: :cascade }, index: true, null: false
t.references :issue, foreign_key: { on_delete: :cascade }, null: false
end
add_index :issue_assignees, [:issue_id, :user_id], unique: true
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MigrateAssignees < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def up
execute <<-EOF
INSERT INTO issue_assignees(issue_id, user_id)
SELECT id, assignee_id FROM issues WHERE assignee_id IS NOT NULL
EOF
end
def down
execute <<-EOF
DELETE FROM issue_assignees
EOF
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170317203554) do
ActiveRecord::Schema.define(version: 20170320173259) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -495,6 +495,14 @@ ActiveRecord::Schema.define(version: 20170317203554) do
add_index "index_statuses", ["project_id"], name: "index_index_statuses_on_project_id", unique: true, using: :btree
create_table "issue_assignees", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_id", null: false
end
add_index "issue_assignees", ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree
add_index "issue_assignees", ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
create_table "issue_metrics", force: :cascade do |t|
t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at"
......@@ -1491,6 +1499,8 @@ ActiveRecord::Schema.define(version: 20170317203554) do
add_foreign_key "boards", "projects"
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", on_delete: :cascade
add_foreign_key "issue_assignees", "users", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
......@@ -28,7 +28,7 @@ module Banzai
nodes,
Issue.all.includes(
:author,
:assignee,
:assignees,
{
# These associations are primarily used for checking permissions.
# Eager loading these ensures we don't end up running dozens of
......
......@@ -16,7 +16,7 @@ describe Dashboard::TodosController do
describe 'GET #index' do
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
let!(:issues) { create_list(:issue, 2, project: project, assignees: [user]) }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
......
require 'spec_helper'
describe Groups::AnalyticsController do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:push_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
def create_event(author, project, target, action)
Event.create!(
project: project,
action: action,
target: target,
author: author,
created_at: Time.now)
end
def create_push_event(author, project)
event = create_event(author, project, nil, Event::PUSHED)
event.data = push_data
event.save
end
before do
group.add_owner(user)
group.add_user(user2, GroupMember::DEVELOPER)
group.add_user(user3, GroupMember::MASTER)
sign_in(user)
create_event(user, project, issue, Event::CLOSED)
create_event(user2, project, issue, Event::CLOSED)
create_event(user2, project, merge_request, Event::CREATED)
create_event(user3, project, merge_request, Event::CREATED)
create_push_event(user, project)
create_push_event(user3, project)
end
it 'sets instance variables properly' do
get :show, group_id: group.path
expect(controller.instance_variable_get(:@users)).to match_array([user, user2, user3])
expect(controller.instance_variable_get(:@events).length).to eq(6)
stats = controller.instance_variable_get(:@stats)
expect(stats[:total_events]).to eq([2, 2, 2])
expect(stats[:merge_requests_merged]).to eq([0, 0, 0])
expect(stats[:merge_requests_created]).to eq([1, 1, 0])
expect(stats[:issues_closed]).to eq([0, 1, 1])
expect(stats[:push]).to eq([1, 0, 1])
end
describe 'with views' do
render_views
it 'avoids a N+1 query in #show' do
control_count = ActiveRecord::QueryRecorder.new { get :show, group_id: group.path }.count
# Clear out controller state to force a refresh of the group
controller.instance_variable_set(:@group, nil)
user4 = create(:user)
group.add_user(user4, GroupMember::DEVELOPER)
expect { get :show, group_id: group.path }.not_to exceed_query_limit(control_count)
end
end
end
......@@ -34,7 +34,7 @@ describe Projects::Boards::IssuesController do
issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
create(:labeled_issue, project: project, labels: [development], assignees: [johndoe])
issue.subscribe(johndoe, project)
list_issues user: user, board: board, list: list2
......
......@@ -336,7 +336,7 @@ describe Projects::IssuesController do
let(:admin) { create(:admin) }
let!(:issue) { create(:issue, project: project) }
let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignees: [assignee]) }
describe 'GET #index' do
it 'does not list confidential issues for guests' do
......
......@@ -1329,7 +1329,7 @@ describe Projects::MergeRequestsController do
end
it 'correctly pluralizes flash message on success' do
issue2.update!(assignee: user)
issue2.update!(assignees: [user])
post_assign_issues
......
......@@ -32,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do
end
context "issue with basic fields" do
let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') }
let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
it "renders issue fields" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
......@@ -51,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do
context "issue with label and milestone" do
let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
let!(:label1) { create(:label, project: project1, title: 'label1') }
let!(:issue1) { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) }
let!(:issue1) { create(:issue, author: user, assignees: [assignee], project: project1, milestone: milestone1) }
before do
issue1.labels << label1
......
......@@ -6,7 +6,7 @@ describe 'Issues Feed', feature: true do
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:group) { create(:group) }
let!(:project) { create(:project) }
let!(:issue) { create(:issue, author: user, assignee: assignee, project: project) }
let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) }
before do
project.team << [user, :developer]
......
......@@ -72,7 +72,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) }
let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) }
let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
......
......@@ -98,7 +98,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
end
context 'assignee' do
let!(:issue) { create(:issue, project: project, assignee: user2) }
let!(:issue) { create(:issue, project: project, assignees: [user2]) }
before do
project.team << [user2, :developer]
......
......@@ -11,7 +11,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) }
let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], milestone: milestone, labels: [development], relative_position: 2) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
......
......@@ -7,7 +7,7 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
issue.update(assignee: user)
issue.assignees = [user]
merge_request.update(assignee: user)
login_as(user)
end
......@@ -17,7 +17,7 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do
expect_counters('issues', '1')
issue.update(assignee: nil)
issue.assignees = []
visit issues_dashboard_path
expect_counters('issues', '1')
......
......@@ -11,7 +11,7 @@ RSpec.describe 'Dashboard Issues', feature: true do
let!(:authored_issue) { create :issue, author: current_user, project: project }
let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
let!(:assigned_issue) { create :issue, assignee: current_user, project: project }
let!(:assigned_issue) { create :issue, assignees: [current_user], project: project }
let!(:other_issue) { create :issue, project: project }
before do
......
......@@ -10,8 +10,8 @@ describe "Dashboard Issues filtering", feature: true, js: true do
project.team << [user, :master]
login_as(user)
create(:issue, project: project, author: user, assignee: user)
create(:issue, project: project, author: user, assignee: user, milestone: milestone)
create(:issue, project: project, author: user, assignees: [user])
create(:issue, project: project, author: user, assignees: [user], milestone: milestone)
visit_issues
end
......
......@@ -52,11 +52,11 @@ describe "GitLab Flavored Markdown", feature: true do
before do
@other_issue = create(:issue,
author: @user,
assignee: @user,
assignees: [@user],
project: project)
@issue = create(:issue,
author: @user,
assignee: @user,
assignees: [@user],
project: project,
title: "fix #{@other_issue.to_reference}",
description: "ask #{fred.to_reference} for details")
......
......@@ -7,7 +7,7 @@ describe 'Awards Emoji', feature: true do
let!(:user) { create(:user) }
let(:issue) do
create(:issue,
assignee: @user,
assignees: [@user],
project: project)
end
......
......@@ -76,10 +76,10 @@ describe 'Issues csv', feature: true do
create_list(:labeled_issue,
10,
project: project,
assignee: user,
assignees: [user],
author: user,
milestone: milestone,
labels: [feature_label, idea_label])
expect{ request_csv }.not_to exceed_query_limit(control_count + 5)
expect{ request_csv }.not_to exceed_query_limit(control_count + 23)
end
end
......@@ -50,15 +50,15 @@ describe 'Filter issues', js: true, feature: true do
create(:issue, title: "issue with 'single quotes'", project: project)
create(:issue, title: "issue with \"double quotes\"", project: project)
create(:issue, title: "issue with !@\#{$%^&*()-+", project: project)
create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user)
create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user)
create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignees: [user])
create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignees: [user])
issue = create(:issue,
title: "Bug 2",
project: project,
milestone: milestone,
author: user,
assignee: user)
assignees: [user])
issue.labels << bug_label
issue_with_caps_label = create(:issue,
......@@ -66,7 +66,7 @@ describe 'Filter issues', js: true, feature: true do
project: project,
milestone: milestone,
author: user,
assignee: user)
assignees: [user])
issue_with_caps_label.labels << caps_sensitive_label
issue_with_everything = create(:issue,
......@@ -74,7 +74,7 @@ describe 'Filter issues', js: true, feature: true do
project: project,
milestone: milestone,
author: user,
assignee: user)
assignees: [user])
issue_with_everything.labels << bug_label
issue_with_everything.labels << caps_sensitive_label
......
......@@ -31,7 +31,7 @@ describe 'Filter issues weight', js: true, feature: true do
title: 'Bug report 1',
milestone: milestone,
author: user,
assignee: user)
assignees: [user])
issue.labels << label
visit namespace_project_issues_path(project.namespace, project)
......
......@@ -9,7 +9,7 @@ describe 'New/edit issue', feature: true, js: true do
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) }
let!(:issue) { create(:issue, project: project, assignee: user, milestone: milestone) }
let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do
project.team << [user, :master]
......
......@@ -101,7 +101,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
def create_assigned
create(:issue, project: project, assignee: user)
create(:issue, project: project, assignees: [user])
end
def create_with_milestone
......
......@@ -19,7 +19,7 @@ describe 'Issues', feature: true do
let!(:issue) do
create(:issue,
author: @user,
assignee: @user,
assignees: [@user],
project: project)
end
......@@ -44,7 +44,7 @@ describe 'Issues', feature: true do
let!(:issue) do
create(:issue,
author: @user,
assignee: @user,
assignees: [@user],
project: project)
end
......@@ -139,7 +139,7 @@ describe 'Issues', feature: true do
describe 'Issue info' do
it 'excludes award_emoji from comment count' do
issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'foobar')
create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
......@@ -165,14 +165,14 @@ describe 'Issues', feature: true do
%w(foobar barbaz gitlab).each do |title|
create(:issue,
author: @user,
assignee: @user,
assignees: [@user],
project: project,
title: title)
end
@issue = Issue.find_by(title: 'foobar')
@issue.milestone = create(:milestone, project: project)
@issue.assignee = nil
@issue.assignees = []
@issue.save
end
......@@ -408,7 +408,7 @@ describe 'Issues', feature: true do
end
describe 'update labels from issue#show', js: true do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
let!(:label) { create(:label, project: project) }
before do
......@@ -426,7 +426,7 @@ describe 'Issues', feature: true do
end
describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
context 'by authorized user' do
it 'allows user to select unassigned', js: true do
......@@ -668,7 +668,7 @@ describe 'Issues', feature: true do
describe 'due date' do
context 'update due on issue#show', js: true do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
before do
visit namespace_project_issue_path(project.namespace, project, issue)
......
......@@ -33,7 +33,7 @@ feature 'Merge request issue assignment', js: true, feature: true do
end
it "doesn't display if related issues are already assigned" do
[issue1, issue2].each { |issue| issue.update!(assignee: user) }
[issue1, issue2].each { |issue| issue.update!(assignees: [user]) }
visit_merge_request
......
......@@ -5,10 +5,10 @@ describe 'Milestone show', feature: true do
let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_list(:label, 2, project: project) }
let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } }
let(:issue_params) { { project: project, assignees: [user], author: user, milestone: milestone, labels: labels } }
before do
project.add_user(user, :developer)
project.add_user(user, :developer)
login_as(user)
end
......
......@@ -14,7 +14,7 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates' do
let(:template_content) { 'this is a test "bug" template' }
let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
let(:description_addition) { ' appending to description' }
background do
......@@ -74,7 +74,7 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates, with a prior description' do
let(:prior_description) { 'test issue description' }
let(:template_content) { 'this is a test "bug" template' }
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
background do
project.repository.create_file(
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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