From c0449e0a612fc44c4a4bff06a35c1f744c898608 Mon Sep 17 00:00:00 2001 From: Fabrice Bellingard Date: Thu, 29 Nov 2012 11:28:02 +0100 Subject: [PATCH] SONAR-37 & SONAR-2911 Add comparison tool - To compare projects to each others (SONAR-37) - To compare X versions of a project (SONAR-2911) --- .../plugins/core/DefaultResourceTypes.java | 23 +- .../resources/org/sonar/l10n/core.properties | 19 ++ .../org/sonar/api/resources/ResourceType.java | 2 + .../app/controllers/comparison_controller.rb | 87 +++++++ .../WEB-INF/app/helpers/application_helper.rb | 1 + .../app/views/comparison/_versions.html.erb | 8 + .../app/views/comparison/index.html.erb | 216 ++++++++++++++++++ .../app/views/layouts/_layout.html.erb | 4 + .../WEB-INF/app/views/layouts/_tools.html.erb | 1 + .../main/webapp/images/controls/move_down.png | Bin 0 -> 3068 bytes .../main/webapp/images/controls/move_left.png | Bin 0 -> 336 bytes .../webapp/images/controls/move_right.png | Bin 0 -> 342 bytes .../main/webapp/images/controls/move_up.png | Bin 0 -> 3039 bytes 13 files changed, 350 insertions(+), 11 deletions(-) create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/comparison/_versions.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb create mode 100644 sonar-server/src/main/webapp/images/controls/move_down.png create mode 100644 sonar-server/src/main/webapp/images/controls/move_left.png create mode 100644 sonar-server/src/main/webapp/images/controls/move_right.png create mode 100644 sonar-server/src/main/webapp/images/controls/move_up.png diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java index bc3125b2d07..72bd2cb244e 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java @@ -39,27 +39,28 @@ public final class DefaultResourceTypes extends ExtensionProvider implements Bat .setProperty("hasRolePolicy", true) .setProperty("updatable_key", true) .setProperty("supports_measure_filters", true) + .setProperty("comparable", true) .build()) .addType(ResourceType.builder(Qualifiers.MODULE) .setProperty("updatable_key", true) .setProperty("supports_measure_filters", true) .build()) .addType(ResourceType.builder(Qualifiers.DIRECTORY) - .setProperty("supports_measure_filters", true) - .build()) + .setProperty("supports_measure_filters", true) + .build()) .addType(ResourceType.builder(Qualifiers.PACKAGE) - .build()) + .build()) .addType(ResourceType.builder(Qualifiers.FILE) - .hasSourceCode() - .setProperty("supports_measure_filters", true) - .build()) + .hasSourceCode() + .setProperty("supports_measure_filters", true) + .build()) .addType(ResourceType.builder(Qualifiers.CLASS) - .hasSourceCode() - .build()) + .hasSourceCode() + .build()) .addType(ResourceType.builder(Qualifiers.UNIT_TEST_FILE) - .hasSourceCode() - .setProperty("supports_measure_filters", true) - .build()) + .hasSourceCode() + .setProperty("supports_measure_filters", true) + .build()) .addRelations(Qualifiers.PROJECT, Qualifiers.MODULE) .addRelations(Qualifiers.MODULE, Qualifiers.DIRECTORY, Qualifiers.PACKAGE) diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties index c882eef8435..8becc0439de 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -354,6 +354,7 @@ update_key.page=Update Key project_quality_profiles.page=Quality Profiles bulk_deletion.page=Bulk Deletion system_measure_filters.page=System Measure Filters +comparison.page=Comparison # GWT pages @@ -511,6 +512,24 @@ reviews.filtered_by.from=From date reviews.filtered_by.to=To date +#------------------------------------------------------------------------------ +# +# COMPARISON +# +#------------------------------------------------------------------------------ + +comparison.compare=Compare +comparison.add_a_new_metric=Add a new metric +comparison.select_resource_to_compare=Select a resource to compare +comparison.select_version=Select a version +comparison.remove_resource=Remove resource +comparison.remove_metric=Remove metric +comparison.move_left=Move left +comparison.move_right=Move right +comparison.move_down=Move down +comparison.move_up=Move up + + #------------------------------------------------------------------------------ # # ACTION PLANS diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java b/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java index bab797e764b..bda5124bff6 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java @@ -45,8 +45,10 @@ import java.util.Map; *
  • "updatable_key" (since 3.2): if set to "true", then it is possible to update the key of this resource
  • *
  • "supportsGlobalDashboards" (since 3.2): if true, this resource can be displayed in global dashboards
  • *
  • "hasRolePolicy" : if true, roles configuration is available in sidebar
  • + *
  • "comparable" (since 3.4) : if true, the resource can be compared to other resources
  • * * + * @see DefaultResourceTypes in Sonar Core Plugin to see the default resource types * @since 2.14 */ @Beta diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb new file mode 100644 index 00000000000..6de07617062 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb @@ -0,0 +1,87 @@ +# +# Sonar, entreprise quality control 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 +# + +class ComparisonController < ApplicationController + + def index + snapshots = [] + resource_key = params[:resource] + if resource_key && !resource_key.blank? + # the request comes from a project: let's select its 5 latest versions + project = Project.by_key(resource_key) + snapshots = project.events.select { |event| !event.snapshot_id.nil? && event.category==EventCategory::KEY_VERSION }[0..5].reverse.map {|e| e.snapshot} + else + # the request comes from the comparison page: let's compare the given snapshots + sids = get_params_as_array(:sids) + unless sids.empty? + selected_snapshots = Snapshot.find(:all, :conditions => ['id in (?)', sids]) + # next loop is required to keep the order that was decided by the user and which comes from the "sids" parameter + sids.each do |id| + selected_snapshots.each do |s| + snapshots << s if id==s.id.to_s + end + end + end + end + @snapshots = select_authorized(:user, snapshots) + + metrics = get_params_as_array(:metrics) + if metrics.empty? + metrics = [ + 'ncloc', + 'complexity', + 'comment_lines_density', + 'duplicated_lines_density', + 'violations', + 'coverage' + ] + end + @metrics = Metric.by_keys(metrics) + + @metric_to_choose = Metric.all.select {|m| m.display? && !@metrics.include?(m)}.sort_by(&:short_name) + + end + + def versions + key = params[:resource] + sids = get_params_as_array(:sids) + + unless key.blank? + resource = Project.by_key(params[:resource]) + # we look for the events that are versions and that are not linked to snapshots already displayed on the page + @versions = resource.events.select { |event| !event.snapshot_id.nil? && event.category==EventCategory::KEY_VERSION && !sids.include?(event.snapshot_id.to_s) } + end + + render :partial => 'versions' + end + + + private + + def get_params_as_array(name) + list = params[name] + if list.blank? + [] + else + list.split(',') + end + end + +end \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb index 76bc0264591..e44991b4189 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb @@ -718,6 +718,7 @@ module ApplicationHelper # add a select + <% @versions.each do |version| %> + + <% end %> + +<% end %> \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb new file mode 100644 index 00000000000..938441aaff4 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb @@ -0,0 +1,216 @@ + + +
    +
    + + + + + + + + + <% + last_index = @snapshots.size-1 + @snapshots.each_with_index do |s, index| + %> + + <% end %> + + + + + + + + <% + last_index = @metrics.size-1 + @metrics.each_with_index do |m, index| + %> + + + + <% @snapshots.each do |s| %> + + <% end %> + + + + + <% end %> + + +
    + <%= metric_select_tag 'new_metric', @metric_to_choose, { + :allow_empty => true, + :select2_options => {'placeholder' => "'" + message('comparison.add_a_new_metric') + "'"} + } -%> + + + + + + + + +
    + <% if index > 0 %> + + <% else %> + + <% end %> + +
    + <%= s.resource.name(true) -%> +
    + <%= s.event(EventCategory::KEY_VERSION).name -%> +
    + <%= human_short_date s.created_at -%> +
    + <% if index < last_index %> + + <% else %> + + <% end %> +
    +
    + <%= resource_select_tag 'new_resource', { + :resource_type_property => 'comparable', + :width => '250px', + :select2_options => {'placeholder' => "'" + message('comparison.select_resource_to_compare') + "'"} + } -%> + + + + + + + +
    +
    <%= m.short_name -%>
    +
    + <% if index > 0 %> + + <% end %> + <% if index < last_index %> + + <% end %> + +
    +
    + <%= format_measure s.measure(m) -%> +
    + + +
    + + diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb index 9fb3b7bb9cb..ef763e40276 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb @@ -80,6 +80,10 @@ <%= message('components.page') -%>
  • "><%= message('violations_drilldown.page') -%> + <% if controller.java_facade.getResourceTypeBooleanProperty(@project.qualifier, 'comparable') %> +
  • + <%= message('comparison.page') -%>
  • + <% end %>
  • <%= message('clouds.page') -%>
  • <% controller.java_facade.getPages(Navigation::SECTION_RESOURCE, @project.scope, @project.qualifier, @project.language, @project.last_snapshot.metric_keys.to_java(:string)).each do |page| diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb index b1bca085e65..a5c93c4ecf0 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb @@ -4,6 +4,7 @@ diff --git a/sonar-server/src/main/webapp/images/controls/move_down.png b/sonar-server/src/main/webapp/images/controls/move_down.png new file mode 100644 index 0000000000000000000000000000000000000000..2ae468a07e24542a93b5f5708218d097aa888489 GIT binary patch literal 3068 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 KMNUMnLSTZi^4PNg literal 0 HcmV?d00001 diff --git a/sonar-server/src/main/webapp/images/controls/move_left.png b/sonar-server/src/main/webapp/images/controls/move_left.png new file mode 100644 index 0000000000000000000000000000000000000000..8c166e875cf4fa7adf71162a0bb9190b1d057429 GIT binary patch literal 336 zcmV-W0k8gvP)rJj`(b$1Wo;U*n zI&VJ8p8Mec)EzJWH?DsCzi8q8|IKS3|CdfZNt^+W4c9iuPrdWMd-K!(*h#tpAS`UTWr1P*k^jP$+sQKkgar*Y i%oZ}lkB!;Ak3Q}6bX zY(UtGfA@d!p|}4_mpuOO-g0doNd_cMfB1jl!8iXWZGZ89=I)pO6Q|$(uTyk( z3DE`w_uU3+eD%L!)#Lw-s~`XG+W6$ZcgMB=Qc1@i5oLgL%eDU#wm$z~viQOO@+A*| zhCKT3Tz~n$WaP2UL>XXOe(8Vv`X~R37TyQO#>4;iRTutC1Rq!<>bjkUC&T>&^va oB_uJw1OG)Fx6dB9%tR^%0PN`Nzkr|b%707*qoM6N<$f+g6kDgXcg literal 0 HcmV?d00001 diff --git a/sonar-server/src/main/webapp/images/controls/move_up.png b/sonar-server/src/main/webapp/images/controls/move_up.png new file mode 100644 index 0000000000000000000000000000000000000000..83c005763c2f214887b38412f48e73995e55c3ae GIT binary patch literal 3039 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