]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3012 New widget to monitor the review activity
authorFabrice Bellingard <bellingard@gmail.com>
Thu, 12 Jan 2012 17:30:40 +0000 (18:30 +0100)
committerFabrice Bellingard <bellingard@gmail.com>
Thu, 12 Jan 2012 17:40:45 +0000 (18:40 +0100)
- 5 new metrics added
- Decorator implemented to compute those metrics
- Widget implemented to report those metrics

17 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualViolationInjector.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecorator.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ReviewsMetricsWidget.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest/fixture.xml [new file with mode: 0644]
plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/main/java/org/sonar/core/review/ReviewDao.java
sonar-core/src/main/java/org/sonar/core/review/ReviewDto.java
sonar-core/src/main/java/org/sonar/core/review/ReviewMapper.java
sonar-core/src/main/java/org/sonar/core/review/ReviewQuery.java
sonar-core/src/main/resources/org/sonar/core/review/ReviewMapper.xml
sonar-core/src/test/java/org/sonar/core/review/ReviewDaoTest.java
sonar-core/src/test/resources/org/sonar/core/review/ReviewDaoTest/shared.xml
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-server/src/main/java/org/sonar/server/ui/DefaultPages.java

index 463ccb895021e2deec5ebfb9a99540c6c30be571..24319ea90df80cc2d43ec10c4767e58fa9afec1a 100644 (file)
@@ -48,6 +48,7 @@ import org.sonar.plugins.core.widgets.reviews.FalsePositiveReviewsWidget;
 import org.sonar.plugins.core.widgets.reviews.MyReviewsWidget;
 import org.sonar.plugins.core.widgets.reviews.PlannedReviewsWidget;
 import org.sonar.plugins.core.widgets.reviews.ProjectReviewsWidget;
+import org.sonar.plugins.core.widgets.reviews.ReviewsMetricsWidget;
 import org.sonar.plugins.core.widgets.reviews.ReviewsPerDeveloperWidget;
 import org.sonar.plugins.core.widgets.reviews.UnplannedReviewsWidget;
 
