Parcourir la source

SONAR-4834 Create a changelog formatter on java side to manage display of changelog in order to integrate more easily technical debt in changelog

tags/4.1-RC1
Julien Lancelot il y a 10 ans
Parent
révision
f0f5058c48

+ 17
- 3
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties Voir le fichier

@@ -535,15 +535,29 @@ issue.manual.no_rules.non_admin=At least one manual rule must exist before manua
issue.reported_by=Reported by
issue.authorLogin=Author:
issue.component_deleted=Removed
issue.changelog.changed_to={0} changed to {1}
issue.changelog.was=was {0}
issue.changelog.removed={0} removed
issue.technical_debt=Technical debt:
issue.technical_debt.x_days={0} days
issue.technical_debt.x_hours={0} hours
issue.technical_debt.x_minutes={0} minutes


#------------------------------------------------------------------------------
#
# ISSUE CHANGELOG
#
#------------------------------------------------------------------------------
issue.changelog.changed_to={0} changed to {1}
issue.changelog.was=was {0}
issue.changelog.removed={0} removed
issue.changelog.field.severity=Severity
issue.changelog.field.actionPlan=Action Plan
issue.changelog.field.assignee=Assignee
issue.changelog.field.author=Author
issue.changelog.field.resolution=Resolution
issue.changelog.field.technicalDebt=Technical Debt
issue.changelog.field.status=Status


#------------------------------------------------------------------------------
#
# ISSUE FILTERS

+ 2
- 0
sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java Voir le fichier

@@ -48,6 +48,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
public static final String STATUS = "status";
public static final String AUTHOR = "author";
public static final String ACTION_PLAN = "actionPlan";
public static final String TECHNICAL_DEBT = "technicalDebt";

