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 { ...@@ -15,7 +15,7 @@ export default {
this.filteredSearch = new FilteredSearchBoards(this.store); this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.removeTokens(); this.filteredSearch.removeTokens();
}, },
beforeDestroy() { destroyed() {
this.filteredSearch.cleanup(); this.filteredSearch.cleanup();
FilteredSearchContainer.container = document; FilteredSearchContainer.container = document;
this.store.path = ''; this.store.path = '';
......
...@@ -65,8 +65,15 @@ require('./empty_state'); ...@@ -65,8 +65,15 @@ require('./empty_state');
}, },
filter: { filter: {
handler() { handler() {
this.page = 1; if (this.$el.tagName) {
this.loadIssues(true); this.page = 1;
this.filterLoading = true;
this.loadIssues(true)
.then(() => {
this.filterLoading = false;
});
}
}, },
deep: true, deep: true,
}, },
...@@ -140,14 +147,14 @@ require('./empty_state'); ...@@ -140,14 +147,14 @@ require('./empty_state');
:image="blankStateImage" :image="blankStateImage"
:issue-link-base="issueLinkBase" :issue-link-base="issueLinkBase"
:root-path="rootPath" :root-path="rootPath"
v-if="!loading && showList"></modal-list> v-if="!loading && showList && !filterLoading"></modal-list>
<empty-state <empty-state
v-if="showEmptyState" v-if="showEmptyState"
:image="blankStateImage" :image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state> :new-issue-path="newIssuePath"></empty-state>
<section <section
class="add-issues-list text-center" class="add-issues-list text-center"
v-if="loading"> v-if="loading || filterLoading">
<div class="add-issues-list-loading"> <div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin"></i>
</div> </div>
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
searchTerm: '', searchTerm: '',
loading: false, loading: false,
loadingNewPage: false, loadingNewPage: false,
filterLoading: false,
page: 1, page: 1,
perPage: 50, perPage: 50,
filter: { filter: {
......
...@@ -30,9 +30,9 @@ ...@@ -30,9 +30,9 @@
$loading.fadeOut(); $loading.fadeOut();
$selectbox.hide(); $selectbox.hide();
if (data.weight != null) { if (data.weight != null) {
$value.html(data.weight); $value.html(`<strong>${data.weight}</strong>`);
} else { } else {
$value.html('None'); $value.html('<span class="no-value">None</span>');
} }
return $sidebarCollapsedValue.html(data.weight); return $sidebarCollapsedValue.html(data.weight);
}); });
......
...@@ -68,23 +68,19 @@ ...@@ -68,23 +68,19 @@
} }
@mixin btn-green { @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 { @mixin btn-blue {
@include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-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-blue-medium {
@include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light);
} }
@mixin btn-orange { @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 { @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 { @mixin btn-gray {
...@@ -145,11 +141,11 @@ ...@@ -145,11 +141,11 @@
&.btn-new, &.btn-new,
&.btn-create, &.btn-create,
&.btn-save { &.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 { &.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 @@ ...@@ -157,11 +153,8 @@
@include btn-gray; @include btn-gray;
} }
&.btn-primary {
@include btn-blue-medium;
}
&.btn-info, &.btn-info,
&.btn-primary,
&.btn-register { &.btn-register {
@include btn-blue; @include btn-blue;
} }
...@@ -171,11 +164,11 @@ ...@@ -171,11 +164,11 @@
} }
&.btn-close { &.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 { &.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, &.btn-danger,
...@@ -360,7 +353,7 @@ ...@@ -360,7 +353,7 @@
.btn-inverted { .btn-inverted {
&-secondary { &-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 @@ ...@@ -199,7 +199,7 @@
text-decoration: none; text-decoration: none;
.badge { .badge {
background-color: darken($row-hover, 5%); background-color: darken($dropdown-link-hover-bg, 5%);
} }
} }
......
...@@ -177,34 +177,34 @@ label { ...@@ -177,34 +177,34 @@ label {
} }
.gl-field-error { .gl-field-error {
color: $red-normal; color: $red-500;
} }
.gl-show-field-errors { .gl-show-field-errors {
.gl-field-success-outline { .gl-field-success-outline {
border: 1px solid $green-normal; border: 1px solid $green-600;
&:focus { &: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; border: 0 none;
} }
} }
.gl-field-error-outline { .gl-field-error-outline {
border: 1px solid $red-normal; border: 1px solid $red-500;
&:focus { &: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; border: 0 none;
} }
} }
.gl-field-success-message { .gl-field-success-message {
color: $green-normal; color: $green-600;
} }
.gl-field-error-message { .gl-field-error-message {
color: $red-normal; color: $red-500;
} }
.gl-field-hint { .gl-field-hint {
......
...@@ -265,7 +265,7 @@ header { ...@@ -265,7 +265,7 @@ header {
} }
.impersonation i { .impersonation i {
color: $red-normal; color: $red-500;
} }
} }
......
.ci-status-icon-success { .ci-status-icon-success {
color: $gl-success; color: $green-500;
svg { svg {
fill: $gl-success; fill: $green-500;
} }
} }
...@@ -17,18 +17,18 @@ ...@@ -17,18 +17,18 @@
.ci-status-icon-pending, .ci-status-icon-pending,
.ci-status-icon-failed_with_warnings, .ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings { .ci-status-icon-success_with_warnings {
color: $gl-warning; color: $orange-500;
svg { svg {
fill: $gl-warning; fill: $orange-500;
} }
} }
.ci-status-icon-running { .ci-status-icon-running {
color: $blue-normal; color: $blue-400;
svg { svg {
fill: $blue-normal; fill: $blue-400;
} }
} }
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
} }
&.status-box-open { &.status-box-open {
background-color: $green-light; background-color: $green-500;
} }
&.status-box-expired { &.status-box-expired {
......
...@@ -76,28 +76,28 @@ body { ...@@ -76,28 +76,28 @@ body {
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning { .alert-warning {
transition: background-color 0.15s, border-color 0.15s; transition: background-color 0.15s, border-color 0.15s;
background-color: lighten($gl-warning, 4%); background-color: $orange-500;
border-color: lighten($gl-warning, 4%); border-color: $orange-500;
} }
.alert-warning + .alert-warning { .alert-warning + .alert-warning {
background-color: $gl-warning; background-color: $orange-600;
border-color: $gl-warning; border-color: $orange-600;
} }
.alert-warning + .alert-warning + .alert-warning { .alert-warning + .alert-warning + .alert-warning {
background-color: darken($gl-warning, 4%); background-color: $orange-700;
border-color: darken($gl-warning, 4%); border-color: $orange-700;
} }
.alert-warning + .alert-warning + .alert-warning + .alert-warning { .alert-warning + .alert-warning + .alert-warning + .alert-warning {
background-color: darken($gl-warning, 8%); background-color: $orange-800;
border-color: darken($gl-warning, 8%); border-color: $orange-800;
} }
.alert-warning:only-of-type { .alert-warning:only-of-type {
background-color: $gl-warning; background-color: $orange-500;
border-color: $gl-warning; border-color: $orange-500;
} }
} }
......
...@@ -122,7 +122,7 @@ ul.content-list { ...@@ -122,7 +122,7 @@ ul.content-list {
} }
.member-group-link { .member-group-link {
color: $blue-normal; color: $blue-600;
} }
.description { .description {
......
...@@ -31,6 +31,7 @@ $border-radius-small: 3px !default; ...@@ -31,6 +31,7 @@ $border-radius-small: 3px !default;
// //
$text-color: $gl-text-color; $text-color: $gl-text-color;
$link-color: $gl-link-color; $link-color: $gl-link-color;
$link-hover-color: $gl-link-hover-color;
//== Typography //== Typography
...@@ -73,7 +74,7 @@ $pagination-hover-color: $gl-text-color; ...@@ -73,7 +74,7 @@ $pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover; $pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color; $pagination-hover-border: $border-color;
$pagination-active-color: $blue-dark; $pagination-active-color: $blue-600;
$pagination-active-bg: $white-light; $pagination-active-bg: $white-light;
$pagination-active-border: $border-color; $pagination-active-border: $border-color;
...@@ -135,8 +136,8 @@ $well-border: #eee; ...@@ -135,8 +136,8 @@ $well-border: #eee;
// //
//## //##
$code-color: #c7254e; $code-color: $red-600;
$code-bg: #f9f2f4; $code-bg: lighten($red-50, 2%);
$kbd-color: $white-light; $kbd-color: $white-light;
$kbd-bg: #333; $kbd-bg: #333;
......
...@@ -536,9 +536,9 @@ ...@@ -536,9 +536,9 @@
right: -3px; right: -3px;
top: -3px; top: -3px;
width: 17px; width: 17px;
background-color: $blue-light; background-color: $blue-500;
color: $white-light; color: $white-light;
border: 1px solid $border-blue-light; border: 1px solid $blue-600;
font-size: 9px; font-size: 9px;
line-height: 15px; line-height: 15px;
border-radius: 50%; border-radius: 50%;
......
...@@ -239,18 +239,18 @@ ...@@ -239,18 +239,18 @@
margin: 1px; margin: 1px;
&-finished { &-finished {
background-color: lighten($green-light, 25%); background-color: $green-100;
border-color: $green-light; border-color: $green-400;
} }
&-deploying { &-deploying {
background-color: lighten($green-light, 40%); background-color: $green-50;
border-color: $green-light; border-color: $green-400;
} }
&-failed { &-failed {
background-color: lighten($red-light, 20%); background-color: $red-200;
border-color: $red-normal; border-color: $red-500;
} }
&-ready { &-ready {
......
...@@ -530,12 +530,12 @@ ...@@ -530,12 +530,12 @@
&.over_estimate { &.over_estimate {
.meter-fill { .meter-fill {
background: $red-light; background: $red-500;
} }
.time-remaining, .time-remaining,
.compare-value.spent { .compare-value.spent {
color: $red-light; color: $red-500;
} }
} }
} }
......
...@@ -69,21 +69,17 @@ ul.related-merge-requests > li { ...@@ -69,21 +69,17 @@ ul.related-merge-requests > li {
height: 20px; height: 20px;
border-radius: 3px; border-radius: 3px;
line-height: 18px; line-height: 18px;
border: 1px solid;
&.merged { &.merged {
border-color: darken($blue-normal, 10%); background: $blue-500;
background: $blue-normal;
} }
&.closed { &.closed {
border-color: darken($red-normal, 10%); background: $red-500;
background: $red-normal;
} }
&.open { &.open {
border: 1px solid darken($green-normal, 10%); background: $green-500;
background: $green-normal;
} }
} }
...@@ -143,7 +139,7 @@ ul.related-merge-requests > li { ...@@ -143,7 +139,7 @@ ul.related-merge-requests > li {
} }
.export-checkmark { .export-checkmark {
color: $green-light; color: $green-400;
} }
} }
......
...@@ -85,11 +85,11 @@ ...@@ -85,11 +85,11 @@
} }
.username .validation-success { .username .validation-success {
color: $green-normal; color: $green-600;
} }
.username .validation-error { .username .validation-error {
color: $red-normal; color: $red-500;
} }
} }
} }
......
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
> p { > p {
float: left; float: left;
margin-bottom: 10px; margin-bottom: 10px;
color: $orange-normal; color: $orange-600;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-left: 55px; padding-left: 55px;
......
...@@ -255,7 +255,7 @@ $colors: ( ...@@ -255,7 +255,7 @@ $colors: (
&.saved { &.saved {
.editor { .editor {
border-top: solid 2px $border-green-extra-light; border-top: solid 2px $green-200;
} }
} }
......
...@@ -535,7 +535,7 @@ ...@@ -535,7 +535,7 @@
} }
.fa-info-circle { .fa-info-circle {
color: $orange-normal; color: $orange-500;
padding-right: 5px; padding-right: 5px;
} }
} }
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
} }
.remaining-days { .remaining-days {
color: $orange-light; color: $orange-600;
} }
.milestone-stats-and-buttons { .milestone-stats-and-buttons {
......
...@@ -462,17 +462,18 @@ ul.notes { ...@@ -462,17 +462,18 @@ ul.notes {
background: $white-light; background: $white-light;
padding: 1px 5px; padding: 1px 5px;
font-size: 12px; font-size: 12px;
color: $gl-link-color; color: $blue-500;
margin-left: -55px; margin-left: -55px;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
width: 23px; width: 23px;
height: 23px; height: 23px;
border: 1px solid $border-color; border: 1px solid $blue-500;
transition: transform .1s ease-in-out; transition: transform .1s ease-in-out;
&:hover { &:hover {
background: $gl-info; background: $blue-500;
border-color: $blue-600;
color: $white-light; color: $white-light;
transform: scale(1.15); transform: scale(1.15);
} }
......
...@@ -671,51 +671,71 @@ ...@@ -671,51 +671,71 @@
// Dropdown button animation in mini pipeline graph // Dropdown button animation in mini pipeline graph
&.ci-status-icon-success { &.ci-status-icon-success {
border-color: $gl-success; border-color: $green-500;
color: $gl-success; color: $green-500;
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
background-color: rgba($gl-success, 0.1); background-color: $green-50;
border-color: $gl-success; border-color: $green-600;
color: $green-600;
svg {
fill: $green-600;
}
} }
} }
&.ci-status-icon-failed { &.ci-status-icon-failed {
border-color: $gl-danger; border-color: $red-500;
color: $gl-danger; color: $red-500;
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
background-color: rgba($gl-danger, 0.1); background-color: $red-50;
border-color: $gl-danger; border-color: $red-600;
color: $red-600;
svg {
fill: $red-600;
}
} }
} }
&.ci-status-icon-pending, &.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings { &.ci-status-icon-success_with_warnings {
border-color: $gl-warning; border-color: $orange-500;
color: $gl-warning; color: $orange-500;
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
background-color: rgba($gl-warning, 0.1); background-color: $orange-50;
border-color: $gl-warning; border-color: $orange-600;
color: $orange-600;
svg {
fill: $orange-600;
}
} }
} }
&.ci-status-icon-running { &.ci-status-icon-running {
border-color: $blue-normal; border-color: $blue-400;
color: $blue-normal; color: $blue-400;
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
background-color: rgba($blue-normal, 0.1); background-color: $blue-50;
border-color: $blue-normal; border-color: $blue-600;
color: $blue-600;
svg {
fill: $blue-600;
}
} }
} }
......
...@@ -74,7 +74,6 @@ ...@@ -74,7 +74,6 @@
display: inline; display: inline;
a { a {
color: $blue-dark;
text-decoration: none; text-decoration: none;
} }
} }
......
...@@ -28,6 +28,6 @@ table .sherlock-code { ...@@ -28,6 +28,6 @@ table .sherlock-code {
} }
.sherlock-line-samples-table .slow { .sherlock-line-samples-table .slow {
color: $red-light; color: $red-500;
font-weight: bold; font-weight: bold;
} }
...@@ -21,42 +21,41 @@ ...@@ -21,42 +21,41 @@
&.ci-failed, &.ci-failed,
&.ci-failed_with_warnings { &.ci-failed_with_warnings {
color: $gl-danger; color: $red-500;
border-color: $gl-danger; border-color: $red-500;
&:not(span):hover { &: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 { svg {
fill: $gl-danger; fill: $red-500;
} }
} }
&.ci-success, &.ci-success,
&.ci-success_with_warnings { &.ci-success_with_warnings {
color: $gl-success; color: $green-600;
border-color: $gl-success; border-color: $green-500;
&:not(span):hover { &:not(span):hover {
background-color: rgba($gl-success, .07); background-color: $green-50;
} color: $green-700;
border-color: $green-600;
svg {
fill: $gl-success;
}
}
&.ci-info {
color: $gl-info;
border-color: $gl-info;
&:not(span):hover { svg {
background-color: rgba($gl-info, .07); fill: $green-600;
}
} }
svg { svg {
fill: $gl-info; fill: $green-500;
} }
} }
...@@ -75,28 +74,41 @@ ...@@ -75,28 +74,41 @@
} }
&.ci-pending { &.ci-pending {
color: $gl-warning; color: $orange-600;
border-color: $gl-warning; border-color: $orange-500;
&:not(span):hover { &: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 { svg {
fill: $gl-warning; fill: $orange-500;
} }
} }
&.ci-info,
&.ci-running { &.ci-running {
color: $blue-normal; color: $blue-500;
border-color: $blue-normal; border-color: $blue-500;
&:not(span):hover { &: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 { svg {
fill: $blue-normal; fill: $blue-500;
} }
} }
......
...@@ -40,7 +40,7 @@ module IssuableCollections ...@@ -40,7 +40,7 @@ module IssuableCollections
end end
def issues_collection 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 end
def merge_requests_collection def merge_requests_collection
......
...@@ -4,7 +4,7 @@ class Groups::AnalyticsController < Groups::ApplicationController ...@@ -4,7 +4,7 @@ class Groups::AnalyticsController < Groups::ApplicationController
layout 'group' layout 'group'
def show def show
@users = @group.users @users = @group.users.select(:id, :name, :username)
@start_date = params[:start_date] || Date.today - 1.week @start_date = params[:start_date] || Date.today - 1.week
@events = Event.contributions @events = Event.contributions
.where("created_at > ?", @start_date) .where("created_at > ?", @start_date)
...@@ -12,16 +12,21 @@ class Groups::AnalyticsController < Groups::ApplicationController ...@@ -12,16 +12,21 @@ class Groups::AnalyticsController < Groups::ApplicationController
@stats = {} @stats = {}
@stats[:merge_requests] = @users.map do |user| @stats[:total_events] = count_by_user(@events.totals_by_author)
@events.merge_requests.created.where(author_id: user).count @stats[:push] = count_by_user(@events.code_push.totals_by_author)
end @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| def count_by_user(data)
@events.issues.closed.where(author_id: user).count user_ids.map { |id| data.fetch(id, 0) }
end end
@stats[:push] = @users.map do |user| def user_ids
@events.code_push.where(author_id: user).count @user_ids ||= @users.map(&:id)
end
end end
end end
...@@ -82,7 +82,7 @@ module Projects ...@@ -82,7 +82,7 @@ module Projects
labels: true, labels: true,
only: [:id, :iid, :title, :confidential, :due_date, :relative_position], only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
include: { include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] } milestone: { only: [:id, :title] }
}, },
user: current_user user: current_user
......
...@@ -65,7 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -65,7 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController
def new def new
params[:issue] ||= ActionController::Parameters.new( params[:issue] ||= ActionController::Parameters.new(
assignee_id: "" assignee_ids: ""
) )
build_params = issue_params.merge( build_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
......
...@@ -26,17 +26,31 @@ class IssuesFinder < IssuableFinder ...@@ -26,17 +26,31 @@ class IssuesFinder < IssuableFinder
IssuesFinder.not_restricted_by_confidentiality(current_user) IssuesFinder.not_restricted_by_confidentiality(current_user)
end 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) 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 issues.confidential IS NULL
OR issues.confidential IS FALSE OR issues.confidential IS FALSE
OR (issues.confidential = TRUE OR (issues.confidential = TRUE
AND (issues.author_id = :user_id 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)))', OR issues.project_id IN(:project_ids)))',
user_id: user.id, user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
......
...@@ -11,10 +11,12 @@ module Emails ...@@ -11,10 +11,12 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end 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) 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)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
......
...@@ -25,7 +25,6 @@ module Issuable ...@@ -25,7 +25,6 @@ module Issuable
cache_markdown_field :description cache_markdown_field :description
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User" belongs_to :updated_by, class_name: "User"
belongs_to :milestone belongs_to :milestone
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do
...@@ -94,7 +93,6 @@ module Issuable ...@@ -94,7 +93,6 @@ module Issuable
attr_mentionable :description attr_mentionable :description
participant :author participant :author
participant :assignee
participant :notes_with_associations participant :notes_with_associations
strip_attributes :title strip_attributes :title
...@@ -285,7 +283,11 @@ module Issuable ...@@ -285,7 +283,11 @@ module Issuable
# DEPRECATED # DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage) 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 hook_data
end end
...@@ -319,14 +321,6 @@ module Issuable ...@@ -319,14 +321,6 @@ module Issuable
@human_class_name ||= self.class.name.titleize.downcase @human_class_name ||= self.class.name.titleize.downcase
end 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 def notes_with_associations
# If A has_many Bs, and B has_many Cs, and you do # 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 # `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
...@@ -358,11 +352,6 @@ module Issuable ...@@ -358,11 +352,6 @@ module Issuable
false false
end 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 def record_metrics
metrics = self.metrics || create_metrics metrics = self.metrics || create_metrics
metrics.record! metrics.record!
......
...@@ -46,6 +46,7 @@ class Event < ActiveRecord::Base ...@@ -46,6 +46,7 @@ class Event < ActiveRecord::Base
scope :created, -> { where(action: CREATED) } scope :created, -> { where(action: CREATED) }
scope :closed, -> { where(action: CLOSED) } scope :closed, -> { where(action: CLOSED) }
scope :merged, -> { where(action: MERGED) } scope :merged, -> { where(action: MERGED) }
scope :totals_by_author, -> { group(:author_id).count }
class << self class << self
# Update Gitlab::ContributionsCalendar#activity_dates if this changes # Update Gitlab::ContributionsCalendar#activity_dates if this changes
......
...@@ -29,11 +29,14 @@ class Issue < ActiveRecord::Base ...@@ -29,11 +29,14 @@ class Issue < ActiveRecord::Base
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all 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 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 :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } 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 :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_before, ->(date) { where('issues.due_date < ?', date) }
...@@ -51,6 +54,8 @@ class Issue < ActiveRecord::Base ...@@ -51,6 +54,8 @@ class Issue < ActiveRecord::Base
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true attr_spammable :description, spam_description: true
participant :assignees
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
transition [:reopened, :opened] => :closed transition [:reopened, :opened] => :closed
...@@ -127,6 +132,28 @@ class Issue < ActiveRecord::Base ...@@ -127,6 +132,28 @@ class Issue < ActiveRecord::Base
"id DESC") "id DESC")
end 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. # `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
...@@ -261,7 +288,7 @@ class Issue < ActiveRecord::Base ...@@ -261,7 +288,7 @@ class Issue < ActiveRecord::Base
true true
elsif confidential? elsif confidential?
author == user || author == user ||
assignee == user || assignees.include?(user) ||
project.team.member?(user, Gitlab::Access::REPORTER) project.team.member?(user, Gitlab::Access::REPORTER)
else else
project.public? || project.public? ||
......
...@@ -2,7 +2,7 @@ class License < ActiveRecord::Base ...@@ -2,7 +2,7 @@ class License < ActiveRecord::Base
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
validate :valid_license 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 :check_trueup, unless: :persisted?, if: :validate_with_trueup?
validate :not_expired, unless: :persisted? validate :not_expired, unless: :persisted?
...@@ -99,6 +99,10 @@ class License < ActiveRecord::Base ...@@ -99,6 +99,10 @@ class License < ActiveRecord::Base
restricted_attr(:previous_user_count) restricted_attr(:previous_user_count)
end end
def current_active_users_count
@current_active_users_count ||= User.active.count
end
def validate_with_trueup? def validate_with_trueup?
[restricted_attr(:trueup_quantity), [restricted_attr(:trueup_quantity),
restricted_attr(:trueup_from), restricted_attr(:trueup_from),
...@@ -127,37 +131,41 @@ class License < ActiveRecord::Base ...@@ -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.") self.errors.add(:base, "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc.")
end 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 HistoricalData.during(from..to).maximum(:active_user_count) || 0
end end
def active_user_count def check_users_limit
return unless restricted_user_count return unless restricted_user_count
historical_user_count = historical_max((starts_at - 1.year), starts_at) if previous_user_count && (historical_max <= previous_user_count)
overage = historical_user_count - restricted_user_count return if restricted_user_count >= current_active_users_count
else
return if historical_user_count <= restricted_user_count 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 end
def check_trueup def check_trueup
trueup_qty = restrictions[:trueup_quantity] trueup_qty = restrictions[:trueup_quantity]
trueup_from = Date.parse(restrictions[:trueup_from]) rescue (starts_at - 1.year) trueup_from = Date.parse(restrictions[:trueup_from]) rescue (starts_at - 1.year)
trueup_to = Date.parse(restrictions[:trueup_to]) rescue starts_at trueup_to = Date.parse(restrictions[:trueup_to]) rescue starts_at
active_user_count = User.active.count
max_historical = historical_max(trueup_from, trueup_to) 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 expected_trueup_qty = if previous_user_count
max_historical - previous_user_count max_historical - previous_user_count
else else
max_historical - active_user_count max_historical - current_active_users_count
end end
if trueup_qty >= expected_trueup_qty if trueup_qty >= expected_trueup_qty
if restricted_user_count < active_user_count if restricted_user_count < current_active_users_count
add_limit_error(trueup: true, user_count: active_user_count, restricted_user_count: restricted_user_count, overage: overage) add_limit_error(trueup: true, user_count: current_active_users_count, restricted_user_count: restricted_user_count, overage: overage)
end end
else else
message = "You have applied a True-up for #{trueup_qty} #{"user".pluralize(trueup_qty)} " message = "You have applied a True-up for #{trueup_qty} #{"user".pluralize(trueup_qty)} "
......
...@@ -22,6 +22,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -22,6 +22,8 @@ class MergeRequest < ActiveRecord::Base
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
belongs_to :assignee, class_name: "User"
serialize :merge_params, Hash serialize :merge_params, Hash
after_create :ensure_merge_request_diff, unless: :importing? after_create :ensure_merge_request_diff, unless: :importing?
...@@ -121,6 +123,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -121,6 +123,7 @@ class MergeRequest < ActiveRecord::Base
scope :references_project, -> { references(:target_project) } scope :references_project, -> { references(:target_project) }
participant :approvers_left participant :approvers_left
participant :assignee
after_save :keep_around_commit after_save :keep_around_commit
...@@ -182,6 +185,23 @@ class MergeRequest < ActiveRecord::Base ...@@ -182,6 +185,23 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}" work_in_progress?(title) ? title : "WIP: #{title}"
end 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. # `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
......
class IssuableEntity < Grape::Entity class IssuableEntity < Grape::Entity
expose :id expose :id
expose :iid expose :iid
expose :assignee_id
expose :assignees, using: UserEntity
expose :author_id expose :author_id
expose :description expose :description
expose :lock_version expose :lock_version
...@@ -18,4 +16,4 @@ class IssuableEntity < Grape::Entity ...@@ -18,4 +16,4 @@ class IssuableEntity < Grape::Entity
expose :total_time_spent expose :total_time_spent
expose :human_time_estimate expose :human_time_estimate
expose :human_total_time_spent expose :human_total_time_spent
end end
\ No newline at end of file
class IssueEntity < IssuableEntity class IssueEntity < IssuableEntity
expose :branch_name expose :branch_name
expose :confidential expose :confidential
expose :assignee_ids
expose :due_date expose :due_date
expose :moved_to_id expose :moved_to_id
expose :project_id expose :project_id
......
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
expose :approvals_before_merge expose :approvals_before_merge
expose :assignee_id
expose :in_progress_merge_commit_sha expose :in_progress_merge_commit_sha
expose :locked_at expose :locked_at
expose :merge_commit_sha expose :merge_commit_sha
......
class IssuableBaseService < BaseService class IssuableBaseService < BaseService
private private
def create_assignee_note(issuable)
SystemNoteService.change_assignee(
issuable, issuable.project, current_user, issuable.assignee)
end
def create_milestone_note(issuable) def create_milestone_note(issuable)
SystemNoteService.change_milestone( SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone) issuable, issuable.project, current_user, issuable.milestone)
...@@ -53,7 +48,13 @@ class IssuableBaseService < BaseService ...@@ -53,7 +48,13 @@ class IssuableBaseService < BaseService
params.delete(:add_label_ids) params.delete(:add_label_ids)
params.delete(:remove_label_ids) params.delete(:remove_label_ids)
params.delete(: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) params.delete(:due_date)
end end
...@@ -77,7 +78,7 @@ class IssuableBaseService < BaseService ...@@ -77,7 +78,7 @@ class IssuableBaseService < BaseService
def assignee_can_read?(issuable, assignee_id) def assignee_can_read?(issuable, assignee_id)
new_assignee = User.find_by_id(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}" ability_name = :"read_#{issuable.to_ability_name}"
resource = issuable.persisted? ? issuable : project resource = issuable.persisted? ? issuable : project
...@@ -207,6 +208,7 @@ class IssuableBaseService < BaseService ...@@ -207,6 +208,7 @@ class IssuableBaseService < BaseService
filter_params(issuable) filter_params(issuable)
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.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) 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) params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
...@@ -222,7 +224,13 @@ class IssuableBaseService < BaseService ...@@ -222,7 +224,13 @@ class IssuableBaseService < BaseService
handle_common_system_notes(issuable, old_labels: old_labels) handle_common_system_notes(issuable, old_labels: old_labels)
end 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) after_update(issuable)
issuable.create_new_cross_references!(current_user) issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update') execute_hooks(issuable, 'update')
......
...@@ -9,11 +9,28 @@ module Issues ...@@ -9,11 +9,28 @@ module Issues
private private
def create_assignee_note(issue)
SystemNoteService.change_issue_assignees(
issue, issue.project, current_user, issue.assignees)
end
def execute_hooks(issue, action = 'open') def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action) issue_data = hook_data(issue, action)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope) issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope) issue.project.execute_services(issue_data, hooks_scope)
end 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
end end
...@@ -21,7 +21,7 @@ module Issues ...@@ -21,7 +21,7 @@ module Issues
def csv_builder def csv_builder
@csv_builder ||= @csv_builder ||=
CsvBuilder.new(@issues.includes(:author, :assignee), header_to_value_hash) CsvBuilder.new(@issues.includes(:author), header_to_value_hash)
end end
private private
...@@ -35,8 +35,8 @@ module Issues ...@@ -35,8 +35,8 @@ module Issues
'Description' => 'description', 'Description' => 'description',
'Author' => 'author_name', 'Author' => 'author_name',
'Author Username' => -> (issue) { issue.author&.username }, 'Author Username' => -> (issue) { issue.author&.username },
'Assignee' => 'assignee_name', 'Assignee' => -> (issue) { issue.assignees.pluck(:name).join(', ') },
'Assignee Username' => -> (issue) { issue.assignee&.username }, 'Assignee Username' => -> (issue) { issue.assignees.pluck(:username).join(', ') },
'Confidential' => -> (issue) { issue.confidential? ? 'Yes' : 'No' }, 'Confidential' => -> (issue) { issue.confidential? ? 'Yes' : 'No' },
'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) }, 'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) },
'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) }, 'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) },
......
...@@ -12,7 +12,11 @@ module Issues ...@@ -12,7 +12,11 @@ module Issues
spam_check(issue, current_user) spam_check(issue, current_user)
end 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) if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user) todo_service.mark_pending_todos_as_done(issue, current_user)
end end
...@@ -26,9 +30,9 @@ module Issues ...@@ -26,9 +30,9 @@ module Issues
create_milestone_note(issue) create_milestone_note(issue)
end end
if issue.previous_changes.include?('assignee_id') if issue.previous_changes.include?('assignee_ids')
create_assignee_note(issue) 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) todo_service.reassigned_issue(issue, current_user)
end end
......
...@@ -4,7 +4,7 @@ module MergeRequests ...@@ -4,7 +4,7 @@ module MergeRequests
@assignable_issues ||= begin @assignable_issues ||= begin
if current_user == merge_request.author if current_user == merge_request.author
closes_issues.select do |issue| 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 end
else else
[] []
...@@ -14,7 +14,7 @@ module MergeRequests ...@@ -14,7 +14,7 @@ module MergeRequests
def execute def execute
assignable_issues.each do |issue| 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 end
{ {
......
...@@ -38,6 +38,11 @@ module MergeRequests ...@@ -38,6 +38,11 @@ module MergeRequests
private 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. # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
def merge_requests_for(source_branch, mr_states: [:opened]) def merge_requests_for(source_branch, mr_states: [:opened])
MergeRequest MergeRequest
......
...@@ -31,7 +31,10 @@ module MergeRequests ...@@ -31,7 +31,10 @@ module MergeRequests
merge_request merge_request
end 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) if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user) todo_service.mark_pending_todos_as_done(merge_request, current_user)
end end
......
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
# #
class NotificationRecipientService class NotificationRecipientService
attr_reader :project attr_reader :project
def initialize(project) def initialize(project)
@project = project @project = project
end 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) def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
...@@ -23,9 +24,13 @@ class NotificationRecipientService ...@@ -23,9 +24,13 @@ class NotificationRecipientService
# Re-assign is considered as a mention of the new assignee so we add the # 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 # new assignee to the list of recipients after we rejected users with
# the "on mention" notification level # 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 << previous_assignee if previous_assignee
recipients << target.assignee recipients << target.assignee
when :reassign_issue
recipients.concat(previous_assignee) if previous_assignee.any?
recipients.concat(target.assignees)
end end
recipients = reject_muted_users(recipients) recipients = reject_muted_users(recipients)
......
...@@ -66,8 +66,23 @@ class NotificationService ...@@ -66,8 +66,23 @@ class NotificationService
# * issue new assignee if their notification level is not Disabled # * issue new assignee if their notification level is not Disabled
# * users with custom level checked with "reassign issue" # * users with custom level checked with "reassign issue"
# #
def reassigned_issue(issue, current_user) def reassigned_issue(issue, current_user, previous_assignees = [])
reassign_resource_email(issue, issue.project, current_user, :reassigned_issue_email) 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 end
# When we add labels to an issue we should send an email to: # When we add labels to an issue we should send an email to:
...@@ -407,10 +422,10 @@ class NotificationService ...@@ -407,10 +422,10 @@ class NotificationService
end end
def previous_record(object, attribute) def previous_record(object, attribute)
if object && attribute return unless object && attribute
if object.previous_changes.include?(attribute)
object.previous_changes[attribute].first if object.previous_changes.include?(attribute)
end object.previous_changes[attribute].first
end end
end end
end end
...@@ -49,6 +49,42 @@ module SystemNoteService ...@@ -49,6 +49,42 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end 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 # Called when one or more labels on a Noteable are added and/or removed
# #
# noteable - Noteable object # noteable - Noteable object
......
...@@ -264,13 +264,13 @@ class TodoService ...@@ -264,13 +264,13 @@ class TodoService
end end
def create_assignment_todo(issuable, author) def create_assignment_todo(issuable, author)
if issuable.assignee if issuable.assignees.any?
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED) attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
create_todos(issuable.assignee, attributes) create_todos(issuable.assignees, attributes)
end end
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 # Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(project, note || target, author) directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note) attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
.col-md-8 .col-md-8
%div %div
%p.light Merge requests created per group member %p.light Merge requests created per group member
%canvas#merge_requests{ height: 250 } %canvas#merge_requests_created{ height: 250 }
%h3 Issues %h3 Issues
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
.col-md-8 .col-md-8
%div %div
%p.light Issues closed per group member %p.light Issues closed per group member
%canvas#issues{ height: 250 } %canvas#issues_closed{ height: 250 }
.gray-content-block .gray-content-block
.oneline .oneline
...@@ -109,21 +109,21 @@ ...@@ -109,21 +109,21 @@
Total Contributions Total Contributions
= icon('sort') = icon('sort')
%tbody %tbody
- @users.each do |user| - @users.each_with_index do |user, index|
%tr %tr
%td %td
%strong %strong
= link_to user.name, user = link_to user.name, user
%td= @events.code_push.where(author_id: user).count %td= @stats[:push][index]
%td= @events.issues.created.where(author_id: user).count %td= @stats[:issues_created][index]
%td= @events.issues.closed.where(author_id: user).count %td= @stats[:issues_closed][index]
%td= @events.merge_requests.created.where(author_id: user).count %td= @stats[:merge_requests_created][index]
%td= @events.merge_requests.merged.where(author_id: user).count %td= @stats[:merge_requests_merged][index]
%td= @events.where(author_id: user).count %td= @stats[:total_events][index]
- [:push, :issues, :merge_requests].each do |scope| - [:push, :issues_closed, :merge_requests_created].each do |scope|
:javascript :javascript
var data = { var data = {
labels : #{@users.map(&:name).to_json}, labels : #{@users.map(&:name).to_json},
......
...@@ -23,10 +23,12 @@ xml.entry do ...@@ -23,10 +23,12 @@ xml.entry do
end end
end end
if issue.assignee if issue.assignees.any?
xml.assignee do xml.assignees do
xml.name issue.assignee.name issue.assignees.each do |assignee|
xml.email issue.assignee_public_email xml.name ssignee.name
xml.email assignee.assignee_public_email
end
end end
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 -%> Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %> to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
- if @issue.description - if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author) = markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present? - if @issue.assignees.any?
%p %p
Assignee: #{@issue.assignee_name} Assignee: #{@issue.assignee_list}
...@@ -2,6 +2,6 @@ New Issue was created. ...@@ -2,6 +2,6 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
Author: <%= @issue.author_name %> Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_name %> Assignee: <%= @issue.assignee_list %>
<%= @issue.description %> <%= @issue.description %>
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
- if @issue.description - if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author) = markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present? - if @issue.assignees.any?
%p %p
Assignee: #{@issue.assignee_name} Assignee: #{@issue.assignee_list}
...@@ -2,6 +2,6 @@ You have been mentioned in an issue. ...@@ -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)) %> Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
Author: <%= @issue.author_name %> Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_name %> Assignee: <%= @issue.assignee_list %>
<%= @issue.description %> <%= @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 @@ ...@@ -13,7 +13,7 @@
%li %li
CLOSED CLOSED
- if issue.assignee - if issue.assignees.any?
%li %li
= render 'shared/issuable/assignees', project: @project, issue: issue = render 'shared/issuable/assignees', project: @project, issue: issue
...@@ -48,4 +48,4 @@ ...@@ -48,4 +48,4 @@
= issue.weight = issue.weight
.pull-right.issue-updated-at .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 @@ ...@@ -22,36 +22,8 @@
= icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true') = 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| = 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 = render "shared/issuable/form/#{issuable.model_name.singular}_assignee", can_edit_issuable: can_edit_issuable, issuable: issuable, f: f
= 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 } })
.block.milestone .block.milestone
.sidebar-collapsed-icon .sidebar-collapsed-icon
...@@ -162,11 +134,11 @@ ...@@ -162,11 +134,11 @@
= icon('spinner spin', class: 'block-loading') = icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed .value.hide-collapsed
- if issuable.weight - if issuable.weight
= issuable.weight %strong= issuable.weight
- else - else
.light None %span.no-value None
.selectbox.hide-collapsed .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 = 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 %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 @@ ...@@ -10,13 +10,24 @@
.row .row
%div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
.form-group.issue-assignee .form-group.issue-assignee
= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" - if issuable.is_a?(Issue)
.col-sm-10{ class: ("col-lg-8" if has_due_date) } = form.label :assignee_ids, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.issuable-form-select-holder .col-sm-10{ class: ("col-lg-8" if has_due_date) }
= form.hidden_field :assignee_id .issuable-form-select-holder
= 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", = form.hidden_field :assignee_ids
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}" = 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-group.issue-milestone
= form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" = 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) } .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 ...@@ -8,7 +8,7 @@ Gitlab::Seeder.quiet do
description: FFaker::Lorem.sentence, description: FFaker::Lorem.sentence,
state: ['opened', 'closed'].sample, state: ['opened', 'closed'].sample,
milestone: project.milestones.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 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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170317203554) do ActiveRecord::Schema.define(version: 20170320173259) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -495,6 +495,14 @@ ActiveRecord::Schema.define(version: 20170317203554) do ...@@ -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 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| create_table "issue_metrics", force: :cascade do |t|
t.integer "issue_id", null: false t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at" t.datetime "first_mentioned_in_commit_at"
...@@ -1491,6 +1499,8 @@ ActiveRecord::Schema.define(version: 20170317203554) do ...@@ -1491,6 +1499,8 @@ ActiveRecord::Schema.define(version: 20170317203554) do
add_foreign_key "boards", "projects" add_foreign_key "boards", "projects"
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade 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 "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 "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
...@@ -28,7 +28,7 @@ module Banzai ...@@ -28,7 +28,7 @@ module Banzai
nodes, nodes,
Issue.all.includes( Issue.all.includes(
:author, :author,
:assignee, :assignees,
{ {
# These associations are primarily used for checking permissions. # These associations are primarily used for checking permissions.
# Eager loading these ensures we don't end up running dozens of # Eager loading these ensures we don't end up running dozens of
......
...@@ -16,7 +16,7 @@ describe Dashboard::TodosController do ...@@ -16,7 +16,7 @@ describe Dashboard::TodosController do
describe 'GET #index' do describe 'GET #index' do
context 'when using pagination' do context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages } 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 before do
issues.each { |issue| todo_service.new_issue(issue, user) } 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 ...@@ -34,7 +34,7 @@ describe Projects::Boards::IssuesController do
issue = create(:labeled_issue, project: project, labels: [planning]) issue = create(:labeled_issue, project: project, labels: [planning])
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], 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) issue.subscribe(johndoe, project)
list_issues user: user, board: board, list: list2 list_issues user: user, board: board, list: list2
......
...@@ -336,7 +336,7 @@ describe Projects::IssuesController do ...@@ -336,7 +336,7 @@ describe Projects::IssuesController do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let!(:issue) { create(:issue, project: project) } let!(:issue) { create(:issue, project: project) }
let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) } 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 describe 'GET #index' do
it 'does not list confidential issues for guests' do it 'does not list confidential issues for guests' do
......
...@@ -1329,7 +1329,7 @@ describe Projects::MergeRequestsController do ...@@ -1329,7 +1329,7 @@ describe Projects::MergeRequestsController do
end end
it 'correctly pluralizes flash message on success' do it 'correctly pluralizes flash message on success' do
issue2.update!(assignee: user) issue2.update!(assignees: [user])
post_assign_issues post_assign_issues
......
...@@ -32,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do ...@@ -32,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do
end end
context "issue with basic fields" do 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 it "renders issue fields" do
visit issues_dashboard_path(:atom, private_token: user.private_token) visit issues_dashboard_path(:atom, private_token: user.private_token)
...@@ -51,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do ...@@ -51,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do
context "issue with label and milestone" do context "issue with label and milestone" do
let!(:milestone1) { create(:milestone, project: project1, title: 'v1') } let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
let!(:label1) { create(:label, project: project1, title: 'label1') } 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 before do
issue1.labels << label1 issue1.labels << label1
......
...@@ -6,7 +6,7 @@ describe 'Issues Feed', feature: true do ...@@ -6,7 +6,7 @@ describe 'Issues Feed', feature: true do
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:project) { create(:project) } 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 before do
project.team << [user, :developer] project.team << [user, :developer]
......
...@@ -72,7 +72,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -72,7 +72,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list2) { create(:list, board: board, label: development, position: 1) } 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!(: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!(: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!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) } 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 ...@@ -98,7 +98,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
end end
context 'assignee' do context 'assignee' do
let!(:issue) { create(:issue, project: project, assignee: user2) } let!(:issue) { create(:issue, project: project, assignees: [user2]) }
before do before do
project.team << [user2, :developer] project.team << [user2, :developer]
......
...@@ -11,7 +11,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -11,7 +11,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:bug) { create(:label, project: project, name: 'Bug') } let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:regression) { create(:label, project: project, name: 'Regression') } let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') } 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!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) } let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) } 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 ...@@ -7,7 +7,7 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do
let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request) { create(:merge_request, source_project: project) }
before do before do
issue.update(assignee: user) issue.assignees = [user]
merge_request.update(assignee: user) merge_request.update(assignee: user)
login_as(user) login_as(user)
end end
...@@ -17,7 +17,7 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do ...@@ -17,7 +17,7 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do
expect_counters('issues', '1') expect_counters('issues', '1')
issue.update(assignee: nil) issue.assignees = []
visit issues_dashboard_path visit issues_dashboard_path
expect_counters('issues', '1') expect_counters('issues', '1')
......
...@@ -11,7 +11,7 @@ RSpec.describe 'Dashboard Issues', feature: true do ...@@ -11,7 +11,7 @@ RSpec.describe 'Dashboard Issues', feature: true do
let!(:authored_issue) { create :issue, author: current_user, project: project } 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!(: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 } let!(:other_issue) { create :issue, project: project }
before do before do
......
...@@ -10,8 +10,8 @@ describe "Dashboard Issues filtering", feature: true, js: true do ...@@ -10,8 +10,8 @@ describe "Dashboard Issues filtering", feature: true, js: true do
project.team << [user, :master] project.team << [user, :master]
login_as(user) login_as(user)
create(:issue, project: project, author: user, assignee: user) create(:issue, project: project, author: user, assignees: [user])
create(:issue, project: project, author: user, assignee: user, milestone: milestone) create(:issue, project: project, author: user, assignees: [user], milestone: milestone)
visit_issues visit_issues
end end
......
...@@ -52,11 +52,11 @@ describe "GitLab Flavored Markdown", feature: true do ...@@ -52,11 +52,11 @@ describe "GitLab Flavored Markdown", feature: true do
before do before do
@other_issue = create(:issue, @other_issue = create(:issue,
author: @user, author: @user,
assignee: @user, assignees: [@user],
project: project) project: project)
@issue = create(:issue, @issue = create(:issue,
author: @user, author: @user,
assignee: @user, assignees: [@user],
project: project, project: project,
title: "fix #{@other_issue.to_reference}", title: "fix #{@other_issue.to_reference}",
description: "ask #{fred.to_reference} for details") description: "ask #{fred.to_reference} for details")
......
...@@ -7,7 +7,7 @@ describe 'Awards Emoji', feature: true do ...@@ -7,7 +7,7 @@ describe 'Awards Emoji', feature: true do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let(:issue) do let(:issue) do
create(:issue, create(:issue,
assignee: @user, assignees: [@user],
project: project) project: project)
end end
......
...@@ -76,10 +76,10 @@ describe 'Issues csv', feature: true do ...@@ -76,10 +76,10 @@ describe 'Issues csv', feature: true do
create_list(:labeled_issue, create_list(:labeled_issue,
10, 10,
project: project, project: project,
assignee: user, assignees: [user],
author: user, author: user,
milestone: milestone, milestone: milestone,
labels: [feature_label, idea_label]) 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
end end
...@@ -50,15 +50,15 @@ describe 'Filter issues', js: true, feature: true do ...@@ -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 'single quotes'", project: project)
create(:issue, title: "issue with \"double quotes\"", project: project) create(:issue, title: "issue with \"double quotes\"", project: project)
create(:issue, title: "issue with !@\#{$%^&*()-+", 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", project: project, milestone: milestone, author: user, assignees: [user])
create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user) create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignees: [user])
issue = create(:issue, issue = create(:issue,
title: "Bug 2", title: "Bug 2",
project: project, project: project,
milestone: milestone, milestone: milestone,
author: user, author: user,
assignee: user) assignees: [user])
issue.labels << bug_label issue.labels << bug_label
issue_with_caps_label = create(:issue, issue_with_caps_label = create(:issue,
...@@ -66,7 +66,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -66,7 +66,7 @@ describe 'Filter issues', js: true, feature: true do
project: project, project: project,
milestone: milestone, milestone: milestone,
author: user, author: user,
assignee: user) assignees: [user])
issue_with_caps_label.labels << caps_sensitive_label issue_with_caps_label.labels << caps_sensitive_label
issue_with_everything = create(:issue, issue_with_everything = create(:issue,
...@@ -74,7 +74,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -74,7 +74,7 @@ describe 'Filter issues', js: true, feature: true do
project: project, project: project,
milestone: milestone, milestone: milestone,
author: user, author: user,
assignee: user) assignees: [user])
issue_with_everything.labels << bug_label issue_with_everything.labels << bug_label
issue_with_everything.labels << caps_sensitive_label issue_with_everything.labels << caps_sensitive_label
......
...@@ -31,7 +31,7 @@ describe 'Filter issues weight', js: true, feature: true do ...@@ -31,7 +31,7 @@ describe 'Filter issues weight', js: true, feature: true do
title: 'Bug report 1', title: 'Bug report 1',
milestone: milestone, milestone: milestone,
author: user, author: user,
assignee: user) assignees: [user])
issue.labels << label issue.labels << label
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
......
...@@ -9,7 +9,7 @@ describe 'New/edit issue', feature: true, js: true do ...@@ -9,7 +9,7 @@ describe 'New/edit issue', feature: true, js: true do
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
let!(:label2) { 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 before do
project.team << [user, :master] project.team << [user, :master]
......
...@@ -101,7 +101,7 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -101,7 +101,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end end
def create_assigned def create_assigned
create(:issue, project: project, assignee: user) create(:issue, project: project, assignees: [user])
end end
def create_with_milestone def create_with_milestone
......
...@@ -19,7 +19,7 @@ describe 'Issues', feature: true do ...@@ -19,7 +19,7 @@ describe 'Issues', feature: true do
let!(:issue) do let!(:issue) do
create(:issue, create(:issue,
author: @user, author: @user,
assignee: @user, assignees: [@user],
project: project) project: project)
end end
...@@ -44,7 +44,7 @@ describe 'Issues', feature: true do ...@@ -44,7 +44,7 @@ describe 'Issues', feature: true do
let!(:issue) do let!(:issue) do
create(:issue, create(:issue,
author: @user, author: @user,
assignee: @user, assignees: [@user],
project: project) project: project)
end end
...@@ -139,7 +139,7 @@ describe 'Issues', feature: true do ...@@ -139,7 +139,7 @@ describe 'Issues', feature: true do
describe 'Issue info' do describe 'Issue info' do
it 'excludes award_emoji from comment count' 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) create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
...@@ -165,14 +165,14 @@ describe 'Issues', feature: true do ...@@ -165,14 +165,14 @@ describe 'Issues', feature: true do
%w(foobar barbaz gitlab).each do |title| %w(foobar barbaz gitlab).each do |title|
create(:issue, create(:issue,
author: @user, author: @user,
assignee: @user, assignees: [@user],
project: project, project: project,
title: title) title: title)
end end
@issue = Issue.find_by(title: 'foobar') @issue = Issue.find_by(title: 'foobar')
@issue.milestone = create(:milestone, project: project) @issue.milestone = create(:milestone, project: project)
@issue.assignee = nil @issue.assignees = []
@issue.save @issue.save
end end
...@@ -408,7 +408,7 @@ describe 'Issues', feature: true do ...@@ -408,7 +408,7 @@ describe 'Issues', feature: true do
end end
describe 'update labels from issue#show', js: true do 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) } let!(:label) { create(:label, project: project) }
before do before do
...@@ -426,7 +426,7 @@ describe 'Issues', feature: true do ...@@ -426,7 +426,7 @@ describe 'Issues', feature: true do
end end
describe 'update assignee from issue#show' do 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 context 'by authorized user' do
it 'allows user to select unassigned', js: true do it 'allows user to select unassigned', js: true do
...@@ -668,7 +668,7 @@ describe 'Issues', feature: true do ...@@ -668,7 +668,7 @@ describe 'Issues', feature: true do
describe 'due date' do describe 'due date' do
context 'update due on issue#show', js: true 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 before do
visit namespace_project_issue_path(project.namespace, project, issue) visit namespace_project_issue_path(project.namespace, project, issue)
......
...@@ -33,7 +33,7 @@ feature 'Merge request issue assignment', js: true, feature: true do ...@@ -33,7 +33,7 @@ feature 'Merge request issue assignment', js: true, feature: true do
end end
it "doesn't display if related issues are already assigned" do 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 visit_merge_request
......
...@@ -5,10 +5,10 @@ describe 'Milestone show', feature: true do ...@@ -5,10 +5,10 @@ describe 'Milestone show', feature: true do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_list(:label, 2, 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 before do
project.add_user(user, :developer) project.add_user(user, :developer)
login_as(user) login_as(user)
end end
......
...@@ -14,7 +14,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -14,7 +14,7 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates' do context 'user creates an issue using templates' do
let(:template_content) { 'this is a test "bug" template' } 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(: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' } let(:description_addition) { ' appending to description' }
background do background do
...@@ -74,7 +74,7 @@ feature 'issuable templates', feature: true, js: true 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 context 'user creates an issue using templates, with a prior description' do
let(:prior_description) { 'test issue description' } let(:prior_description) { 'test issue description' }
let(:template_content) { 'this is a test "bug" template' } 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 background do
project.repository.create_file( 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