@@ -248,6 +249,7 @@ public class CorePlugin extends SonarPlugin {
     extensions.add(PlannedReviewsWidget.class);
     extensions.add(UnplannedReviewsWidget.class);
     extensions.add(ActionPlansWidget.class);
+    extensions.add(ReviewsMetricsWidget.class);
 
     // dashboards
     extensions.add(DefaultDashboard.class);
@@ -291,6 +293,7 @@ public class CorePlugin extends SonarPlugin {
     extensions.add(UpdateReviewsDecorator.class);
     extensions.add(ViolationSeverityUpdater.class);
     extensions.add(IndexProjectPostJob.class);
+    extensions.add(ReviewsMeasuresDecorator.class);
 
     // time machine
     extensions.add(TendencyDecorator.class);
index 78065d011dbdfa7c627127920d77ff04ad181ee6..5a5c0d64d0279a49a240be4514a6d9b81bda1429 100644 (file)
@@ -51,7 +51,7 @@ public class ManualViolationInjector implements Decorator {
 
   public void decorate(Resource resource, DecoratorContext context) {
     if (resource.getId() != null) {
-      ReviewQuery query = ReviewQuery.create().setManualViolation(true).setResourceId(resource.getId()).setStatus(ReviewDto.STATUS_OPEN);
+      ReviewQuery query = ReviewQuery.create().setManualViolation(true).setResourceId(resource.getId()).addStatus(ReviewDto.STATUS_OPEN);
       List<ReviewDto> reviewDtos = reviewDao.selectByQuery(query);
       for (ReviewDto reviewDto : reviewDtos) {
         if (reviewDto.getRuleId() == null) {
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecorator.java
new file mode 100644 (file)
index 0000000..706cc6f
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.sensors;
+
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.core.review.ReviewDao;
+import org.sonar.core.review.ReviewDto;
+import org.sonar.core.review.ReviewQuery;
+import org.sonar.plugins.core.timemachine.ViolationTrackingDecorator;
+
+/**
+ * Decorator that creates measures related to reviews.
+ */
+@DependsUpon(CloseReviewsDecorator.REVIEW_LIFECYCLE_BARRIER)
+public class ReviewsMeasuresDecorator implements Decorator {
+
+  private ReviewDao reviewDao;
+
+  public ReviewsMeasuresDecorator(ReviewDao reviewDao) {
+    this.reviewDao = reviewDao;
+  }
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return project.isLatestAnalysis();
+  }
+
+  @SuppressWarnings("rawtypes")
+  @DependsUpon
+  public Class dependsUponViolationTracking() {
+    // permanent ids of violations have been updated, so we can link them with reviews
+    return ViolationTrackingDecorator.class;
+  }
+
+  @SuppressWarnings({"rawtypes"})
+  public void decorate(Resource resource, DecoratorContext context) {
+    if (!ResourceUtils.isFile(resource)) {
+      return;
+    }
+
+    // Open reviews
+    ReviewQuery openReviewQuery = ReviewQuery.create().setResourceId(resource.getId()).addStatus(ReviewDto.STATUS_OPEN)
+        .addStatus(ReviewDto.STATUS_REOPENED);
+    Integer openReviewsCount = reviewDao.countByQuery(openReviewQuery);
+    context.saveMeasure(CoreMetrics.ACTIVE_REVIEWS, openReviewsCount.doubleValue());
+
+    // Unassigned reviews
+    ReviewQuery unassignedReviewQuery = ReviewQuery.copy(openReviewQuery).setNoAssignee();
+    Integer unassignedReviewsCount = reviewDao.countByQuery(unassignedReviewQuery);
+    context.saveMeasure(CoreMetrics.UNASSIGNED_REVIEWS, unassignedReviewsCount.doubleValue());
+
+    // Unplanned reviews
+    ReviewQuery plannedReviewQuery = ReviewQuery.copy(openReviewQuery).setPlanned();
+    int plannedReviewsCount = reviewDao.countByQuery(plannedReviewQuery);
+    context.saveMeasure(CoreMetrics.UNPLANNED_REVIEWS, (double) (openReviewsCount - plannedReviewsCount));
+
+    // False positive reviews
+    ReviewQuery falsePositiveReviewQuery = ReviewQuery.create().setResourceId(resource.getId())
+        .addResolution(ReviewDto.RESOLUTION_FALSE_POSITIVE);
+    Integer falsePositiveReviewsCount = reviewDao.countByQuery(falsePositiveReviewQuery);
+    context.saveMeasure(CoreMetrics.FALSE_POSITIVE_REVIEWS, falsePositiveReviewsCount.doubleValue());
+
+    // Violations without a review
+    int violationsCount = context.getViolations().size();
+    context.saveMeasure(CoreMetrics.VIOLATIONS_WITHOUT_REVIEW, (double) (violationsCount - openReviewsCount - falsePositiveReviewsCount));
+  }
+
+}
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ReviewsMetricsWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ReviewsMetricsWidget.java
new file mode 100644 (file)
index 0000000..7ad4e69
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.widgets.reviews;
+
+import org.sonar.api.web.AbstractRubyTemplate;
+import org.sonar.api.web.RubyRailsWidget;
+import org.sonar.api.web.WidgetCategory;
+
+@WidgetCategory({ "Reviews" })
+public class ReviewsMetricsWidget extends AbstractRubyTemplate implements RubyRailsWidget {
+  public String getId() {
+    return "reviews_metrics";
+  }
+
+  public String getTitle() {
+    return "Reviews metrics";
+  }
+
+  @Override
+  protected String getTemplatePath() {
+    return "/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb";
+  }
+}
diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb
new file mode 100644 (file)
index 0000000..3388d4f
--- /dev/null
@@ -0,0 +1,50 @@
+<%
+  active_reviews=measure('active_reviews')
+  unassigned_reviews=measure('unassigned_reviews')
+  unplanned_reviews=measure('unplanned_reviews')
+  false_positive_reviews=measure('false_positive_reviews')
+  violations_without_review=measure('violations_without_review')
+%>
+<table width="100%">
+  <tr>
+    <td valign="top" width="48%" nowrap>
+      <div class="dashbox">
+        <h3><%= message('widget.reviews_metrics.active_reviews') -%></h3>
+        <% if active_reviews %>
+        <p>
+          <span class="big"><%= format_measure(active_reviews, :suffix => '', :url => url_for_drilldown(active_reviews)) -%></span>
+          <%= dashboard_configuration.selected_period? ? format_variation(active_reviews) : trend_icon(active_reviews) -%>
+        </p>
+        <p>
+          <%= format_measure(unassigned_reviews, :suffix => message('widget.reviews_metrics.unassigned.suffix'), :url => url_for_drilldown(unassigned_reviews)) -%>
+          <%= dashboard_configuration.selected_period? ? format_variation(unassigned_reviews) : trend_icon(unassigned_reviews) -%>
+        </p>
+        <p>
+          <%= format_measure(unplanned_reviews, :suffix => message('widget.reviews_metrics.unplanned.suffix'), :url => url_for_drilldown(unplanned_reviews)) -%>
+          <%= dashboard_configuration.selected_period? ? format_variation(unplanned_reviews) : trend_icon(unplanned_reviews) -%>
+        </p>
+        <p>
+          <%= format_measure(false_positive_reviews, :suffix => message('widget.reviews_metrics.false_positives.suffix'), :url => url_for_drilldown(false_positive_reviews)) -%>
+          <%= dashboard_configuration.selected_period? ? format_variation(false_positive_reviews) : trend_icon(false_positive_reviews) -%>
+        </p>
+        <% else %>
+          <span class="empty_widget"><%= message('widget.reviews_metrics.no_data') -%></span>
+        <% end %>
+      </div>
+    </td>
+    <td width="10"> </td>
+    <td valign="top">
+      <div class="dashbox">
+        <h3><%= message('widget.reviews_metrics.unreviewed_violations') -%></h3>
+        <% if violations_without_review %>
+        <p>
+          <span class="big"><%= format_measure(violations_without_review, :suffix => '', :url => url_for_drilldown(violations_without_review)) -%></span>
+          <%= dashboard_configuration.selected_period? ? format_variation(violations_without_review) : trend_icon(violations_without_review) -%>
+        </p>
+        <% else %>
+          <span class="empty_widget"><%= message('widget.reviews_metrics.no_data') -%></span>
+        <% end %>
+      </div>
+    </td>
+  </tr>
+</table>
\ No newline at end of file
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest.java
new file mode 100644 (file)
index 0000000..b72c4b9
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.sensors;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyDouble;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rules.Violation;
+import org.sonar.core.review.ReviewDao;
+import org.sonar.core.review.ReviewDto;
+import org.sonar.core.review.ReviewQuery;
+import org.sonar.plugins.core.timemachine.ViolationTrackingDecorator;
+
+import com.google.common.collect.Lists;
+
+public class ReviewsMeasuresDecoratorTest {
+
+  @Test
+  public void testDependsUponViolationTracking() throws Exception {
+    ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(null);
+    assertEquals(decorator.dependsUponViolationTracking(), ViolationTrackingDecorator.class);
+  }
+
+  @Test
+  public void shouldExecuteOnProject() throws Exception {
+    ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(null);
+    Project project = new Project("foo");
+    project.setLatestAnalysis(true);
+    assertThat(decorator.shouldExecuteOnProject(project), is(true));
+  }
+
+  @Test
+  public void shouldDecorateOnlyFiles() throws Exception {
+    ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(null);
+    DecoratorContext context = mock(DecoratorContext.class);
+    Resource<?> resource = new Project("Foo");
+    decorator.decorate(resource, context);
+    verify(context, never()).saveMeasure(any(Metric.class), anyDouble());
+  }
+
+  @Test
+  public void shouldComputeReviewMetrics() throws Exception {
+    ReviewDao reviewDao = mock(ReviewDao.class);
+    when(reviewDao.countByQuery(argThat(openReviewQueryMatcher()))).thenReturn(10);
+    when(reviewDao.countByQuery(argThat(unassignedReviewQueryMatcher()))).thenReturn(2);
+    when(reviewDao.countByQuery(argThat(plannedReviewQueryMatcher()))).thenReturn(3);
+    when(reviewDao.countByQuery(argThat(falsePositiveReviewQueryMatcher()))).thenReturn(4);
+
+    ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(reviewDao);
+    Resource<?> resource = new File("foo").setId(1);
+    DecoratorContext context = mock(DecoratorContext.class);
+    List<Violation> violations = mock(List.class);
+    when(violations.size()).thenReturn(35);
+    when(context.getViolations()).thenReturn(violations);
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ACTIVE_REVIEWS, 10d);
+    verify(context).saveMeasure(CoreMetrics.UNASSIGNED_REVIEWS, 2d);
+    verify(context).saveMeasure(CoreMetrics.UNPLANNED_REVIEWS, 7d);
+    verify(context).saveMeasure(CoreMetrics.FALSE_POSITIVE_REVIEWS, 4d);
+    verify(context).saveMeasure(CoreMetrics.VIOLATIONS_WITHOUT_REVIEW, 21d);
+  }
+
+  private BaseMatcher<ReviewQuery> openReviewQueryMatcher() {
+    return new BaseMatcher<ReviewQuery>() {
+      public boolean matches(Object o) {
+        ReviewQuery query = (ReviewQuery) o;
+        if (query == null) {
+          return false;
+        }
+        return Lists.newArrayList(ReviewDto.STATUS_OPEN, ReviewDto.STATUS_REOPENED).equals(query.getStatuses())
+          && query.getNoAssignee() == null && query.getPlanned() == null;
+      }
+
+      public void describeTo(Description description) {
+      }
+    };
+  }
+
+  private BaseMatcher<ReviewQuery> unassignedReviewQueryMatcher() {
+    return new BaseMatcher<ReviewQuery>() {
+      public boolean matches(Object o) {
+        ReviewQuery query = (ReviewQuery) o;
+        if (query == null) {
+          return false;
+        }
+        return Lists.newArrayList(ReviewDto.STATUS_OPEN, ReviewDto.STATUS_REOPENED).equals(query.getStatuses())
+          && query.getNoAssignee() == Boolean.TRUE;
+      }
+
+      public void describeTo(Description description) {
+      }
+    };
+  }
+
+  private BaseMatcher<ReviewQuery> plannedReviewQueryMatcher() {
+    return new BaseMatcher<ReviewQuery>() {
+      public boolean matches(Object o) {
+        ReviewQuery query = (ReviewQuery) o;
+        if (query == null) {
+          return false;
+        }
+        return Lists.newArrayList(ReviewDto.STATUS_OPEN, ReviewDto.STATUS_REOPENED).equals(query.getStatuses())
+          && query.getPlanned() == Boolean.TRUE;
+      }
+
+      public void describeTo(Description description) {
+      }
+    };
+  }
+
+  private BaseMatcher<ReviewQuery> falsePositiveReviewQueryMatcher() {
+    return new BaseMatcher<ReviewQuery>() {
+      public boolean matches(Object o) {
+        ReviewQuery query = (ReviewQuery) o;
+        if (query == null) {
+          return false;
+        }
+        return Lists.newArrayList(ReviewDto.RESOLUTION_FALSE_POSITIVE).equals(query.getResolutions());
+      }
+
+      public void describeTo(Description description) {
+      }
+    };
+  }
+}
diff --git a/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest/fixture.xml b/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest/fixture.xml
new file mode 100644 (file)
index 0000000..7384c0e
--- /dev/null
@@ -0,0 +1,69 @@
+<dataset>
+  <reviews
+    id="1"
+    status="OPEN"
+    rule_failure_permanent_id="1"
+    resource_id="1"
+    title="message OLD"
+    resource_line="0"
+    resolution="[null]"
+    created_at="[null]"
+    updated_at="[null]"
+    project_id="[null]"
+    severity="[null]"
+    user_id="[null]"
+    rule_id="[null]"
+    manual_violation="false"
+    manual_severity="false"/>
+  <reviews
+    id="2"
+    status="OPEN"
+    rule_failure_permanent_id="2"
+    resource_id="1"
+    title="message 2"
+    resource_line="2"
+    rule_id="[null]"
+    manual_violation="false"
+    manual_severity="false"/>
+  <reviews
+    id="3"
+    status="OPEN"
+    rule_failure_permanent_id="3"
+    resource_id="1"
+    title="message 3"
+    resource_line="0"
+    rule_id="[null]"
+    manual_violation="false"
+    manual_severity="false"/>
+  <reviews
+    id="4"
+    status="OPEN"
+    rule_failure_permanent_id="4"
+    resource_id="1"
+    title="message OLD"
+    resource_line="4"
+    rule_id="[null]"
+    manual_violation="false"
+    manual_severity="false"/>
+  <reviews
+    id="5"
+    status="OPEN"
+    rule_failure_permanent_id="5"
+    resource_id="1"
+    title="message 5"
+    resource_line="[null]"
+    rule_id="[null]"
+    manual_violation="false"
+    manual_severity="false"/>
+  <reviews
+    id="6"
+    status="OPEN"
+    rule_failure_permanent_id="6"
+    resource_id="1"
+    title="message OLD"
+    resource_line="[null]"
+    rule_id="[null]"
+    manual_violation="false"
+    manual_severity="false"/>
+
+</dataset>
\ No newline at end of file
index 5798be1507c4eef5acdc6d381c2dff76bab70a92..dbb2a5c49fc3eb8b81ff38de39fa77a94cb65734 100644 (file)
@@ -719,6 +719,15 @@ widget.planned_reviews.no_action_plan=No action plan
 widget.unplanned_reviews.name=Unplanned reviews
 widget.unplanned_reviews.description=Shows all the reviews of the project that are not planned yet in an action plan
 
+widget.reviews_metrics.name=Reviews metrics
+widget.reviews_metrics.description=Reports metrics about reviews
+widget.reviews_metrics.no_data=No data
+widget.reviews_metrics.active_reviews=Active reviews
+widget.reviews_metrics.unassigned.suffix=\ unassigned
+widget.reviews_metrics.unplanned.suffix=\ unplanned
+widget.reviews_metrics.false_positives.suffix=\ false-positives
+widget.reviews_metrics.unreviewed_violations=Unreviewed violations
+
 
 
 #------------------------------------------------------------------------------
@@ -1542,3 +1551,23 @@ metric.business_value.description=An indication on the value of the project for
 metric.team_size.name=Team size
 metric.team_size.description=Size of the project team
 
+
+#--------------------------------------------------------------------------------------------------------------------
+#
+# REVIEWS METRICS
+#
+#--------------------------------------------------------------------------------------------------------------------
+metric.violations_without_review.name=Unreviewed violations
+metric.violations_without_review.description=Violations that have not been reviewed yet
+
+metric.false_positive_reviews.name=False-positive reviews
+metric.false_positive_reviews.description=Active false-positive reviews
+
+metric.active_reviews.name=Active reviews
+metric.active_reviews.description=Active open and reopened reviews
+
+metric.unassigned_reviews.name=Unassigned reviews
+metric.unassigned_reviews.description=Active unassigned reviews
+
+metric.unplanned_reviews.name=Unplanned reviews
+metric.unplanned_reviews.description=Active unplanned reviews
index 27ea0b8076441dcae60a3dcae064665ee93126cf..438bb9110213e7d08e38a4415a5b2cd912c0442c 100644 (file)
  */
 package org.sonar.core.review;
 
-import com.google.common.collect.Lists;
+import java.util.List;
+
 import org.apache.ibatis.session.SqlSession;
 import org.sonar.api.BatchComponent;
 import org.sonar.api.ServerComponent;
 import org.sonar.core.persistence.MyBatis;
 
-import java.util.List;
+import com.google.common.collect.Lists;
 
 public class ReviewDao implements BatchComponent, ServerComponent {
   private final MyBatis mybatis;
@@ -44,29 +45,36 @@ public class ReviewDao implements BatchComponent, ServerComponent {
     }
   }
 
-  public List<ReviewDto> selectByResource(int resourceId) {
+  public List<ReviewDto> selectByQuery(ReviewQuery query) {
     SqlSession sqlSession = mybatis.openSession();
     try {
       ReviewMapper mapper = sqlSession.getMapper(ReviewMapper.class);
-      return mapper.selectByResource(resourceId);
+      List<ReviewDto> result;
+      if (query.needToPartitionQuery()) {
+        result = Lists.newArrayList();
+        for (ReviewQuery partitionedQuery : query.partition()) {
+          result.addAll(mapper.selectByQuery(partitionedQuery));
+        }
+      } else {
+        result = mapper.selectByQuery(query);
+      }
+      return result;
     } finally {
       sqlSession.close();
     }
   }
 
-  public List<ReviewDto> selectByQuery(ReviewQuery query) {
+  public Integer countByQuery(ReviewQuery query) {
     SqlSession sqlSession = mybatis.openSession();
     try {
       ReviewMapper mapper = sqlSession.getMapper(ReviewMapper.class);
-      List<ReviewDto> result;
+      Integer result = 0;
       if (query.needToPartitionQuery()) {
-        result = Lists.newArrayList();
         for (ReviewQuery partitionedQuery : query.partition()) {
-          result.addAll(mapper.selectByQuery(partitionedQuery));
+          result += mapper.countByQuery(partitionedQuery);
         }
-
       } else {
-        result = mapper.selectByQuery(query);
+        result = mapper.countByQuery(query);
       }
       return result;
     } finally {
index 7f349a6b627f217d034fe9d1becb0ce1aff31bd2..9fe21da9b2e45e9608462377d748cdce2a873a20 100644 (file)
@@ -34,6 +34,9 @@ public final class ReviewDto {
   public static final String STATUS_RESOLVED = "RESOLVED";
   public static final String STATUS_CLOSED = "CLOSED";
 
+  public static final String RESOLUTION_FALSE_POSITIVE = "FALSE-POSITIVE";
+  public static final String RESOLUTION_FIXED = "FIXED";
+
   private Long id;
   private Integer userId;
   private Integer assigneeId;
index 89c7a6a84c5b8367fcb807a21bd2ace687b1247d..1048128212f69b5a97de47e8c0633a311e8218ec 100644 (file)
@@ -19,8 +19,6 @@
  */
 package org.sonar.core.review;
 
-import org.apache.ibatis.annotations.Param;
-
 import java.util.List;
 
 /**
@@ -29,7 +27,7 @@ import java.util.List;
 public interface ReviewMapper {
   ReviewDto selectById(long id);
 
-  List<ReviewDto> selectByResource(int resourceId);
-
   List<ReviewDto> selectByQuery(ReviewQuery query);
+
+  Integer countByQuery(ReviewQuery query);
 }
index afaa5ec369d34cb637dc7331f0572f6d906db6b4..9481adc008efd52b1213c9d5abc8e59a24bb45f4 100644 (file)
  */
 package org.sonar.core.review;
 
-import com.google.common.collect.Lists;
-import org.sonar.core.persistence.DatabaseUtils;
-
 import java.util.Collection;
 import java.util.List;
 
+import org.sonar.core.persistence.DatabaseUtils;
+
+import com.google.common.collect.Lists;
+
 /**
  * @since 2.13
  */
@@ -35,27 +36,35 @@ public final class ReviewQuery {
   private Integer userId;
   private List<Integer> violationPermanentIds;
   private Integer ruleId;
-  private String status;
-  private String resolution;
+  private List<String> statuses;
+  private List<String> resolutions;
+  private Boolean noAssignee;
+  private Boolean planned;
 
   private ReviewQuery() {
   }
 
-  private ReviewQuery(ReviewQuery other, List<Integer> permanentIds) {
+  private ReviewQuery(ReviewQuery other) {
     this.manualViolation = other.manualViolation;
     this.manualSeverity = other.manualSeverity;
     this.resourceId = other.resourceId;
     this.userId = other.userId;
-    this.violationPermanentIds = permanentIds;
+    this.violationPermanentIds = other.violationPermanentIds;
     this.ruleId = other.ruleId;
-    this.status = other.status;
-    this.resolution = other.resolution;
+    this.statuses = other.statuses;
+    this.resolutions = other.resolutions;
+    this.noAssignee = other.noAssignee;
+    this.planned = other.planned;
   }
 
   public static ReviewQuery create() {
     return new ReviewQuery();
   }
 
+  public static ReviewQuery copy(ReviewQuery reviewQuery) {
+    return new ReviewQuery(reviewQuery);
+  }
+
   public Boolean getManualViolation() {
     return manualViolation;
   }
@@ -74,12 +83,15 @@ public final class ReviewQuery {
     return this;
   }
 
-  public String getStatus() {
-    return status;
+  public List<String> getStatuses() {
+    return statuses;
   }
 
-  public ReviewQuery setStatus(String status) {
-    this.status = status;
+  public ReviewQuery addStatus(String status) {
+    if (statuses == null) {
+      statuses = Lists.newArrayList();
+    }
+    statuses.add(status);
     return this;
   }
 
@@ -110,12 +122,15 @@ public final class ReviewQuery {
     return this;
   }
 
-  public String getResolution() {
-    return resolution;
+  public List<String> getResolutions() {
+    return resolutions;
   }
 
-  public ReviewQuery setResolution(String resolution) {
-    this.resolution = resolution;
+  public ReviewQuery addResolution(String resolution) {
+    if (resolutions == null) {
+      resolutions = Lists.newArrayList();
+    }
+    resolutions.add(resolution);
     return this;
   }
 
@@ -128,6 +143,24 @@ public final class ReviewQuery {
     return this;
   }
 
+  public Boolean getNoAssignee() {
+    return noAssignee;
+  }
+
+  public ReviewQuery setNoAssignee() {
+    this.noAssignee = Boolean.TRUE;
+    return this;
+  }
+
+  public Boolean getPlanned() {
+    return planned;
+  }
+
+  public ReviewQuery setPlanned() {
+    this.planned = Boolean.TRUE;
+    return this;
+  }
+
   boolean needToPartitionQuery() {
     return violationPermanentIds != null && violationPermanentIds.size() > DatabaseUtils.MAX_IN_ELEMENTS;
   }
@@ -136,7 +169,7 @@ public final class ReviewQuery {
     List<List<Integer>> partitions = Lists.partition(violationPermanentIds, DatabaseUtils.MAX_IN_ELEMENTS);
     ReviewQuery[] result = new ReviewQuery[partitions.size()];
     for (int index = 0; index < partitions.size(); index++) {
-      result[index] = new ReviewQuery(this, partitions.get(index));
+      result[index] = ReviewQuery.copy(this).setViolationPermanentIds(partitions.get(index));
     }
 
     return result;
index 2f897ff6e1ca4f2e8602e8e8b5b388343fabd7c1..8af600daad9abdf619ebf46d48060457ec6f2b0a 100644 (file)
         <include refid="reviewColumns"/>
         from reviews where id=#{id}
     </select>
-
-    <select id="selectByResource" parameterType="int" resultMap="reviewResultMap">
-        select
-        <include refid="reviewColumns"/>
-        from reviews where resource_id=#{id}
-    </select>
-
-    <select id="selectByQuery" parameterType="org.sonar.core.review.ReviewQuery" resultMap="reviewResultMap">
-        select
-        <include refid="reviewColumns"/>
+    
+    <sql id="selectOrCountFromWhere">
         from reviews
+        <if test="planned != null">R, action_plans_reviews APR</if>
         <where>
             <if test="userId != null">user_id = #{userId}</if>
             <if test="violationPermanentIds != null">AND rule_failure_permanent_id in
             </if>
             <if test="ruleId != null">AND rule_id = #{ruleId}</if>
             <if test="resourceId != null">AND resource_id = #{resourceId}</if>
-            <if test="status != null">AND status = #{status}</if>
+            <if test="statuses != null">AND 
+                <foreach item="status" index="index" collection="statuses"
+                         open="(" separator=" OR " close=")">status = #{status}
+                </foreach>
+            </if>
             <if test="manualViolation != null">AND manual_violation = #{manualViolation}</if>
             <if test="manualSeverity != null">AND manual_severity = #{manualSeverity}</if>
-            <if test="resolution != null">AND resolution = #{resolution}</if>
+            <if test="resolutions != null">AND 
+                <foreach item="resolution" index="index" collection="resolutions"
+                         open="(" separator=" OR " close=")">resolution = #{resolution}
+                </foreach>
+            </if>
+            <if test="noAssignee != null">AND assignee_id IS NULL</if>
+            <if test="planned != null">AND R.id = APR.review_id</if>
         </where>
+    </sql>
+
+    <select id="selectByQuery" parameterType="org.sonar.core.review.ReviewQuery" resultMap="reviewResultMap">
+        select
+        <include refid="reviewColumns"/>
+        <include refid="selectOrCountFromWhere"/>
+    </select>
+    
+
+    <select id="countByQuery" parameterType="org.sonar.core.review.ReviewQuery" resultType="Integer">
+        select count(id)
+        <include refid="selectOrCountFromWhere"/>
     </select>
 
 </mapper>
index de1806a978da16bd21a15bb5e3ce9116bddd6fbc..3a307c0f2532bbd545607324b2cee3e18653b443 100644 (file)
  */
 package org.sonar.core.review;
 
-import com.google.common.collect.Lists;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.junit.Before;
 import org.junit.Test;
 import org.sonar.core.persistence.DaoTestCase;
 
-import java.util.List;
-
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThat;
+import com.google.common.collect.Lists;
 
 public class ReviewDaoTest extends DaoTestCase {
 
@@ -48,7 +52,7 @@ public class ReviewDaoTest extends DaoTestCase {
     ReviewDto reviewDto = dao.selectById(100L);
     assertThat(reviewDto.getId(), is(100L));
     assertThat(reviewDto.getStatus(), is("OPEN"));
-    assertThat(reviewDto.getResolution(), is("RESOLVE"));
+    assertThat(reviewDto.getResolution(), is(nullValue()));
     assertThat(reviewDto.getProjectId(), is(20));
     assertThat(reviewDto.getViolationPermanentId(), is(1));
     assertThat(reviewDto.getSeverity(), is("BLOCKER"));
@@ -66,10 +70,10 @@ public class ReviewDaoTest extends DaoTestCase {
   }
 
   @Test
-  public void shouldSelectByResource() {
+  public void shouldSelectByQuery() {
     setupData("shared");
 
-    List<ReviewDto> reviewDtos = dao.selectByResource(400);
+    List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setResourceId(400));
     assertThat(reviewDtos.size(), is(2));
     for (ReviewDto reviewDto : reviewDtos) {
       assertThat(reviewDto.getId(), anyOf(is(100L), is(101L)));
@@ -78,19 +82,62 @@ public class ReviewDaoTest extends DaoTestCase {
   }
 
   @Test
-  public void shouldSelectByQuery() {
+  public void shouldSelectByQueryWithStatuses() {
     setupData("shared");
 
-    List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setResourceId(400));
+    List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().addStatus(ReviewDto.STATUS_OPEN)
+        .addStatus(ReviewDto.STATUS_REOPENED));
+    assertThat(reviewDtos.size(), is(3));
+    for (ReviewDto reviewDto : reviewDtos) {
+      assertThat(reviewDto.getId(), anyOf(is(100L), is(102L), is(103L)));
+    }
+  }
+
+  @Test
+  public void shouldSelectByQueryWithResolutions() {
+    setupData("shared");
+
+    List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().addResolution(ReviewDto.RESOLUTION_FALSE_POSITIVE)
+        .addResolution(ReviewDto.RESOLUTION_FIXED));
+    assertThat(reviewDtos.size(), is(2));
+    for (ReviewDto reviewDto : reviewDtos) {
+      assertThat(reviewDto.getId(), anyOf(is(101L), is(104L)));
+    }
+  }
+
+  @Test
+  public void shouldSelectByQueryWithNoAssignee() {
+    setupData("shared");
+
+    List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setNoAssignee());
+    assertThat(reviewDtos.size(), is(2));
+    for (ReviewDto reviewDto : reviewDtos) {
+      assertThat(reviewDto.getId(), anyOf(is(101L), is(103L)));
+    }
+  }
+
+  @Test
+  public void shouldSelectByQueryWithPlanned() {
+    setupData("shared");
+
+    List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setPlanned());
     assertThat(reviewDtos.size(), is(2));
     for (ReviewDto reviewDto : reviewDtos) {
       assertThat(reviewDto.getId(), anyOf(is(100L), is(101L)));
-      assertThat(reviewDto.getResourceId(), is(400));
     }
   }
 
   @Test
-  public void shouldSelectByQuery_booleanCriteria() {
+  public void shouldCountByQuery() {
+    setupData("shared");
+
+    Integer count = dao.countByQuery(ReviewQuery.create().addStatus(ReviewDto.STATUS_OPEN)
+        .addStatus(ReviewDto.STATUS_REOPENED));
+    assertThat(count, is(3));
+  }
+
+  @Test
+  public void shouldSelectByQueryWithBooleanCriteria() {
     setupData("shared");
 
     List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setResourceId(400).setManualViolation(true));
@@ -111,12 +158,16 @@ public class ReviewDaoTest extends DaoTestCase {
     }
     ReviewQuery query = ReviewQuery.create().setViolationPermanentIds(permanentIds);
 
+    // test select query
     List<ReviewDto> reviewDtos = dao.selectByQuery(query);
 
     assertThat(reviewDtos.size(), is(3));
     assertThat(reviewDtos, hasItem(new ReviewMatcherByViolationPermanentId(100)));
     assertThat(reviewDtos, hasItem(new ReviewMatcherByViolationPermanentId(1300)));
     assertThat(reviewDtos, hasItem(new ReviewMatcherByViolationPermanentId(3200)));
+    
+    // and test count query
+    assertThat(dao.countByQuery(query), is(3));
   }
 
   static class ReviewMatcherByViolationPermanentId extends BaseMatcher<ReviewDto> {
index 70c101759dedb9cd82b0bb7e66552ebbded4d219..376ff96eb8cc44b1eb9fe7828a1dd2cdbe20fc10 100644 (file)
@@ -1,17 +1,21 @@
 <dataset>
 
+  <action_plans_reviews action_plan_id="1" review_id="100"/>
+  <action_plans_reviews action_plan_id="2" review_id="101"/>
+
   <!-- First resource -->
   <reviews
     id="100"
     status="OPEN"
     rule_failure_permanent_id="1"
-    resolution="RESOLVE"
+    resolution="[null]"
     created_at="[null]"
     updated_at="[null]"
     project_id="20"
     resource_line="200"
     severity="BLOCKER"
     user_id="300"
+    assignee_id="300"
     resource_id="400"
     rule_id="500"
     manual_violation="[true]"
     id="101"
     status="CLOSED"
     rule_failure_permanent_id="1"
-    resolution="RESOLVE"
+    resolution="FIXED"
     created_at="[null]"
     updated_at="[null]"
     project_id="30"
     resource_line="120"
     severity="MAJOR"
     user_id="300"
+    assignee_id="[null]"
     resource_id="400"
     rule_id="505"
     manual_violation="[false]"
     id="102"
     status="OPEN"
     rule_failure_permanent_id="1"
-    resolution="RESOLVE"
+    resolution="[null]"
     created_at="[null]"
     updated_at="[null]"
     project_id="20"
     resource_line="200"
     severity="BLOCKER"
     user_id="300"
+    assignee_id="300"
     resource_id="401"
     rule_id="500"
     manual_violation="[true]"
     manual_severity="[false]"/>
+    
+  <reviews
+    id="103"
+    status="REOPENED"
+    rule_failure_permanent_id="1"
+    resolution="[null]"
+    created_at="[null]"
+    updated_at="[null]"
+    project_id="20"
+    resource_line="200"
+    severity="BLOCKER"
+    user_id="300"
+    assignee_id="[null]"
+    resource_id="401"
+    rule_id="500"
+    manual_violation="[false]"
+    manual_severity="[false]"/>
+    
+  <reviews
+    id="104"
+    status="RESOLVED"
+    rule_failure_permanent_id="1"
+    resolution="FALSE-POSITIVE"
+    created_at="[null]"
+    updated_at="[null]"
+    project_id="20"
+    resource_line="200"
+    severity="BLOCKER"
+    user_id="300"
+    assignee_id="300"
+    resource_id="401"
+    rule_id="500"
+    manual_violation="[false]"
+    manual_severity="[false]"/>
 
 </dataset>
index 824aab6bdc870f548eb1cca9ac3b7fb55afb97b2..01b28da154e3f2f590ce2159593a596ac999d31d 100644 (file)
@@ -43,6 +43,7 @@ public final class CoreMetrics {
   public static final String DOMAIN_DOCUMENTATION = "Documentation";
   public static final String DOMAIN_RULES = "Rules";
   public static final String DOMAIN_SCM = "SCM";
+  public static final String DOMAIN_REVIEWS = "Reviews";
 
   /**
    * @deprecated since 2.5 See http://jira.codehaus.org/browse/SONAR-2007
@@ -1398,6 +1399,101 @@ public final class CoreMetrics {
       .create();
 
 
+  //--------------------------------------------------------------------------------------------------------------------
+  //
+  // REVIEWS (since 2.14)
+  //
+  //--------------------------------------------------------------------------------------------------------------------
+
+  /**
+   * @since 2.14
+   */
+  public static final String VIOLATIONS_WITHOUT_REVIEW_KEY = "violations_without_review";
+
+  /**
+   * @since 2.14
+   */
+  public static final Metric VIOLATIONS_WITHOUT_REVIEW = new Metric.Builder(VIOLATIONS_WITHOUT_REVIEW_KEY, "Unreviewed violations", Metric.ValueType.INT)
+      .setDescription("Violations that have not been reviewed yet")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setDomain(DOMAIN_REVIEWS)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setFormula(new SumChildValuesFormula(false))
+      .create();
+
+  /**
+   * @since 2.14
+   */
+  public static final String FALSE_POSITIVE_REVIEWS_KEY = "false_positive_reviews";
+
+  /**
+   * @since 2.14
+   */
+  public static final Metric FALSE_POSITIVE_REVIEWS = new Metric.Builder(FALSE_POSITIVE_REVIEWS_KEY, "False-positive reviews", Metric.ValueType.INT)
+      .setDescription("Active false-positive reviews")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setDomain(DOMAIN_REVIEWS)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setFormula(new SumChildValuesFormula(false))
+      .create();
+
+  /**
+   * @since 2.14
+   */
+  public static final String ACTIVE_REVIEWS_KEY = "active_reviews";
+
+  /**
+   * @since 2.14
+   */
+  public static final Metric ACTIVE_REVIEWS = new Metric.Builder(ACTIVE_REVIEWS_KEY, "Active reviews", Metric.ValueType.INT)
+      .setDescription("Active open and reopened reviews")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setDomain(DOMAIN_REVIEWS)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setFormula(new SumChildValuesFormula(false))
+      .create();
+
+  /**
+   * @since 2.14
+   */
+  public static final String UNASSIGNED_REVIEWS_KEY = "unassigned_reviews";
+
+  /**
+   * @since 2.14
+   */
+  public static final Metric UNASSIGNED_REVIEWS = new Metric.Builder(UNASSIGNED_REVIEWS_KEY, "Unassigned reviews", Metric.ValueType.INT)
+      .setDescription("Active unassigned reviews")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setDomain(DOMAIN_REVIEWS)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setFormula(new SumChildValuesFormula(false))
+      .create();
+
+  /**
+   * @since 2.14
+   */
+  public static final String UNPLANNED_REVIEWS_KEY = "unplanned_reviews";
+
+  /**
+   * @since 2.14
+   */
+  public static final Metric UNPLANNED_REVIEWS = new Metric.Builder(UNPLANNED_REVIEWS_KEY, "Unplanned reviews", Metric.ValueType.INT)
+      .setDescription("Active unplanned reviews")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setDomain(DOMAIN_REVIEWS)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setFormula(new SumChildValuesFormula(false))
+      .create();
+
+  
+  
+
+
   //--------------------------------------------------------------------------------------------------------------------
   //
   // OTHERS
index 970b2d1598b3ac471bb4e78910ecbf2a2290244b..d0268838f5cd694d8318b206f15a6dd743b49621 100644 (file)
@@ -93,7 +93,8 @@ public final class DefaultPages {
   @DefaultTab(metrics = {CoreMetrics.VIOLATIONS_DENSITY_KEY, CoreMetrics.WEIGHTED_VIOLATIONS_KEY, CoreMetrics.VIOLATIONS_KEY, CoreMetrics.BLOCKER_VIOLATIONS_KEY,
     CoreMetrics.CRITICAL_VIOLATIONS_KEY, CoreMetrics.MAJOR_VIOLATIONS_KEY, CoreMetrics.MINOR_VIOLATIONS_KEY, CoreMetrics.INFO_VIOLATIONS_KEY,
     CoreMetrics.NEW_VIOLATIONS_KEY, CoreMetrics.NEW_BLOCKER_VIOLATIONS_KEY, CoreMetrics.NEW_CRITICAL_VIOLATIONS_KEY, CoreMetrics.NEW_MAJOR_VIOLATIONS_KEY,
-    CoreMetrics.NEW_MINOR_VIOLATIONS_KEY, CoreMetrics.NEW_INFO_VIOLATIONS_KEY})
+    CoreMetrics.NEW_MINOR_VIOLATIONS_KEY, CoreMetrics.NEW_INFO_VIOLATIONS_KEY, CoreMetrics.ACTIVE_REVIEWS_KEY, CoreMetrics.UNASSIGNED_REVIEWS_KEY,
+    CoreMetrics.UNPLANNED_REVIEWS_KEY, CoreMetrics.FALSE_POSITIVE_REVIEWS_KEY, CoreMetrics.VIOLATIONS_WITHOUT_REVIEW_KEY})
   @ResourceQualifier({Qualifiers.VIEW, Qualifiers.SUBVIEW, Qualifiers.PROJECT, Qualifiers.MODULE, Qualifiers.PACKAGE, Qualifiers.DIRECTORY, Qualifiers.FILE, Qualifiers.CLASS})
   /* all exept unit tests...*/
   @UserRole(UserRole.CODEVIEWER)