@@ -36,7 +36,6 @@ import org.sonar.server.db.BaseDao; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import javax.annotation.CheckForNull; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.List; |
@@ -22,18 +22,16 @@ package org.sonar.server.computation.step; | |||
import com.google.common.collect.ImmutableSet; | |||
import org.sonar.api.issue.internal.DefaultIssue; | |||
import org.sonar.api.resources.Qualifiers; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.core.component.ComponentDto; | |||
import org.sonar.server.computation.ComputationContext; | |||
import org.sonar.server.computation.issue.IssueCache; | |||
import org.sonar.server.computation.issue.RuleCache; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesStatistics; | |||
import org.sonar.server.issue.notification.*; | |||
import org.sonar.server.notifications.NotificationService; | |||
import org.sonar.server.util.CloseableIterator; | |||
import java.util.Date; | |||
import java.util.Map; | |||
import java.util.Set; | |||
/** | |||
@@ -45,18 +43,18 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
/** | |||
* Types of the notifications sent by this step | |||
*/ | |||
static final Set<String> NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE); | |||
static final Set<String> NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE, MyNewIssuesNotification.TYPE); | |||
private final IssueCache issueCache; | |||
private final RuleCache rules; | |||
private final NotificationService service; | |||
private final Durations durations; | |||
private NewIssuesNotificationFactory newIssuesNotificationFactory; | |||
public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service, Durations durations) { | |||
public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service, NewIssuesNotificationFactory newIssuesNotificationFactory) { | |||
this.issueCache = issueCache; | |||
this.rules = rules; | |||
this.service = service; | |||
this.durations = durations; | |||
this.newIssuesNotificationFactory = newIssuesNotificationFactory; | |||
} | |||
@Override | |||
@@ -94,15 +92,33 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
sendNewIssuesStatistics(context, newIssuesStats); | |||
} | |||
private void sendNewIssuesStatistics(ComputationContext context, NewIssuesStatistics stats) { | |||
if (stats.hasIssues()) { | |||
private void sendNewIssuesStatistics(ComputationContext context, NewIssuesStatistics statistics) { | |||
if (statistics.hasIssues()) { | |||
NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics(); | |||
ComponentDto project = context.getProject(); | |||
NewIssuesNotification notification = new NewIssuesNotification(); | |||
notification.setProject(project); | |||
notification.setAnalysisDate(new Date(context.getReportMetadata().getAnalysisDate())); | |||
notification.setStatistics(project, stats); | |||
notification.setDebt(durations.encode(stats.debt())); | |||
NewIssuesNotification notification = newIssuesNotificationFactory | |||
.newNewIssuesNotication() | |||
.setProject(project) | |||
.setAnalysisDate(new Date(context.getReportMetadata().getAnalysisDate())) | |||
.setStatistics(project, globalStatistics) | |||
.setDebt(globalStatistics.debt()); | |||
service.deliver(notification); | |||
// send email to each user having issues | |||
for (Map.Entry<String, NewIssuesStatistics.Stats> assigneeAndStatisticsTuple : statistics.assigneesStatistics().entrySet()) { | |||
String assignee = assigneeAndStatisticsTuple.getKey(); | |||
NewIssuesStatistics.Stats assigneeStatistics = assigneeAndStatisticsTuple.getValue(); | |||
MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory | |||
.newMyNewIssuesNotification() | |||
.setAssignee(assignee); | |||
myNewIssuesNotification | |||
.setProject(project) | |||
.setAnalysisDate(new Date(context.getReportMetadata().getAnalysisDate())) | |||
.setStatistics(project, assigneeStatistics) | |||
.setDebt(assigneeStatistics.debt()); | |||
service.deliver(myNewIssuesNotification); | |||
} | |||
} | |||
} | |||
@@ -0,0 +1,188 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.collect.Lists; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.plugins.emailnotifications.api.EmailMessage; | |||
import org.sonar.plugins.emailnotifications.api.EmailTemplate; | |||
import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC; | |||
import java.io.UnsupportedEncodingException; | |||
import java.net.URLEncoder; | |||
import java.util.Date; | |||
import java.util.Iterator; | |||
import java.util.Locale; | |||
import static com.google.common.base.Preconditions.checkNotNull; | |||
/** | |||
* Base class to create emails for new issues | |||
*/ | |||
public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate { | |||
protected static final char NEW_LINE = '\n'; | |||
protected static final String TAB = " "; | |||
protected static final String DOT = "."; | |||
protected static final String COUNT = DOT + "count"; | |||
protected static final String LABEL = DOT + "label"; | |||
static final String FIELD_PROJECT_NAME = "projectName"; | |||
static final String FIELD_PROJECT_KEY = "projectKey"; | |||
static final String FIELD_PROJECT_DATE = "projectDate"; | |||
static final String FIELD_PROJECT_UUID = "projectUuid"; | |||
static final String FIELD_ASSIGNEE = "assignee"; | |||
protected final EmailSettings settings; | |||
protected final I18n i18n; | |||
public AbstractNewIssuesEmailTemplate(EmailSettings settings, I18n i18n) { | |||
this.settings = settings; | |||
this.i18n = i18n; | |||
} | |||
public static String encode(String toEncode) { | |||
try { | |||
return URLEncoder.encode(toEncode, "UTF-8"); | |||
} catch (UnsupportedEncodingException e) { | |||
throw new IllegalStateException("Encoding not supported", e); | |||
} | |||
} | |||
@Override | |||
public EmailMessage format(Notification notification) { | |||
if (shouldNotFormat(notification)) { | |||
return null; | |||
} | |||
String projectName = checkNotNull(notification.getFieldValue(FIELD_PROJECT_NAME)); | |||
StringBuilder message = new StringBuilder(); | |||
message.append("Project: ").append(projectName).append(NEW_LINE).append(NEW_LINE); | |||
appendSeverity(message, notification); | |||
appendAssignees(message, notification); | |||
appendTags(message, notification); | |||
appendComponents(message, notification); | |||
appendFooter(message, notification); | |||
return new EmailMessage() | |||
.setMessageId(notification.getType() + "/" + notification.getFieldValue(FIELD_PROJECT_KEY)) | |||
.setSubject(subject(notification, projectName)) | |||
.setMessage(message.toString()); | |||
} | |||
protected abstract boolean shouldNotFormat(Notification notification); | |||
protected String subject(Notification notification, String projectName) { | |||
return String.format("%s: %s new issues (new debt: %s)", | |||
projectName, | |||
notification.getFieldValue(METRIC.SEVERITY + COUNT), | |||
notification.getFieldValue(METRIC.DEBT + COUNT)); | |||
} | |||
private boolean doNotHaveValue(Notification notification, METRIC metric) { | |||
return notification.getFieldValue(metric + DOT + "1" + LABEL) == null; | |||
} | |||
private void genericAppendOfMetric(METRIC metric, String label, StringBuilder message, Notification notification) { | |||
if (doNotHaveValue(notification, metric)) { | |||
return; | |||
} | |||
message | |||
.append(TAB) | |||
.append(label) | |||
.append(NEW_LINE); | |||
int i = 1; | |||
while (notification.getFieldValue(metric + DOT + i + LABEL) != null && i <= 5) { | |||
String name = notification.getFieldValue(metric + DOT + i + LABEL); | |||
message | |||
.append(TAB).append(TAB) | |||
.append(name) | |||
.append(": ") | |||
.append(notification.getFieldValue(metric + DOT + i + COUNT)) | |||
.append(NEW_LINE); | |||
i += 1; | |||
} | |||
message.append(NEW_LINE); | |||
} | |||
protected void appendAssignees(StringBuilder message, Notification notification) { | |||
genericAppendOfMetric(METRIC.ASSIGNEE, "Assignees", message, notification); | |||
} | |||
protected void appendComponents(StringBuilder message, Notification notification) { | |||
genericAppendOfMetric(METRIC.COMPONENT, "Most impacted files", message, notification); | |||
} | |||
protected void appendTags(StringBuilder message, Notification notification) { | |||
genericAppendOfMetric(METRIC.TAGS, "Tags", message, notification); | |||
} | |||
protected void appendSeverity(StringBuilder message, Notification notification) { | |||
message | |||
.append(String.format("%s new issues (new debt: %s)", | |||
notification.getFieldValue(METRIC.SEVERITY + COUNT), | |||
notification.getFieldValue(METRIC.DEBT + COUNT))) | |||
.append(NEW_LINE).append(NEW_LINE) | |||
.append(TAB) | |||
.append("Severity") | |||
.append(NEW_LINE) | |||
.append(TAB) | |||
.append(TAB); | |||
for (Iterator<String> severityIterator = Lists.reverse(Severity.ALL).iterator(); severityIterator.hasNext();) { | |||
String severity = severityIterator.next(); | |||
String severityLabel = i18n.message(getLocale(), "severity." + severity, severity); | |||
message.append(severityLabel).append(": ").append(notification.getFieldValue(METRIC.SEVERITY + DOT + severity + COUNT)); | |||
if (severityIterator.hasNext()) { | |||
message.append(TAB); | |||
} | |||
} | |||
message | |||
.append(NEW_LINE) | |||
.append(NEW_LINE); | |||
} | |||
protected void appendFooter(StringBuilder message, Notification notification) { | |||
String projectUuid = notification.getFieldValue(FIELD_PROJECT_UUID); | |||
String dateString = notification.getFieldValue(FIELD_PROJECT_DATE); | |||
if (projectUuid != null && dateString != null) { | |||
Date date = DateUtils.parseDateTime(dateString); | |||
String url = String.format("%s/issues/search#projectUuids=%s|createdAt=%s", | |||
settings.getServerBaseURL(), encode(projectUuid), encode(DateUtils.formatDateTime(date))); | |||
message | |||
.append("See it in SonarQube: ") | |||
.append(url) | |||
.append(NEW_LINE); | |||
} | |||
} | |||
private Locale getLocale() { | |||
return Locale.ENGLISH; | |||
} | |||
} |
@@ -0,0 +1,72 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC; | |||
import java.util.Date; | |||
/** | |||
* Creates email message for notification "my-new-issues". | |||
*/ | |||
public class MyNewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate { | |||
public MyNewIssuesEmailTemplate(EmailSettings settings, I18n i18n) { | |||
super(settings, i18n); | |||
} | |||
@Override | |||
protected boolean shouldNotFormat(Notification notification) { | |||
return !MyNewIssuesNotification.TYPE.equals(notification.getType()); | |||
} | |||
@Override | |||
protected void appendAssignees(StringBuilder message, Notification notification) { | |||
// do nothing as we don't want to print assignees, it's a personalized email for one person | |||
} | |||
@Override | |||
protected String subject(Notification notification, String projectName) { | |||
return String.format("You have %s new issues on project %s", | |||
notification.getFieldValue(METRIC.SEVERITY + COUNT), | |||
projectName); | |||
} | |||
@Override | |||
protected void appendFooter(StringBuilder message, Notification notification) { | |||
String projectUuid = notification.getFieldValue(FIELD_PROJECT_UUID); | |||
String dateString = notification.getFieldValue(FIELD_PROJECT_DATE); | |||
String assignee = notification.getFieldValue(FIELD_ASSIGNEE); | |||
if (projectUuid != null && dateString != null && assignee != null) { | |||
Date date = DateUtils.parseDateTime(dateString); | |||
String url = String.format("%s/issues/search#projectUuids=%s|assignees=%s|createdAt=%s", | |||
settings.getServerBaseURL(), | |||
encode(projectUuid), | |||
encode(assignee), | |||
encode(DateUtils.formatDateTime(date))); | |||
message.append("See it in SonarQube: ").append(url).append(NEW_LINE); | |||
} | |||
} | |||
} |
@@ -0,0 +1,42 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.server.db.DbClient; | |||
import org.sonar.server.user.index.UserIndex; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_ASSIGNEE; | |||
public class MyNewIssuesNotification extends NewIssuesNotification { | |||
public static final String TYPE = "my-new-issues"; | |||
MyNewIssuesNotification(UserIndex userIndex, DbClient dbClient, Durations durations) { | |||
super(TYPE, userIndex, dbClient, durations); | |||
} | |||
public MyNewIssuesNotification setAssignee(String assignee) { | |||
setFieldValue(FIELD_ASSIGNEE, assignee); | |||
return this; | |||
} | |||
} |
@@ -0,0 +1,64 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.collect.Multimap; | |||
import org.sonar.api.notifications.*; | |||
import java.util.Collection; | |||
/** | |||
* This dispatcher means: "notify me when new issues are introduced during project analysis" | |||
*/ | |||
public class MyNewIssuesNotificationDispatcher extends NotificationDispatcher { | |||
public static final String KEY = "MyNewIssues"; | |||
private final NotificationManager manager; | |||
public MyNewIssuesNotificationDispatcher(NotificationManager manager) { | |||
super(MyNewIssuesNotification.TYPE); | |||
this.manager = manager; | |||
} | |||
public static NotificationDispatcherMetadata newMetadata() { | |||
return NotificationDispatcherMetadata.create(KEY) | |||
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true)) | |||
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)); | |||
} | |||
@Override | |||
public String getKey() { | |||
return KEY; | |||
} | |||
@Override | |||
public void dispatch(Notification notification, Context context) { | |||
String projectKey = notification.getFieldValue("projectKey"); | |||
String assignee = notification.getFieldValue("assignee"); | |||
Multimap<String, NotificationChannel> subscribedRecipients = manager.findNotificationSubscribers(this, projectKey); | |||
Collection<NotificationChannel> channels = subscribedRecipients.get(assignee); | |||
for (NotificationChannel channel : channels) { | |||
context.addUser(assignee, channel); | |||
} | |||
} | |||
} |
@@ -19,171 +19,21 @@ | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.base.Objects; | |||
import com.google.common.collect.Lists; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.plugins.emailnotifications.api.EmailMessage; | |||
import org.sonar.plugins.emailnotifications.api.EmailTemplate; | |||
import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC; | |||
import org.sonar.server.user.index.UserDoc; | |||
import org.sonar.server.user.index.UserIndex; | |||
import java.io.UnsupportedEncodingException; | |||
import java.net.URLEncoder; | |||
import java.util.Date; | |||
import java.util.Iterator; | |||
import java.util.Locale; | |||
/** | |||
* Creates email message for notification "new-issues". | |||
*/ | |||
public class NewIssuesEmailTemplate extends EmailTemplate { | |||
public static final String FIELD_PROJECT_NAME = "projectName"; | |||
public static final String FIELD_PROJECT_KEY = "projectKey"; | |||
public static final String FIELD_PROJECT_DATE = "projectDate"; | |||
public static final String FIELD_PROJECT_UUID = "projectUuid"; | |||
private static final char NEW_LINE = '\n'; | |||
private static final String TAB = " "; | |||
private static final String DOT = "."; | |||
private static final String COUNT = DOT + "count"; | |||
private static final String LABEL = DOT + "label"; | |||
private final EmailSettings settings; | |||
private final I18n i18n; | |||
private final UserIndex userIndex; | |||
public class NewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate { | |||
public NewIssuesEmailTemplate(EmailSettings settings, I18n i18n, UserIndex userIndex) { | |||
this.settings = settings; | |||
this.i18n = i18n; | |||
this.userIndex = userIndex; | |||
} | |||
public static String encode(String toEncode) { | |||
try { | |||
return URLEncoder.encode(toEncode, "UTF-8"); | |||
} catch (UnsupportedEncodingException e) { | |||
throw new IllegalStateException("Encoding not supported", e); | |||
} | |||
public NewIssuesEmailTemplate(EmailSettings settings, I18n i18n) { | |||
super(settings, i18n); | |||
} | |||
@Override | |||
public EmailMessage format(Notification notification) { | |||
if (!NewIssuesNotification.TYPE.equals(notification.getType())) { | |||
return null; | |||
} | |||
String projectName = notification.getFieldValue(FIELD_PROJECT_NAME); | |||
StringBuilder message = new StringBuilder(); | |||
message.append("Project: ").append(projectName).append(NEW_LINE).append(NEW_LINE); | |||
appendSeverity(message, notification); | |||
appendAssignees(message, notification); | |||
appendTags(message, notification); | |||
appendComponents(message, notification); | |||
appendFooter(message, notification); | |||
return new EmailMessage() | |||
.setMessageId("new-issues/" + notification.getFieldValue(FIELD_PROJECT_KEY)) | |||
.setSubject(projectName + ": " + notification.getFieldValue(METRIC.SEVERITY + COUNT) + " new issues") | |||
.setMessage(message.toString()); | |||
} | |||
private void appendComponents(StringBuilder message, Notification notification) { | |||
if (notification.getFieldValue(METRIC.COMPONENT + ".1.label") == null) { | |||
return; | |||
} | |||
message.append(" Components:\n"); | |||
int i = 1; | |||
while (notification.getFieldValue(METRIC.COMPONENT + DOT + i + LABEL) != null && i <= 5) { | |||
String component = notification.getFieldValue(METRIC.COMPONENT + DOT + i + LABEL); | |||
message | |||
.append(TAB).append(TAB) | |||
.append(component) | |||
.append(" : ") | |||
.append(notification.getFieldValue(METRIC.COMPONENT + DOT + i + COUNT)) | |||
.append("\n"); | |||
i += 1; | |||
} | |||
protected boolean shouldNotFormat(Notification notification) { | |||
return !NewIssuesNotification.TYPE.equals(notification.getType()); | |||
} | |||
private void appendAssignees(StringBuilder message, Notification notification) { | |||
if (notification.getFieldValue(METRIC.LOGIN + DOT + "1" + LABEL) == null) { | |||
return; | |||
} | |||
message.append(TAB + "Assignee - "); | |||
int i = 1; | |||
while (notification.getFieldValue(METRIC.LOGIN + DOT + i + LABEL) != null && i <= 5) { | |||
String login = notification.getFieldValue(METRIC.LOGIN + DOT + i + LABEL); | |||
UserDoc user = userIndex.getNullableByLogin(login); | |||
String name = user == null ? null : user.name(); | |||
message.append(Objects.firstNonNull(name, login)) | |||
.append(": ") | |||
.append(notification.getFieldValue(METRIC.LOGIN + DOT + i + COUNT)); | |||
if (i < 5) { | |||
message.append(TAB); | |||
} | |||
i += 1; | |||
} | |||
message.append("\n"); | |||
} | |||
private void appendTags(StringBuilder message, Notification notification) { | |||
if (notification.getFieldValue(METRIC.TAGS + DOT + "1" + LABEL) == null) { | |||
return; | |||
} | |||
message.append(TAB + "Tags - "); | |||
int i = 1; | |||
while (notification.getFieldValue(METRIC.TAGS + DOT + i + LABEL) != null && i <= 5) { | |||
String tag = notification.getFieldValue(METRIC.TAGS + DOT + i + LABEL); | |||
message.append(tag) | |||
.append(": ") | |||
.append(notification.getFieldValue(METRIC.TAGS + DOT + i + COUNT)); | |||
if (i < 5) { | |||
message.append(TAB); | |||
} | |||
i += 1; | |||
} | |||
message.append(NEW_LINE); | |||
} | |||
private void appendSeverity(StringBuilder message, Notification notification) { | |||
message.append(notification.getFieldValue(METRIC.SEVERITY + COUNT)).append(" new issues - Total debt: ") | |||
.append(notification.getFieldValue(METRIC.DEBT + COUNT)) | |||
.append(NEW_LINE).append(NEW_LINE) | |||
.append(TAB + "Severity - "); | |||
for (Iterator<String> severityIterator = Lists.reverse(Severity.ALL).iterator(); severityIterator.hasNext();) { | |||
String severity = severityIterator.next(); | |||
String severityLabel = i18n.message(getLocale(), "severity." + severity, severity); | |||
message.append(severityLabel).append(": ").append(notification.getFieldValue(METRIC.SEVERITY + DOT + severity + COUNT)); | |||
if (severityIterator.hasNext()) { | |||
message.append(TAB); | |||
} | |||
} | |||
message.append(NEW_LINE); | |||
} | |||
private void appendFooter(StringBuilder message, Notification notification) { | |||
String projectUuid = notification.getFieldValue(FIELD_PROJECT_UUID); | |||
String dateString = notification.getFieldValue(FIELD_PROJECT_DATE); | |||
if (projectUuid != null && dateString != null) { | |||
Date date = DateUtils.parseDateTime(dateString); | |||
String url = String.format("%s/issues/search#projectUuids=%s|createdAt=%s", | |||
settings.getServerBaseURL(), encode(projectUuid), encode(DateUtils.formatDateTime(date))); | |||
message.append("\n").append("See it in SonarQube: ").append(url).append(NEW_LINE); | |||
} | |||
} | |||
private Locale getLocale() { | |||
return Locale.ENGLISH; | |||
} | |||
} |
@@ -24,8 +24,14 @@ import org.sonar.api.component.Component; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.Duration; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.core.component.ComponentDto; | |||
import org.sonar.core.persistence.DbSession; | |||
import org.sonar.server.db.DbClient; | |||
import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC; | |||
import org.sonar.server.user.index.UserDoc; | |||
import org.sonar.server.user.index.UserIndex; | |||
import java.util.Date; | |||
import java.util.List; | |||
@@ -35,11 +41,25 @@ import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.SEV | |||
public class NewIssuesNotification extends Notification { | |||
private static final long serialVersionUID = -6305871981920103093L; | |||
public static final String TYPE = "new-issues"; | |||
private static final String COUNT = ".count"; | |||
private static final String LABEL = ".label"; | |||
private final transient UserIndex userIndex; | |||
private final transient DbClient dbClient; | |||
private final transient Durations durations; | |||
NewIssuesNotification(UserIndex userIndex, DbClient dbClient, Durations durations) { | |||
this(TYPE, userIndex, dbClient, durations); | |||
} | |||
public NewIssuesNotification() { | |||
super(TYPE); | |||
protected NewIssuesNotification(String type, UserIndex userIndex, DbClient dbClient, Durations durations) { | |||
super(type); | |||
this.userIndex = userIndex; | |||
this.dbClient = dbClient; | |||
this.durations = durations; | |||
} | |||
public NewIssuesNotification setAnalysisDate(Date d) { | |||
@@ -54,31 +74,57 @@ public class NewIssuesNotification extends Notification { | |||
return this; | |||
} | |||
public NewIssuesNotification setStatistics(Component project, NewIssuesStatistics stats) { | |||
public NewIssuesNotification setStatistics(Component project, NewIssuesStatistics.Stats stats) { | |||
setDefaultMessage(stats.countForMetric(SEVERITY) + " new issues on " + project.longName() + ".\n"); | |||
setSeverityStatistics(stats); | |||
setTop5CountsForMetric(stats, METRIC.LOGIN); | |||
setTop5CountsForMetric(stats, METRIC.TAGS); | |||
setTop5CountsForMetric(stats, METRIC.COMPONENT); | |||
setAssigneesStatistics(stats); | |||
setTagsStatistics(stats); | |||
setComponentsStatistics(stats); | |||
return this; | |||
} | |||
public NewIssuesNotification setDebt(String debt) { | |||
setFieldValue(METRIC.DEBT + COUNT, debt); | |||
return this; | |||
protected void setComponentsStatistics(NewIssuesStatistics.Stats stats) { | |||
METRIC metric = METRIC.COMPONENT; | |||
List<Multiset.Entry<String>> componentStats = stats.statsForMetric(metric); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
for (int i = 0; i < 5 && i < componentStats.size(); i++) { | |||
String uuid = componentStats.get(i).getElement(); | |||
String componentName = dbClient.componentDao().getByUuid(dbSession, uuid).name(); | |||
setFieldValue(metric + "." + (i + 1) + LABEL, componentName); | |||
setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(componentStats.get(i).getCount())); | |||
} | |||
} | |||
} | |||
private void setTop5CountsForMetric(NewIssuesStatistics stats, METRIC metric) { | |||
List<Multiset.Entry<String>> loginStats = stats.statsForMetric(metric); | |||
for (int i = 0; i < 5 && i < loginStats.size(); i++) { | |||
setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(loginStats.get(i).getCount())); | |||
setFieldValue(metric + "." + (i + 1) + ".label", loginStats.get(i).getElement()); | |||
protected void setTagsStatistics(NewIssuesStatistics.Stats stats) { | |||
METRIC metric = METRIC.TAGS; | |||
List<Multiset.Entry<String>> metricStats = stats.statsForMetric(metric); | |||
for (int i = 0; i < 5 && i < metricStats.size(); i++) { | |||
setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(metricStats.get(i).getCount())); | |||
setFieldValue(metric + "." + (i + 1) + ".label", metricStats.get(i).getElement()); | |||
} | |||
} | |||
private void setSeverityStatistics(NewIssuesStatistics stats) { | |||
protected void setAssigneesStatistics(NewIssuesStatistics.Stats stats) { | |||
METRIC metric = METRIC.ASSIGNEE; | |||
List<Multiset.Entry<String>> metricStats = stats.statsForMetric(metric); | |||
for (int i = 0; i < 5 && i < metricStats.size(); i++) { | |||
String login = metricStats.get(i).getElement(); | |||
UserDoc user = userIndex.getNullableByLogin(login); | |||
String name = user == null ? login : user.name(); | |||
setFieldValue(metric + "." + (i + 1) + LABEL, name); | |||
setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(metricStats.get(i).getCount())); | |||
} | |||
} | |||
public NewIssuesNotification setDebt(Duration debt) { | |||
setFieldValue(METRIC.DEBT + COUNT, durations.encode(debt)); | |||
return this; | |||
} | |||
protected void setSeverityStatistics(NewIssuesStatistics.Stats stats) { | |||
setFieldValue(SEVERITY + COUNT, String.valueOf(stats.countForMetric(SEVERITY))); | |||
for (String severity : Severity.ALL) { | |||
setFieldValue(SEVERITY + "." + severity + COUNT, String.valueOf(stats.countForMetric(SEVERITY, severity))); |
@@ -0,0 +1,46 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import org.sonar.api.ServerComponent; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.server.db.DbClient; | |||
import org.sonar.server.user.index.UserIndex; | |||
public class NewIssuesNotificationFactory implements ServerComponent { | |||
private final UserIndex userIndex; | |||
private final DbClient dbClient; | |||
private final Durations durations; | |||
public NewIssuesNotificationFactory(UserIndex userIndex, DbClient dbClient, Durations durations) { | |||
this.userIndex = userIndex; | |||
this.dbClient = dbClient; | |||
this.durations = durations; | |||
} | |||
public MyNewIssuesNotification newMyNewIssuesNotification() { | |||
return new MyNewIssuesNotification(userIndex, dbClient, durations); | |||
} | |||
public NewIssuesNotification newNewIssuesNotication() { | |||
return new NewIssuesNotification(userIndex, dbClient, durations); | |||
} | |||
} |
@@ -27,7 +27,7 @@ import org.sonar.api.utils.Duration; | |||
import org.sonar.core.util.MultiSets; | |||
import java.util.EnumMap; | |||
import java.util.HashMap; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
@@ -35,58 +35,50 @@ import static com.google.common.base.Preconditions.checkArgument; | |||
import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*; | |||
public class NewIssuesStatistics { | |||
private Map<String, Stats> statisticsByLogin = new HashMap<>(); | |||
private Map<String, Stats> assigneesStatistics = new LinkedHashMap<>(); | |||
private Stats globalStatistics = new Stats(); | |||
public void add(Issue issue) { | |||
globalStatistics.add(issue); | |||
String login = issue.assignee(); | |||
if (login != null) { | |||
statisticsForLogin(login).add(issue); | |||
getOrCreate(login).add(issue); | |||
} | |||
} | |||
private Stats statisticsForLogin(String login) { | |||
if (statisticsByLogin.get(login) == null) { | |||
statisticsByLogin.put(login, new Stats()); | |||
private Stats getOrCreate(String assignee) { | |||
if (assigneesStatistics.get(assignee) == null) { | |||
assigneesStatistics.put(assignee, new Stats()); | |||
} | |||
return statisticsByLogin.get(login); | |||
return assigneesStatistics.get(assignee); | |||
} | |||
public int countForMetric(METRIC metric) { | |||
return globalStatistics.distributionFor(metric).size(); | |||
public Map<String, Stats> assigneesStatistics() { | |||
return assigneesStatistics; | |||
} | |||
public int countForMetric(METRIC metric, String label) { | |||
return globalStatistics.distributionFor(metric).count(label); | |||
} | |||
public List<Multiset.Entry<String>> statsForMetric(METRIC metric) { | |||
return MultiSets.listOrderedByHighestCounts(globalStatistics.distributionFor(metric)); | |||
} | |||
public Duration debt() { | |||
return globalStatistics.debt(); | |||
public Stats globalStatistics() { | |||
return globalStatistics; | |||
} | |||
public boolean hasIssues() { | |||
return globalStatistics.hasIssues(); | |||
} | |||
public enum METRIC { | |||
SEVERITY(true), TAGS(true), COMPONENT(true), LOGIN(true), DEBT(false); | |||
private final boolean computeDistribution; | |||
enum METRIC { | |||
SEVERITY(true), TAGS(true), COMPONENT(true), ASSIGNEE(true), DEBT(false); | |||
private final boolean isComputedByDistribution; | |||
METRIC(boolean computeDistribution) { | |||
this.computeDistribution = computeDistribution; | |||
METRIC(boolean isComputedByDistribution) { | |||
this.isComputedByDistribution = isComputedByDistribution; | |||
} | |||
boolean isComputedByDistribution() { | |||
return this.computeDistribution; | |||
return this.isComputedByDistribution; | |||
} | |||
} | |||
private static class Stats { | |||
public static class Stats { | |||
private final Map<METRIC, Multiset<String>> distributions = new EnumMap<>(METRIC.class); | |||
private long debtInMinutes = 0L; | |||
@@ -102,7 +94,7 @@ public class NewIssuesStatistics { | |||
distributions.get(SEVERITY).add(issue.severity()); | |||
distributions.get(COMPONENT).add(issue.componentUuid()); | |||
if (issue.assignee() != null) { | |||
distributions.get(LOGIN).add(issue.assignee()); | |||
distributions.get(ASSIGNEE).add(issue.assignee()); | |||
} | |||
for (String tag : issue.tags()) { | |||
distributions.get(TAGS).add(tag); | |||
@@ -113,9 +105,12 @@ public class NewIssuesStatistics { | |||
} | |||
} | |||
public Multiset<String> distributionFor(METRIC metric) { | |||
checkArgument(metric.isComputedByDistribution()); | |||
return distributions.get(metric); | |||
public int countForMetric(METRIC metric) { | |||
return distributionFor(metric).size(); | |||
} | |||
public int countForMetric(METRIC metric, String label) { | |||
return distributionFor(metric).count(label); | |||
} | |||
public Duration debt() { | |||
@@ -125,5 +120,14 @@ public class NewIssuesStatistics { | |||
public boolean hasIssues() { | |||
return distributions.get(SEVERITY) != null; | |||
} | |||
public List<Multiset.Entry<String>> statsForMetric(METRIC metric) { | |||
return MultiSets.listOrderedByHighestCounts(distributionFor(metric)); | |||
} | |||
private Multiset<String> distributionFor(METRIC metric) { | |||
checkArgument(metric.isComputedByDistribution()); | |||
return distributions.get(metric); | |||
} | |||
} | |||
} |
@@ -538,13 +538,17 @@ class ServerComponents { | |||
pico.addSingleton(IssueActionsWriter.class); | |||
pico.addSingleton(IssueQueryService.class); | |||
pico.addSingleton(NewIssuesEmailTemplate.class); | |||
pico.addSingleton(MyNewIssuesEmailTemplate.class); | |||
pico.addSingleton(IssueChangesEmailTemplate.class); | |||
pico.addSingleton(ChangesOnMyIssueNotificationDispatcher.class); | |||
pico.addSingleton(ChangesOnMyIssueNotificationDispatcher.newMetadata()); | |||
pico.addSingleton(NewIssuesNotificationDispatcher.class); | |||
pico.addSingleton(NewIssuesNotificationDispatcher.newMetadata()); | |||
pico.addSingleton(MyNewIssuesNotificationDispatcher.class); | |||
pico.addSingleton(MyNewIssuesNotificationDispatcher.newMetadata()); | |||
pico.addSingleton(DoNotFixNotificationDispatcher.class); | |||
pico.addSingleton(DoNotFixNotificationDispatcher.newMetadata()); | |||
pico.addSingleton(NewIssuesNotificationFactory.class); | |||
// issue filters | |||
pico.addSingleton(IssueFilterService.class); |
@@ -27,7 +27,6 @@ import org.mockito.Mockito; | |||
import org.sonar.api.issue.internal.DefaultIssue; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.batch.protocol.output.BatchReport; | |||
import org.sonar.server.computation.ComputationContext; | |||
@@ -35,6 +34,7 @@ import org.sonar.server.computation.issue.IssueCache; | |||
import org.sonar.server.computation.issue.RuleCache; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotificationFactory; | |||
import org.sonar.server.notifications.NotificationService; | |||
import java.io.IOException; | |||
@@ -51,13 +51,13 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
NotificationService notifService = mock(NotificationService.class); | |||
ComputationContext context = mock(ComputationContext.class, Mockito.RETURNS_DEEP_STUBS); | |||
IssueCache issueCache; | |||
Durations durations = mock(Durations.class); | |||
NewIssuesNotificationFactory newIssuesNotificationFactory = mock(NewIssuesNotificationFactory.class, Mockito.RETURNS_DEEP_STUBS); | |||
SendIssueNotificationsStep sut; | |||
@Before | |||
public void setUp() throws Exception { | |||
issueCache = new IssueCache(temp.newFile(), System2.INSTANCE); | |||
sut = new SendIssueNotificationsStep(issueCache, ruleCache, notifService, durations); | |||
sut = new SendIssueNotificationsStep(issueCache, ruleCache, notifService, newIssuesNotificationFactory); | |||
} | |||
@Test |
@@ -0,0 +1,165 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.base.Charsets; | |||
import org.apache.commons.io.IOUtils; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.mockito.invocation.InvocationOnMock; | |||
import org.mockito.stubbing.Answer; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.core.i18n.DefaultI18n; | |||
import org.sonar.plugins.emailnotifications.api.EmailMessage; | |||
import org.sonar.server.user.index.UserDoc; | |||
import org.sonar.server.user.index.UserIndex; | |||
import java.util.Date; | |||
import java.util.Locale; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Matchers.*; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*; | |||
public class MyNewIssuesEmailTemplateTest { | |||
MyNewIssuesEmailTemplate sut; | |||
DefaultI18n i18n; | |||
UserIndex userIndex; | |||
Date date; | |||
@Before | |||
public void setUp() { | |||
EmailSettings settings = mock(EmailSettings.class); | |||
when(settings.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); | |||
i18n = mock(DefaultI18n.class); | |||
date = new Date(); | |||
userIndex = mock(UserIndex.class); | |||
// returns the login passed in parameter | |||
when(userIndex.getNullableByLogin(anyString())).thenAnswer(new Answer<UserDoc>() { | |||
@Override | |||
public UserDoc answer(InvocationOnMock invocationOnMock) throws Throwable { | |||
return new UserDoc().setName((String) invocationOnMock.getArguments()[0]); | |||
} | |||
}); | |||
when(i18n.message(any(Locale.class), eq("severity.BLOCKER"), anyString())).thenReturn("Blocker"); | |||
when(i18n.message(any(Locale.class), eq("severity.CRITICAL"), anyString())).thenReturn("Critical"); | |||
when(i18n.message(any(Locale.class), eq("severity.MAJOR"), anyString())).thenReturn("Major"); | |||
when(i18n.message(any(Locale.class), eq("severity.MINOR"), anyString())).thenReturn("Minor"); | |||
when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info"); | |||
sut = new MyNewIssuesEmailTemplate(settings, i18n); | |||
} | |||
@Test | |||
public void no_format_if_not_the_correct_notif() { | |||
Notification notification = new Notification("new-issues"); | |||
EmailMessage message = sut.format(notification); | |||
assertThat(message).isNull(); | |||
} | |||
@Test | |||
public void format_email_with_all_fields_filled() throws Exception { | |||
Notification notification = newNotification(); | |||
addTags(notification); | |||
addComponents(notification); | |||
EmailMessage message = sut.format(notification); | |||
// TODO datetime to be completed when test is isolated from JVM timezone | |||
String file = IOUtils.toString(getClass().getResource("MyNewIssuesEmailTemplateTest/email_with_all_details.txt"), Charsets.UTF_8); | |||
assertThat(message.getMessage()).startsWith(file); | |||
} | |||
@Test | |||
public void message_id() throws Exception { | |||
Notification notification = newNotification(); | |||
EmailMessage message = sut.format(notification); | |||
assertThat(message.getMessageId()).isEqualTo("my-new-issues/org.apache:struts"); | |||
} | |||
@Test | |||
public void subject() throws Exception { | |||
Notification notification = newNotification(); | |||
EmailMessage message = sut.format(notification); | |||
assertThat(message.getSubject()).isEqualTo("You have 32 new issues on project Struts"); | |||
} | |||
@Test | |||
public void format_email_with_no_assignees_tags_nor_components() throws Exception { | |||
Notification notification = newNotification(); | |||
EmailMessage message = sut.format(notification); | |||
// TODO datetime to be completed when test is isolated from JVM timezone | |||
String file = IOUtils.toString(getClass().getResource("MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt"), Charsets.UTF_8); | |||
assertThat(message.getMessage()).startsWith(file); | |||
} | |||
@Test | |||
public void do_not_add_footer_when_properties_missing() { | |||
Notification notification = new Notification(MyNewIssuesNotification.TYPE) | |||
.setFieldValue(SEVERITY + ".count", "32") | |||
.setFieldValue("projectName", "Struts"); | |||
EmailMessage message = sut.format(notification); | |||
assertThat(message.getMessage()).doesNotContain("See it"); | |||
} | |||
private Notification newNotification() { | |||
return new Notification(MyNewIssuesNotification.TYPE) | |||
.setFieldValue("projectName", "Struts") | |||
.setFieldValue("projectKey", "org.apache:struts") | |||
.setFieldValue("projectUuid", "ABCDE") | |||
.setFieldValue("projectDate", "2010-05-18T14:50:45+0000") | |||
.setFieldValue("assignee", "lo.gin") | |||
.setFieldValue(DEBT + ".count", "1d3h") | |||
.setFieldValue(SEVERITY + ".count", "32") | |||
.setFieldValue(SEVERITY + ".INFO.count", "1") | |||
.setFieldValue(SEVERITY + ".MINOR.count", "3") | |||
.setFieldValue(SEVERITY + ".MAJOR.count", "10") | |||
.setFieldValue(SEVERITY + ".CRITICAL.count", "5") | |||
.setFieldValue(SEVERITY + ".BLOCKER.count", "0"); | |||
} | |||
private void addTags(Notification notification) { | |||
notification | |||
.setFieldValue(TAGS + ".1.label", "oscar") | |||
.setFieldValue(TAGS + ".1.count", "3") | |||
.setFieldValue(TAGS + ".2.label", "cesar") | |||
.setFieldValue(TAGS + ".2.count", "10"); | |||
} | |||
private void addComponents(Notification notification) { | |||
notification | |||
.setFieldValue(COMPONENT + ".1.label", "/path/to/file") | |||
.setFieldValue(COMPONENT + ".1.count", "3") | |||
.setFieldValue(COMPONENT + ".2.label", "/path/to/directory") | |||
.setFieldValue(COMPONENT + ".2.count", "7"); | |||
} | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.collect.HashMultimap; | |||
import com.google.common.collect.Multimap; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.notifications.NotificationChannel; | |||
import org.sonar.api.notifications.NotificationDispatcher; | |||
import org.sonar.api.notifications.NotificationManager; | |||
import static org.mockito.Matchers.any; | |||
import static org.mockito.Mockito.*; | |||
public class MyNewIssuesNotificationDispatcherTest { | |||
private MyNewIssuesNotificationDispatcher sut; | |||
private NotificationManager notificationManager = mock(NotificationManager.class); | |||
private NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class); | |||
private NotificationChannel emailChannel = mock(NotificationChannel.class); | |||
private NotificationChannel twitterChannel = mock(NotificationChannel.class); | |||
@Before | |||
public void setUp() { | |||
sut = new MyNewIssuesNotificationDispatcher(notificationManager); | |||
} | |||
@Test | |||
public void do_not_dispatch_if_no_new_notification() throws Exception { | |||
Notification notification = new Notification("other-notif"); | |||
sut.performDispatch(notification, context); | |||
verify(context, never()).addUser(any(String.class), any(NotificationChannel.class)); | |||
} | |||
@Test | |||
public void dispatch_to_users_who_have_subscribed_to_notification_and_project() { | |||
Multimap<String, NotificationChannel> recipients = HashMultimap.create(); | |||
recipients.put("user1", emailChannel); | |||
recipients.put("user2", twitterChannel); | |||
when(notificationManager.findNotificationSubscribers(sut, "struts")).thenReturn(recipients); | |||
Notification notification = new Notification(MyNewIssuesNotification.TYPE) | |||
.setFieldValue("projectKey", "struts") | |||
.setFieldValue("assignee", "user1"); | |||
sut.performDispatch(notification, context); | |||
verify(context).addUser("user1", emailChannel); | |||
verifyNoMoreInteractions(context); | |||
} | |||
} |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import org.junit.Test; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.server.db.DbClient; | |||
import org.sonar.server.user.index.UserIndex; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_ASSIGNEE; | |||
public class MyNewIssuesNotificationTest { | |||
MyNewIssuesNotification sut = new MyNewIssuesNotification(mock(UserIndex.class), mock(DbClient.class), mock(Durations.class)); | |||
@Test | |||
public void set_assignee() throws Exception { | |||
sut.setAssignee("myAssignee"); | |||
assertThat(sut.getFieldValue(FIELD_ASSIGNEE)).isEqualTo("myAssignee"); | |||
} | |||
@Test | |||
public void set_with_a_specific_type() throws Exception { | |||
assertThat(sut.getType()).isEqualTo(MyNewIssuesNotification.TYPE); | |||
} | |||
} |
@@ -19,6 +19,8 @@ | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.base.Charsets; | |||
import org.apache.commons.io.IOUtils; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.mockito.invocation.InvocationOnMock; | |||
@@ -40,16 +42,6 @@ import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*; | |||
public class NewIssuesEmailTemplateTest { | |||
private static final String EMAIL_HEADER = "Project: Struts\n\n"; | |||
private static final String EMAIL_TOTAL_ISSUES = "32 new issues - Total debt: 1d3h\n\n"; | |||
private static final String EMAIL_ISSUES = " Severity - Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1\n"; | |||
private static final String EMAIL_ASSIGNEES = " Assignee - robin.williams: 5 al.pacino: 7 \n"; | |||
private static final String EMAIL_TAGS = " Tags - oscar: 3 cesar: 10 \n"; | |||
private static final String EMAIL_COMPONENTS = " Components:\n" + | |||
" /path/to/file : 3\n" + | |||
" /path/to/directory : 7\n"; | |||
private static final String EMAIL_FOOTER = "\nSee it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1"; | |||
NewIssuesEmailTemplate template; | |||
DefaultI18n i18n; | |||
UserIndex userIndex; | |||
@@ -73,36 +65,36 @@ public class NewIssuesEmailTemplateTest { | |||
when(i18n.message(any(Locale.class), eq("severity.MINOR"), anyString())).thenReturn("Minor"); | |||
when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info"); | |||
template = new NewIssuesEmailTemplate(settings, i18n, userIndex); | |||
template = new NewIssuesEmailTemplate(settings, i18n); | |||
} | |||
@Test | |||
public void shouldNotFormatIfNotCorrectNotification() { | |||
Notification notification = new Notification("other-notif"); | |||
public void no_format_is_not_the_correct_notification() { | |||
Notification notification = new Notification("my-new-issues"); | |||
EmailMessage message = template.format(notification); | |||
assertThat(message).isNull(); | |||
} | |||
/** | |||
* <pre> | |||
* Subject: Project Struts, new issues | |||
* From: Sonar | |||
* | |||
* Project: Foo | |||
* 32 new issues - Total debt: 1d3h | |||
* | |||
* Severity - Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 | |||
* Assignee - robin.williams: 5 al.pacino: 7 | |||
* Tags - oscar: 3 cesar:10 | |||
* Components: | |||
* /path/to/file : 3 | |||
* /path/to/directoy : 7 | |||
* | |||
* See it in SonarQube: http://nemo.sonarsource.org/drilldown/measures/org.sonar.foo:foo?metric=new_violations | |||
* </pre> | |||
*/ | |||
@Test | |||
public void format_email_with_all_fields_filled() { | |||
public void message_id() throws Exception { | |||
Notification notification = newNotification(); | |||
EmailMessage message = template.format(notification); | |||
assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts"); | |||
} | |||
@Test | |||
public void subject() throws Exception { | |||
Notification notification = newNotification(); | |||
EmailMessage message = template.format(notification); | |||
assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues (new debt: 1d3h)"); | |||
} | |||
@Test | |||
public void format_email_with_all_fields_filled() throws Exception { | |||
Notification notification = newNotification(); | |||
addAssignees(notification); | |||
addTags(notification); | |||
@@ -110,55 +102,35 @@ public class NewIssuesEmailTemplateTest { | |||
EmailMessage message = template.format(notification); | |||
assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts"); | |||
assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues"); | |||
// TODO datetime to be completed when test is isolated from JVM timezone | |||
assertThat(message.getMessage()).startsWith("" + | |||
EMAIL_HEADER + | |||
EMAIL_TOTAL_ISSUES + | |||
EMAIL_ISSUES + | |||
EMAIL_ASSIGNEES + | |||
EMAIL_TAGS + | |||
EMAIL_COMPONENTS + | |||
EMAIL_FOOTER); | |||
String expectedContent = IOUtils.toString(getClass().getResource("NewIssuesEmailTemplateTest/email_with_all_details.txt"), Charsets.UTF_8); | |||
assertThat(message.getMessage()).startsWith(expectedContent); | |||
} | |||
@Test | |||
public void format_email_with_no_assignees_tags_nor_components() throws Exception { | |||
Notification notification = newNotification(); | |||
when(i18n.message(any(Locale.class), eq("severity.BLOCKER"), anyString())).thenReturn("Blocker"); | |||
when(i18n.message(any(Locale.class), eq("severity.CRITICAL"), anyString())).thenReturn("Critical"); | |||
when(i18n.message(any(Locale.class), eq("severity.MAJOR"), anyString())).thenReturn("Major"); | |||
when(i18n.message(any(Locale.class), eq("severity.MINOR"), anyString())).thenReturn("Minor"); | |||
when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info"); | |||
EmailMessage message = template.format(notification); | |||
assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts"); | |||
assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues"); | |||
// TODO datetime to be completed when test is isolated from JVM timezone | |||
assertThat(message.getMessage()).startsWith("" + | |||
EMAIL_HEADER + | |||
EMAIL_TOTAL_ISSUES + | |||
EMAIL_ISSUES + | |||
EMAIL_FOOTER); | |||
String expectedContent = IOUtils.toString(getClass().getResource("NewIssuesEmailTemplateTest/email_with_partial_details.txt"), Charsets.UTF_8); | |||
assertThat(message.getMessage()).startsWith(expectedContent); | |||
} | |||
@Test | |||
public void do_not_add_footer_when_properties_missing() { | |||
Notification notification = new NewIssuesNotification() | |||
Notification notification = new Notification(NewIssuesNotification.TYPE) | |||
.setFieldValue(SEVERITY + ".count", "32") | |||
.setFieldValue("projectName", "Struts"); | |||
EmailMessage message = template.format(notification); | |||
assertThat(message.getMessage()).doesNotContain("See it"); | |||
} | |||
private Notification newNotification() { | |||
return new NewIssuesNotification() | |||
return new Notification(NewIssuesNotification.TYPE) | |||
.setFieldValue("projectName", "Struts") | |||
.setFieldValue("projectKey", "org.apache:struts") | |||
.setFieldValue("projectUuid", "ABCDE") | |||
@@ -174,10 +146,10 @@ public class NewIssuesEmailTemplateTest { | |||
private void addAssignees(Notification notification) { | |||
notification | |||
.setFieldValue(LOGIN + ".1.label", "robin.williams") | |||
.setFieldValue(LOGIN + ".1.count", "5") | |||
.setFieldValue(LOGIN + ".2.label", "al.pacino") | |||
.setFieldValue(LOGIN + ".2.count", "7"); | |||
.setFieldValue(ASSIGNEE + ".1.label", "robin.williams") | |||
.setFieldValue(ASSIGNEE + ".1.count", "5") | |||
.setFieldValue(ASSIGNEE + ".2.label", "al.pacino") | |||
.setFieldValue(ASSIGNEE + ".2.count", "7"); | |||
} | |||
private void addTags(Notification notification) { |
@@ -23,8 +23,6 @@ import com.google.common.collect.HashMultimap; | |||
import com.google.common.collect.Multimap; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.mockito.Mock; | |||
import org.mockito.MockitoAnnotations; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.notifications.NotificationChannel; | |||
import org.sonar.api.notifications.NotificationDispatcher; | |||
@@ -35,23 +33,14 @@ import static org.mockito.Mockito.*; | |||
public class NewIssuesNotificationDispatcherTest { | |||
@Mock | |||
private NotificationManager notifications; | |||
@Mock | |||
private NotificationDispatcher.Context context; | |||
@Mock | |||
private NotificationChannel emailChannel; | |||
@Mock | |||
private NotificationChannel twitterChannel; | |||
private NewIssuesNotificationDispatcher dispatcher; | |||
private NotificationManager notifications = mock(NotificationManager.class); | |||
private NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class); | |||
private NotificationChannel emailChannel = mock(NotificationChannel.class); | |||
private NotificationChannel twitterChannel = mock(NotificationChannel.class); | |||
private NewIssuesNotificationDispatcher dispatcher = mock(NewIssuesNotificationDispatcher.class); | |||
@Before | |||
public void init() { | |||
MockitoAnnotations.initMocks(this); | |||
public void setUp() { | |||
dispatcher = new NewIssuesNotificationDispatcher(notifications); | |||
} | |||
@@ -70,7 +59,7 @@ public class NewIssuesNotificationDispatcherTest { | |||
recipients.put("user2", twitterChannel); | |||
when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients); | |||
Notification notification = new NewIssuesNotification().setFieldValue("projectKey", "struts"); | |||
Notification notification = new Notification(NewIssuesNotification.TYPE).setFieldValue("projectKey", "struts"); | |||
dispatcher.performDispatch(notification, context); | |||
verify(context).addUser("user1", emailChannel); |
@@ -22,22 +22,34 @@ package org.sonar.server.issue.notification; | |||
import com.google.common.collect.Lists; | |||
import org.junit.Test; | |||
import org.mockito.Mockito; | |||
import org.sonar.api.issue.internal.DefaultIssue; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.Duration; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.core.component.ComponentDto; | |||
import org.sonar.core.persistence.DbSession; | |||
import org.sonar.server.component.ComponentTesting; | |||
import org.sonar.server.db.DbClient; | |||
import org.sonar.server.user.index.UserIndex; | |||
import java.util.Date; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Matchers.any; | |||
import static org.mockito.Matchers.eq; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*; | |||
public class NewIssuesNotificationTest { | |||
NewIssuesNotification sut = new NewIssuesNotification(); | |||
NewIssuesStatistics stats = new NewIssuesStatistics(); | |||
NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats(); | |||
UserIndex userIndex = mock(UserIndex.class); | |||
DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS); | |||
Durations durations = mock(Durations.class); | |||
NewIssuesNotification sut = new NewIssuesNotification(userIndex, dbClient, durations); | |||
@Test | |||
public void set_project() throws Exception { | |||
@@ -68,29 +80,33 @@ public class NewIssuesNotificationTest { | |||
.setLongName("project-long-name"); | |||
addIssueNTimes(newIssue1(), 5); | |||
addIssueNTimes(newIssue2(), 3); | |||
when(dbClient.componentDao().getByUuid(any(DbSession.class), eq("file-uuid")).name()).thenReturn("file-name"); | |||
when(dbClient.componentDao().getByUuid(any(DbSession.class), eq("directory-uuid")).name()).thenReturn("directory-name"); | |||
sut.setStatistics(component, stats); | |||
assertThat(sut.getFieldValue(SEVERITY + ".INFO.count")).isEqualTo("5"); | |||
assertThat(sut.getFieldValue(SEVERITY + ".BLOCKER.count")).isEqualTo("3"); | |||
assertThat(sut.getFieldValue(LOGIN + ".1.label")).isEqualTo("maynard"); | |||
assertThat(sut.getFieldValue(LOGIN + ".1.count")).isEqualTo("5"); | |||
assertThat(sut.getFieldValue(LOGIN + ".2.label")).isEqualTo("keenan"); | |||
assertThat(sut.getFieldValue(LOGIN + ".2.count")).isEqualTo("3"); | |||
assertThat(sut.getFieldValue(ASSIGNEE + ".1.label")).isEqualTo("maynard"); | |||
assertThat(sut.getFieldValue(ASSIGNEE + ".1.count")).isEqualTo("5"); | |||
assertThat(sut.getFieldValue(ASSIGNEE + ".2.label")).isEqualTo("keenan"); | |||
assertThat(sut.getFieldValue(ASSIGNEE + ".2.count")).isEqualTo("3"); | |||
assertThat(sut.getFieldValue(TAGS + ".1.label")).isEqualTo("owasp"); | |||
assertThat(sut.getFieldValue(TAGS + ".1.count")).isEqualTo("8"); | |||
assertThat(sut.getFieldValue(TAGS + ".2.label")).isEqualTo("bug"); | |||
assertThat(sut.getFieldValue(TAGS + ".2.count")).isEqualTo("5"); | |||
assertThat(sut.getFieldValue(COMPONENT + ".1.label")).isEqualTo("file-uuid"); | |||
assertThat(sut.getFieldValue(COMPONENT + ".1.label")).isEqualTo("file-name"); | |||
assertThat(sut.getFieldValue(COMPONENT + ".1.count")).isEqualTo("5"); | |||
assertThat(sut.getFieldValue(COMPONENT + ".2.label")).isEqualTo("directory-uuid"); | |||
assertThat(sut.getFieldValue(COMPONENT + ".2.label")).isEqualTo("directory-name"); | |||
assertThat(sut.getFieldValue(COMPONENT + ".2.count")).isEqualTo("3"); | |||
assertThat(sut.getDefaultMessage()).startsWith("8 new issues on project-long-name"); | |||
} | |||
@Test | |||
public void set_debt() throws Exception { | |||
sut.setDebt("55min"); | |||
when(durations.encode(any(Duration.class))).thenReturn("55min"); | |||
sut.setDebt(Duration.create(55)); | |||
assertThat(sut.getFieldValue(DEBT + ".count")).isEqualTo("55min"); | |||
} |
@@ -41,21 +41,21 @@ public class NewIssuesStatisticsTest { | |||
sut.add(issue.setAssignee("james")); | |||
sut.add(issue.setAssignee("keenan")); | |||
assertThat(countDistribution(METRIC.LOGIN, "maynard")).isEqualTo(1); | |||
assertThat(countDistribution(METRIC.LOGIN, "james")).isEqualTo(1); | |||
assertThat(countDistribution(METRIC.LOGIN, "keenan")).isEqualTo(1); | |||
assertThat(countDistribution(METRIC.LOGIN, "wrong.login")).isEqualTo(0); | |||
assertThat(countDistribution(METRIC.ASSIGNEE, "maynard")).isEqualTo(1); | |||
assertThat(countDistribution(METRIC.ASSIGNEE, "james")).isEqualTo(1); | |||
assertThat(countDistribution(METRIC.ASSIGNEE, "keenan")).isEqualTo(1); | |||
assertThat(countDistribution(METRIC.ASSIGNEE, "wrong.login")).isEqualTo(0); | |||
assertThat(countDistribution(METRIC.COMPONENT, "file-uuid")).isEqualTo(3); | |||
assertThat(countDistribution(METRIC.COMPONENT, "wrong-uuid")).isEqualTo(0); | |||
assertThat(countDistribution(METRIC.SEVERITY, Severity.INFO)).isEqualTo(3); | |||
assertThat(countDistribution(METRIC.SEVERITY, Severity.CRITICAL)).isEqualTo(0); | |||
assertThat(countDistribution(METRIC.TAGS, "owasp")).isEqualTo(3); | |||
assertThat(countDistribution(METRIC.TAGS, "wrong-tag")).isEqualTo(0); | |||
assertThat(sut.debt().toMinutes()).isEqualTo(15L); | |||
assertThat(sut.globalStatistics().debt().toMinutes()).isEqualTo(15L); | |||
} | |||
private int countDistribution(METRIC metric, String label) { | |||
return sut.countForMetric(metric, label); | |||
return sut.globalStatistics().countForMetric(metric, label); | |||
} | |||
private DefaultIssue defaultIssue() { |
@@ -0,0 +1,16 @@ | |||
Project: Struts | |||
32 new issues (new debt: 1d3h) | |||
Severity | |||
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 | |||
Tags | |||
oscar: 3 | |||
cesar: 10 | |||
Most impacted files | |||
/path/to/file: 3 | |||
/path/to/directory: 7 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1 |
@@ -0,0 +1,8 @@ | |||
Project: Struts | |||
32 new issues (new debt: 1d3h) | |||
Severity | |||
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1 |
@@ -0,0 +1,20 @@ | |||
Project: Struts | |||
32 new issues (new debt: 1d3h) | |||
Severity | |||
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 | |||
Assignees | |||
robin.williams: 5 | |||
al.pacino: 7 | |||
Tags | |||
oscar: 3 | |||
cesar: 10 | |||
Most impacted files | |||
/path/to/file: 3 | |||
/path/to/directory: 7 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1 |
@@ -0,0 +1,8 @@ | |||
Project: Struts | |||
32 new issues (new debt: 1d3h) | |||
Severity | |||
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1 | |||
See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1 |
@@ -2040,6 +2040,7 @@ notification.dispatcher.ChangesOnMyIssue=Changes in issues assigned to me or rep | |||
notification.dispatcher.NewIssues=New issues | |||
notification.dispatcher.NewAlerts=New quality gate status | |||
notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive or won't fix | |||
notification.dispatcher.MyNewIssues=My New Issues | |||
#------------------------------------------------------------------------------ |