public boolean setSeverity(DefaultIssue issue, String severity, IssueChangeContext context) {
if (issue.manualSeverity()) {
@@ -203,6 +204,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
TechnicalDebt oldValue = issue.technicalDebt();
if (!Objects.equal(value, oldValue)) {
issue.setTechnicalDebt(value);
issue.setFieldChange(context, TECHNICAL_DEBT, oldValue.toLong(), value.toLong());
issue.setUpdateDate(context.date());
issue.setChanged(true);
return true;

+ 7
- 0
sonar-core/src/main/java/org/sonar/core/technicaldebt/TechnicalDebtConverter.java Voir le fichier

@@ -65,6 +65,13 @@ public class TechnicalDebtConverter implements BatchComponent, ServerComponent {
}
}

public double toDays(TechnicalDebt technicalDebt) {
double resultDays = technicalDebt.days();
resultDays += Double.valueOf(technicalDebt.hours()) / hoursInDay;
resultDays += Double.valueOf(technicalDebt.minutes()) / (hoursInDay * 60.0);
return resultDays;
}

public TechnicalDebt fromMinutes(Long inMinutes){
int oneHourInMinute = 60;
int days = 0;

+ 4
- 1
sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java Voir le fichier

@@ -371,8 +371,11 @@ public class IssueUpdaterTest {
boolean updated = updater.setPastTechnicalDebt(issue, previousDebt, context);
assertThat(updated).isTrue();
assertThat(issue.technicalDebt()).isEqualTo(TechnicalDebt.of(15, 0, 0));

assertThat(issue.mustSendNotifications()).isFalse();

FieldDiffs.Diff diff = issue.currentChange().get(TECHNICAL_DEBT);
assertThat(diff.oldValue()).isEqualTo(TechnicalDebt.of(10, 0, 0).toLong());
assertThat(diff.newValue()).isEqualTo(TechnicalDebt.of(15, 0, 0).toLong());
}

@Test

+ 23
- 14
sonar-core/src/test/java/org/sonar/core/technicaldebt/TechnicalDebtConverterTest.java Voir le fichier

@@ -19,14 +19,13 @@
*/
package org.sonar.core.technicaldebt;

import org.fest.assertions.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.config.Settings;
import org.sonar.api.technicaldebt.TechnicalDebt;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.fest.assertions.Assertions.assertThat;

public class TechnicalDebtConverterTest {

@@ -41,17 +40,17 @@ public class TechnicalDebtConverterTest {
}

@Test
public void convert_to_days() {
assertThat(converter.toDays(WorkUnit.create(6.0, WorkUnit.DAYS)), is(6.0));
assertThat(converter.toDays(WorkUnit.create(6.0, WorkUnit.HOURS)), is(6.0 / 12.0));
assertThat(converter.toDays(WorkUnit.create(60.0, WorkUnit.MINUTES)), is(1.0 / 12.0));
public void convert_work_unit_to_days() {
assertThat(converter.toDays(WorkUnit.create(6.0, WorkUnit.DAYS))).isEqualTo(6.0);
assertThat(converter.toDays(WorkUnit.create(6.0, WorkUnit.HOURS))).isEqualTo(6.0 / 12.0);
assertThat(converter.toDays(WorkUnit.create(60.0, WorkUnit.MINUTES))).isEqualTo(1.0 / 12.0);
}

@Test
public void concert_to_minutes() {
assertThat(converter.toMinutes(WorkUnit.create(2.0, WorkUnit.DAYS)), is(2 * 12 * 60L));
assertThat(converter.toMinutes(WorkUnit.create(6.0, WorkUnit.HOURS)), is(6 * 60L));
assertThat(converter.toMinutes(WorkUnit.create(60.0, WorkUnit.MINUTES)), is(60L));
public void concert_work_unit_to_minutes() {
assertThat(converter.toMinutes(WorkUnit.create(2.0, WorkUnit.DAYS))).isEqualTo(2 * 12 * 60L);
assertThat(converter.toMinutes(WorkUnit.create(6.0, WorkUnit.HOURS))).isEqualTo(6 * 60L);
assertThat(converter.toMinutes(WorkUnit.create(60.0, WorkUnit.MINUTES))).isEqualTo(60L);
}

@Test
@@ -68,10 +67,20 @@ public class TechnicalDebtConverterTest {
checkValues(converter.fromMinutes(790L), 10L, 1L, 1L);
}

@Test
public void convert_technical_debt_to_days() {
assertThat(converter.toDays(TechnicalDebt.of(0, 0, 6))).isEqualTo(6.0);
assertThat(converter.toDays(TechnicalDebt.of(0, 6, 0))).isEqualTo(0.5);
assertThat(converter.toDays(TechnicalDebt.of(360, 0, 0))).isEqualTo(0.5);
assertThat(converter.toDays(TechnicalDebt.of(45, 0, 0))).isEqualTo(0.0625);

assertThat(converter.toDays(TechnicalDebt.of(45, 6, 1))).isEqualTo(1.5625);
}
private void checkValues(TechnicalDebt technicalDebt, Long expectedMinutes, Long expectedHours, Long expectedDays) {
Assertions.assertThat(technicalDebt.minutes()).isEqualTo(expectedMinutes);
Assertions.assertThat(technicalDebt.hours()).isEqualTo(expectedHours);
Assertions.assertThat(technicalDebt.days()).isEqualTo(expectedDays);
assertThat(technicalDebt.minutes()).isEqualTo(expectedMinutes);
assertThat(technicalDebt.hours()).isEqualTo(expectedHours);
assertThat(technicalDebt.days()).isEqualTo(expectedDays);
}

}

+ 8
- 1
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java Voir le fichier

@@ -32,6 +32,7 @@ import org.sonar.api.issue.IssueComment;
import org.sonar.api.issue.IssueQuery;
import org.sonar.api.issue.action.Action;
import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.issue.internal.FieldDiffs;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;
import org.sonar.api.utils.SonarException;
@@ -81,12 +82,13 @@ public class InternalRubyIssueService implements ServerComponent {
private final ActionService actionService;
private final IssueFilterService issueFilterService;
private final IssueBulkChangeService issueBulkChangeService;
private final IssueChangelogFormatter issueChangelogFormatter;

public InternalRubyIssueService(IssueService issueService,
IssueCommentService commentService,
IssueChangelogService changelogService, ActionPlanService actionPlanService,
IssueStatsFinder issueStatsFinder, ResourceDao resourceDao, ActionService actionService,
IssueFilterService issueFilterService, IssueBulkChangeService issueBulkChangeService) {
IssueFilterService issueFilterService, IssueBulkChangeService issueBulkChangeService, IssueChangelogFormatter issueChangelogFormatter) {
this.issueService = issueService;
this.commentService = commentService;
this.changelogService = changelogService;
@@ -96,6 +98,7 @@ public class InternalRubyIssueService implements ServerComponent {
this.actionService = actionService;
this.issueFilterService = issueFilterService;
this.issueBulkChangeService = issueBulkChangeService;
this.issueChangelogFormatter = issueChangelogFormatter;
}

public IssueStatsFinder.IssueStatsResult findIssueAssignees(Map<String, Object> params) {
@@ -126,6 +129,10 @@ public class InternalRubyIssueService implements ServerComponent {
return changelogService.changelog(issue);
}

public List<String> formatChangelog(FieldDiffs diffs){
return issueChangelogFormatter.format(UserSession.get().locale(), diffs);
}

public Result<Issue> doTransition(String issueKey, String transitionKey) {
Result<Issue> result = Result.of();
try {

+ 61
- 0
sonar-server/src/main/java/org/sonar/server/issue/IssueChangelogFormatter.java Voir le fichier

@@ -0,0 +1,61 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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;

import org.sonar.api.ServerComponent;
import org.sonar.api.issue.internal.FieldDiffs;
import org.sonar.core.i18n.I18nManager;

import java.util.List;
import java.util.Locale;
import java.util.Map;

import static com.google.common.collect.Lists.newArrayList;

public class IssueChangelogFormatter implements ServerComponent {

private final I18nManager i18nManager;

public IssueChangelogFormatter(I18nManager i18nManager) {
this.i18nManager = i18nManager;
}

public List<String> format(Locale locale, FieldDiffs diffs) {
List<String> result = newArrayList();
for (Map.Entry<String, FieldDiffs.Diff> entry : diffs.diffs().entrySet()) {
StringBuilder message = new StringBuilder();
String key = entry.getKey();
FieldDiffs.Diff diff = entry.getValue();
if (diff.newValue() != null && !diff.newValue().equals("")) {
message.append(i18nManager.message(locale, "issue.changelog.changed_to", null, i18nManager.message(locale, "issue.changelog.field." + key, null), diff.newValue()));
} else {
message.append(i18nManager.message(locale, "issue.changelog.removed", null, i18nManager.message(locale, "issue.changelog.field." + key, null)));
}
if (diff.oldValue() != null && !diff.oldValue().equals("")) {
message.append(" (");
message.append(i18nManager.message(locale, "issue.changelog.was", null, diff.oldValue()));
message.append(")");
}
result.add(message.toString());
}
return result;
}

}

+ 6
- 55
sonar-server/src/main/java/org/sonar/server/platform/Platform.java Voir le fichier

@@ -52,13 +52,7 @@ import org.sonar.core.measure.MeasureFilterFactory;
import org.sonar.core.metric.DefaultMetricFinder;
import org.sonar.core.notification.DefaultNotificationManager;
import org.sonar.core.permission.PermissionFacade;
import org.sonar.core.persistence.DaoUtils;
import org.sonar.core.persistence.DatabaseVersion;
import org.sonar.core.persistence.DefaultDatabase;
import org.sonar.core.persistence.MyBatis;
import org.sonar.core.persistence.PreviewDatabaseFactory;
import org.sonar.core.persistence.SemaphoreUpdater;
import org.sonar.core.persistence.SemaphoresImpl;
import org.sonar.core.persistence.*;
import org.sonar.core.preview.PreviewCache;
import org.sonar.core.purge.PurgeProfiler;
import org.sonar.core.qualitymodel.DefaultModelFinder;
@@ -90,63 +84,19 @@ import org.sonar.server.db.EmbeddedDatabaseFactory;
import org.sonar.server.db.migrations.DatabaseMigration;
import org.sonar.server.db.migrations.DatabaseMigrations;
import org.sonar.server.db.migrations.DatabaseMigrator;
import org.sonar.server.issue.ActionPlanService;
import org.sonar.server.issue.ActionService;
import org.sonar.server.issue.AssignAction;
import org.sonar.server.issue.CommentAction;
import org.sonar.server.issue.DefaultIssueFinder;
import org.sonar.server.issue.InternalRubyIssueService;
import org.sonar.server.issue.IssueBulkChangeService;
import org.sonar.server.issue.IssueChangelogService;
import org.sonar.server.issue.IssueCommentService;
import org.sonar.server.issue.IssueFilterService;
import org.sonar.server.issue.IssueService;
import org.sonar.server.issue.IssueStatsFinder;
import org.sonar.server.issue.PlanAction;
import org.sonar.server.issue.PublicRubyIssueService;
import org.sonar.server.issue.ServerIssueStorage;
import org.sonar.server.issue.SetSeverityAction;
import org.sonar.server.issue.TransitionAction;
import org.sonar.server.issue.*;
import org.sonar.server.notifications.NotificationCenter;
import org.sonar.server.notifications.NotificationService;
import org.sonar.server.permission.InternalPermissionService;
import org.sonar.server.permission.InternalPermissionTemplateService;
import org.sonar.server.plugins.ApplicationDeployer;
import org.sonar.server.plugins.DefaultServerPluginRepository;
import org.sonar.server.plugins.InstalledPluginReferentialFactory;
import org.sonar.server.plugins.PluginDeployer;
import org.sonar.server.plugins.PluginDownloader;
import org.sonar.server.plugins.ServerExtensionInstaller;
import org.sonar.server.plugins.UpdateCenterClient;
import org.sonar.server.plugins.UpdateCenterMatrixFactory;
import org.sonar.server.plugins.*;
import org.sonar.server.rule.RubyRuleService;
import org.sonar.server.rules.ProfilesConsole;
import org.sonar.server.rules.RulesConsole;
import org.sonar.server.startup.CleanDryRunCache;
import org.sonar.server.startup.DeleteDeprecatedMeasures;
import org.sonar.server.startup.GenerateBootstrapIndex;
import org.sonar.server.startup.GeneratePluginIndex;
import org.sonar.server.startup.GwtPublisher;
import org.sonar.server.startup.JdbcDriverDeployer;
import org.sonar.server.startup.LogServerId;
import org.sonar.server.startup.RegisterMetrics;
import org.sonar.server.startup.RegisterNewDashboards;
import org.sonar.server.startup.RegisterNewMeasureFilters;
import org.sonar.server.startup.RegisterNewProfiles;
import org.sonar.server.startup.RegisterPermissionTemplates;
import org.sonar.server.startup.RegisterRules;
import org.sonar.server.startup.RegisterServletFilters;
import org.sonar.server.startup.RegisterTechnicalDebtModel;
import org.sonar.server.startup.RenameDeprecatedPropertyKeys;
import org.sonar.server.startup.ServerMetadataPersister;
import org.sonar.server.startup.VerifyNoQualityModelsAreDefined;
import org.sonar.server.startup.*;
import org.sonar.server.text.MacroInterpreter;
import org.sonar.server.text.RubyTextService;
import org.sonar.server.ui.CodeColorizers;
import org.sonar.server.ui.JRubyI18n;
import org.sonar.server.ui.PageDecorations;
import org.sonar.server.ui.SecurityRealmFactory;
import org.sonar.server.ui.Views;
import org.sonar.server.ui.*;
import org.sonar.server.user.DefaultUserService;
import org.sonar.server.user.NewUserNotifier;

@@ -338,6 +288,7 @@ public final class Platform {
servicesContainer.addSingleton(IssueFilterSerializer.class);
servicesContainer.addSingleton(IssueFilterService.class);
servicesContainer.addSingleton(IssueBulkChangeService.class);
servicesContainer.addSingleton(IssueChangelogFormatter.class);
// issues actions
servicesContainer.addSingleton(AssignAction.class);
servicesContainer.addSingleton(PlanAction.class);

+ 2
- 11
sonar-server/src/main/webapp/WEB-INF/app/views/issue/_changelog.html.erb Voir le fichier

@@ -13,19 +13,10 @@
<td class="thin left top" nowrap><%= h(user.name()) if user -%></td>
<td class="left top">
<%
change.diffs.entrySet().each_with_index do |entry, index|
key = entry.getKey()
diff = entry.getValue()
Internal.issues.formatChangelog(change).each_with_index do |message, index|
%>
<% if index>0 %><br/><% end %>
<% if diff.newValue.present? %>
<%= message('issue.changelog.changed_to', :params => [message(key), diff.newValue()]) -%>
<% else %>
<%= message('issue.changelog.removed', :params => [message(key)]) -%>
<% end %>
<% if diff.oldValue.present? %>
(<%= message('issue.changelog.was', :params => [diff.oldValue]) -%>)
<% end %>
<%= message -%>
<% end %>
</td>
</tr>

+ 2
- 1
sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java Voir le fichier

@@ -62,13 +62,14 @@ public class InternalRubyIssueServiceTest {
ActionService actionService = mock(ActionService.class);
IssueFilterService issueFilterService = mock(IssueFilterService.class);
IssueBulkChangeService issueBulkChangeService = mock(IssueBulkChangeService.class);
IssueChangelogFormatter issueChangelogFormatter = mock(IssueChangelogFormatter.class);

@Before
public void setUp() {
ResourceDto project = new ResourceDto().setKey("org.sonar.Sample");
when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(project);
service = new InternalRubyIssueService(issueService, commentService, changelogService, actionPlanService, issueStatsFinder, resourceDao, actionService,
issueFilterService, issueBulkChangeService);
issueFilterService, issueBulkChangeService, issueChangelogFormatter);
}

@Test

+ 123
- 0
sonar-server/src/test/java/org/sonar/server/issue/IssueChangelogFormatterTest.java Voir le fichier

@@ -0,0 +1,123 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.sonar.api.issue.internal.FieldDiffs;
import org.sonar.core.i18n.I18nManager;

import java.util.List;
import java.util.Locale;

import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class IssueChangelogFormatterTest {

private static final Locale DEFAULT_LOCALE = Locale.getDefault();

@Mock
private I18nManager i18nManager;

private IssueChangelogFormatter formatter;

@Before
public void before(){
formatter = new IssueChangelogFormatter(i18nManager);
}

@Test
public void format_field_diffs_with_new_and_old_value(){
FieldDiffs diffs = new FieldDiffs();
diffs.setDiff("severity", "BLOCKER", "INFO");

when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.field.severity", null)).thenReturn("Severity");
when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.changed_to", null, "Severity", "INFO")).thenReturn("Severity changed to INFO");
when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.was", null, "BLOCKER")).thenReturn("was BLOCKER");

List<String> result = formatter.format(DEFAULT_LOCALE, diffs);
assertThat(result).hasSize(1);
String message = result.get(0);
assertThat(message).isEqualTo("Severity changed to INFO (was BLOCKER)");
}

@Test
public void format_field_diffs_with_only_new_value(){
FieldDiffs diffs = new FieldDiffs();
diffs.setDiff("severity", null, "INFO");

when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.field.severity", null)).thenReturn("Severity");
when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.changed_to", null, "Severity", "INFO")).thenReturn("Severity changed to INFO");

List<String> result = formatter.format(DEFAULT_LOCALE, diffs);
assertThat(result).hasSize(1);
String message = result.get(0);
assertThat(message).isEqualTo("Severity changed to INFO");
}

@Test
public void format_field_diffs_with_only_old_value(){
FieldDiffs diffs = new FieldDiffs();
diffs.setDiff("severity", "BLOCKER", null);

when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.field.severity", null)).thenReturn("Severity");
when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.removed", null, "Severity")).thenReturn("Severity removed");
when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.was", null, "BLOCKER")).thenReturn("was BLOCKER");

List<String> result = formatter.format(DEFAULT_LOCALE, diffs);
assertThat(result).hasSize(1);
String message = result.get(0);
assertThat(message).isEqualTo("Severity removed (was BLOCKER)");
}

@Test
public void format_field_diffs_without_value(){
FieldDiffs diffs = new FieldDiffs();
diffs.setDiff("severity", null, null);

when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.field.severity", null)).thenReturn("Severity");
when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.removed", null, "Severity")).thenReturn("Severity removed");

List<String> result = formatter.format(DEFAULT_LOCALE, diffs);
assertThat(result).hasSize(1);
String message = result.get(0);
assertThat(message).isEqualTo("Severity removed");
}

@Test
public void format_field_diffs_with_empty_old_value(){
FieldDiffs diffs = new FieldDiffs();
diffs.setDiff("severity", "", null);

when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.field.severity", null)).thenReturn("Severity");
when(i18nManager.message(DEFAULT_LOCALE, "issue.changelog.removed", null, "Severity")).thenReturn("Severity removed");

List<String> result = formatter.format(DEFAULT_LOCALE, diffs);
assertThat(result).hasSize(1);
String message = result.get(0);
assertThat(message).isEqualTo("Severity removed");
}

}

Chargement…
Annuler
Enregistrer