]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2662 Create action plans which are a set of manual reviews
authorFabrice Bellingard <bellingard@gmail.com>
Fri, 16 Dec 2011 11:37:11 +0000 (12:37 +0100)
committerFabrice Bellingard <bellingard@gmail.com>
Mon, 19 Dec 2011 07:36:48 +0000 (08:36 +0100)
- Create the service to manage action plans

plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/main/java/org/sonar/jpa/entity/SchemaMigration.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/action_plans_controller.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/models/action_plan.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/models/review.rb
sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/_progress.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/index.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/new.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb
sonar-server/src/main/webapp/WEB-INF/db/migrate/238_create_action_plans.rb [new file with mode: 0644]
sonar-server/src/main/webapp/stylesheets/style.css

index 98922d6175e8a5e6111874fe60807b48f39416d0..b07942374378662758fd22dc495c46043085ec54 100644 (file)
@@ -261,6 +261,7 @@ sidebar.system=System
 #
 #------------------------------------------------------------------------------
 
+action_plans.page=Action Plans
 backup.page=Backup
 clouds.page=Clouds
 components.page=Components
@@ -400,6 +401,31 @@ reviews.status.CLOSED=Closed
 reviews.resolution.FALSE-POSITIVE=False-positive
 reviews.resolution.FIXED=Fixed
 
+
+#------------------------------------------------------------------------------
+#
+# ACTION PLANS
+#
+#------------------------------------------------------------------------------
+
+action_plans.page_title=Manage Action plans
+action_plans.add_action_plan=Add action plan
+action_plans.col.status=St.
+action_plans.col.name=Name
+action_plans.col.due_for=Due for
+action_plans.col.progress=Progress
+action_plans.col.description=Description
+action_plans.col.author=Author
+action_plans.col.operations=Operations
+action_plans.no_action_plan=No action plan
+action_plans.confirm_delete=Delete this action plan? Associated reviews will not be deleted.
+action_plans.create_new_action_plan=Create a new action plan
+action_plans.date_format_help=The date should be entered using the following pattern: 'day/month/year'. For instance, '31/12/2011'.
+action_plans.date_not_valid=Date not valid
+action_plans.date_cant_be_in_past=The dead-line can't be in the past
+action_plans.x_out_of_x_reviews_solved={0} of {1} reviews solved
+
+
 #------------------------------------------------------------------------------
 #
 # DEPENDENCIES
index 1b69803b7c855acced8db6a23265706fdf3cea65..a6e83a345f1f488d7f7906a80fd60456402d0c6e 100644 (file)
@@ -42,7 +42,7 @@ public class SchemaMigration {
       - complete the Derby DDL file used for unit tests : sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl
 
    */
-  public static final int LAST_VERSION = 237;
+  public static final int LAST_VERSION = 238;
 
   public final static String TABLE_NAME = "schema_migrations";
 
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/action_plans_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/action_plans_controller.rb
new file mode 100644 (file)
index 0000000..f12a9a9
--- /dev/null
@@ -0,0 +1,77 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2011 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 ActionPlansController < ApplicationController
+
+  SECTION=Navigation::SECTION_RESOURCE
+  before_filter :load_resource
+  verify :method => :post, :only => [:save, :delete], :redirect_to => {:action => :index}
+
+  def index
+    @action_plans = ActionPlan.find(:all, :conditions => ['project_id=?', @resource.id], :include => 'reviews', :order => 'dead_line ASC')
+  end
+
+  def new
+    if params[:name] || params[:description] || params[:dead_line]
+      @action_plan = ActionPlan.new
+    elsif params[:plan_id]
+      @action_plan = ActionPlan.find params[:plan_id]
+    end
+  end
+
+  def save
+    @action_plan = ActionPlan.find params[:plan_id] unless params[:plan_id].blank?
+    unless @action_plan
+      @action_plan = ActionPlan.new(:user_login => current_user.login,
+                                    :project_id => @resource.id,
+                                    :status => ActionPlan::STATUS_OPEN)
+    end
+    @action_plan.name = params[:name]
+    @action_plan.description = params[:description]
+    begin
+      @action_plan.dead_line = Date.strptime(params[:dead_line], '%d/%m/%Y') unless params[:dead_line].blank?
+    rescue
+      date_not_valid = message('action_plans.date_not_valid')
+    end
+
+    if date_not_valid || !@action_plan.valid?
+      @action_plan.errors.add :dead_line, date_not_valid if date_not_valid
+      render :action => :new, :id => @resource.id
+    else
+      @action_plan.save
+      redirect_to :action => 'index', :id => @resource.id
+    end
+  end
+
+  def delete
+    action_plan = ActionPlan.find params[:plan_id]
+    action_plan.destroy
+    redirect_to :action => 'index', :id => @resource.id
+  end
+
+  private
+
+  def load_resource
+    @resource=Project.by_key(params[:id])
+    return redirect_to home_path unless @resource
+    access_denied unless has_role?(:admin, @resource)
+  end
+
+end
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/action_plan.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/action_plan.rb
new file mode 100644 (file)
index 0000000..34e2085
--- /dev/null
@@ -0,0 +1,65 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2011 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 ActionPlan < ActiveRecord::Base
+  belongs_to :project
+  has_and_belongs_to_many :reviews
+
+  validates_uniqueness_of :name
+  validates_length_of :name, :within => 1..200
+  validates_length_of :description, :maximum => 1000, :allow_blank => true, :allow_nil => true
+  validates_presence_of :user_login, :message => "can't be empty"
+  validates_presence_of :status, :message => "can't be empty"
+  validates_presence_of :project, :message => "can't be empty"
+  validate :dead_line_cannot_be_in_the_past
+
+  STATUS_OPEN = 'OPEN'
+  STATUS_CLOSED = 'CLOSED'
+  
+  def user
+    @user ||=
+        begin
+          user_login ? User.find(:first, :conditions => ['login=?', user_login]) : nil
+        end
+  end
+  
+  def closed?
+    status == STATUS_CLOSED
+  end
+
+  def open?
+    status == STATUS_OPEN
+  end
+  
+  def progress
+    total_reviews = reviews.size
+    open_reviews = reviews.select{|r| r.open? || r.reopened?}.size
+    {:total => total_reviews, :open => open_reviews}
+  end
+
+  private
+
+  def dead_line_cannot_be_in_the_past
+    if !dead_line.blank? and dead_line < Date.today
+      errors.add(:dead_line, Api::Utils.message('action_plans.date_cant_be_in_past'))
+    end
+  end
+
+end
index 97c509b52f8d89324610a678e89a68fdeddae9ae..1d3740b9405120ab94db56188b3bc6610ab1e77b 100644 (file)
@@ -25,6 +25,7 @@ class Review < ActiveRecord::Base
   belongs_to :rule
   has_many :review_comments, :order => "created_at", :dependent => :destroy
   alias_attribute :comments, :review_comments
+  has_and_belongs_to_many :action_plans
 
   validates_presence_of :user, :message => "can't be empty"
   validates_presence_of :status, :message => "can't be empty"
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/_progress.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/_progress.html.erb
new file mode 100644 (file)
index 0000000..9ba9f66
--- /dev/null
@@ -0,0 +1,15 @@
+<%
+  open_reviews_url = link_to action_plan.progress[:open].to_s, 
+                             :controller => 'reviews', :action => 'index', 
+                             :action_plans => [action_plan.id], :statuses => [Review::STATUS_OPEN, Review::STATUS_REOPENED], :assignee_login => ''
+  
+  total_reviews_url = link_to action_plan.progress[:total].to_s, 
+                              :controller => 'reviews', :action => 'index', 
+                              :action_plans => [action_plan.id], :statuses => [''], :assignee_login => ''
+%>
+<div class="progress">
+  <div class="bar">
+    <div style="width:<%= action_plan.progress[:total]==0 ? '0' : action_plan.progress[:open]*100/action_plan.progress[:total] -%>%;"> </div>
+  </div>
+  <div class="note"><%= message('action_plans.x_out_of_x_reviews_solved', :params => [open_reviews_url, total_reviews_url]) -%></div>
+</div>
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/index.html.erb
new file mode 100644 (file)
index 0000000..d5d21ca
--- /dev/null
@@ -0,0 +1,56 @@
+<style type="text/css">
+  #actionPlans td {
+    vertical-align: top;
+  }
+  #actionPlans td.progress {
+    padding: 0px 40px;
+  }    
+</style>
+
+<div class="line-block marginbottom10">
+  <ul class="operations">
+    <li class="last">
+      <%= image_tag 'add.png' -%>
+      <%= link_to message('action_plans.add_action_plan'), {:action => 'new', :id => @resource.id}, {:id => 'addActionPlan'} -%>
+    </li>
+  </ul>
+  <h1><%= message('action_plans.page_title') -%></h1>
+</div>
+
+<table class="width100 data sortable" id="actionPlans">
+  <thead>
+  <tr>
+    <th class="thin nowrap"><%= message('action_plans.col.status') -%></th>
+    <th class="thin nowrap"><%= message('action_plans.col.name') -%></th>
+    <th class="thin nowrap righticon" style="text-align: right"><%= message('action_plans.col.due_for') -%></th>
+    <th class="nowrap nosort center"><%= message('action_plans.col.progress') -%></th>
+    <th class="nowrap"><%= message('action_plans.col.description') -%></th>
+    <th class="nowrap"><%= message('action_plans.col.author') -%></th>
+    <th class="thin nowrap nosort"><%= message('action_plans.col.operations') -%></th>
+  </tr>
+  </thead>
+  <tbody>
+  <% if @action_plans.empty? %>
+    <td colspan="7" class="even"><%= message('action_plans.no_action_plan') -%></td>
+  <% end %>
+  <%
+     @action_plans.each do |plan|
+  %>
+    <tr>
+      <td class="thin nowrap center"><img src="<%= ApplicationController.root_context -%>/images/status/<%= plan.status -%>.png" title="<%= message(plan.status.downcase).capitalize -%>"/></td>
+      <td class="thin nowrap"><%= h(plan.name) -%></td>
+      <td class="thin nowrap" align="right"><%= plan.dead_line ? plan.dead_line.strftime('%Y/%m/%d') : ' '  -%></td>
+      <td class="progress thin">
+        <%= render :partial => 'progress', :locals => {:action_plan => plan} -%>
+      </td>
+      <td id="desc"><%= h(plan.description) -%></td>
+      <td id="desc"><%= h(plan.user.name) -%></td>
+      <td class="thin nowrap">
+        <a href="<%= url_for :action => 'new', :id => @resource.id, :plan_id => plan.id -%>"><%= message('edit') -%></a>
+        <%= link_to message('delete'), {:action => 'delete', :id => @resource.id, :plan_id => plan.id}, {:method => 'POST', :confirm => message('action_plans.confirm_delete'), :class => 'action'} -%>
+      </td>
+    </tr>
+  <% end %>
+  </tbody>
+</table>
+<script>TableKit.Sortable.init('actionPlans');</script>
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/new.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/action_plans/new.html.erb
new file mode 100644 (file)
index 0000000..b2c62dd
--- /dev/null
@@ -0,0 +1,65 @@
+<h1 class="marginbottom10"><%= message('action_plans.create_new_action_plan') -%></h1>
+
+<% if @action_plan && @action_plan.errors.on_base
+     @action_plan.errors.on_base.each do |error| %>
+  <div class="error"><%= error -%></div>
+<%   end
+   end
+%>
+
+<form action="<%= url_for :action => 'save' -%>" method="POST" id="createForm">
+  <input type="hidden" name="id" value="<%= @resource.id -%>"/>
+  <input type="hidden" name="plan_id" value="<%= @action_plan.id if @action_plan -%>"/>
+  <table class="width100 form">
+    <tbody>
+      <tr>
+        <td class="keyCell">
+          <%= message('action_plans.col.name') -%>:
+        </td>
+        <td>
+          <input type="text" name="name" id="name" value="<%= @action_plan ? @action_plan.name : '' -%>"/>
+          <% if @action_plan && @action_plan.errors.on('name')
+               @action_plan.errors.on('name').each do |error| %>
+            <span class="error"><%= error -%></span>
+          <%   end
+             end %>
+        </td>
+      </tr>
+      <tr>
+        <td class="keyCell">
+          <%= message('action_plans.col.due_for') -%>:
+        </td>
+        <td>
+          <input type="text" name="dead_line" id="dead_line" value="<%= @action_plan && @action_plan.dead_line ? @action_plan.dead_line.strftime('%d/%m/%Y') : params[:dead_line] -%>"/>
+          <span class="note"><%= message('action_plans.date_format_help') -%></span>
+          <% if @action_plan && @action_plan.errors.on('dead_line')
+               @action_plan.errors.on('dead_line').each do |error| %>
+            <span class="error"><%= error -%></span>
+          <%   end
+             end %>
+        </td>
+      </tr>
+      <tr>
+        <td class="keyCell">
+          <%= message('action_plans.col.description') -%>:
+        </td>
+        <td>
+          <textarea rows="5" cols="80" name="description" id="description" class="width100"><%= @action_plan ? @action_plan.description : '' -%></textarea>
+          <% if @action_plan && @action_plan.errors.on('description')
+               @action_plan.errors.on('description').each do |error| %>
+            <span class="error"><%= error -%></span>
+          <%   end
+             end %>
+        </td>
+      </tr>
+      <tr>
+        <td class="keyCell">
+        </td>
+        <td>
+          <input type="submit" value="<%= message('save') -%>"/>
+          <%= link_to message('cancel'), :action => 'index', :id => @resource.id -%>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</form>
\ No newline at end of file
index 624195af452563d6fd116b91b80e3f21e5658d26..d88104aad2fed0d45c1ac859a54c2a2e0b1db43f 100644 (file)
@@ -71,6 +71,8 @@
               <li class="h2"><%= message('sidebar.project_settings') -%></li>
               <li class="<%= 'selected' if request.request_uri.include?('/manual_measures') -%>">
                 <a href="<%= ApplicationController.root_context -%>/manual_measures/index/<%= @project.id -%>"><%= message('manual_measures.page') -%></a></li>
+              <li class="<%= 'selected' if request.request_uri.include?('/action_plans') -%>">
+                <a href="<%= ApplicationController.root_context -%>/action_plans/index/<%= @project.id -%>"><%= message('action_plans.page') -%></a></li>
               <% if (@project.project? || @project.module?) %>
                 <li class="<%= 'selected' if request.request_uri.include?('/project/settings') -%>">
                   <a href="<%= ApplicationController.root_context -%>/project/settings/<%= @project.id -%>"><%= message('project_settings.page') -%></a></li>
diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/238_create_action_plans.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/238_create_action_plans.rb
new file mode 100644 (file)
index 0000000..ed80268
--- /dev/null
@@ -0,0 +1,48 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2011 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
+#
+
+#
+# Sonar 2.13
+#
+class CreateActionPlans < ActiveRecord::Migration
+
+  def self.up
+    create_table 'action_plans' do |t|
+      t.timestamps
+      t.column 'name',          :string,      :null => true,    :limit => 200
+      t.column 'description',   :string,      :null => true,    :limit => 1000
+      t.column 'dead_line',     :datetime,    :null => true
+      t.column 'user_login',    :string,      :null => true,    :limit => 40
+      t.column 'project_id',    :integer,     :null => true
+      t.column 'status',        :string,      :null => true,    :limit => 10
+    end
+    alter_to_big_primary_key('action_plans')
+    add_index "action_plans", "project_id"
+    
+    create_table :action_plans_reviews, :id => false do |t|
+      t.integer :action_plan_id
+      t.integer :review_id
+    end    
+    add_index "action_plans_reviews", "action_plan_id"
+    add_index "action_plans_reviews", "review_id"
+    
+  end
+
+end
index 7881cffb18f2949051232ec0d7776bc9f93c5f53..7b44c075b92fd8a83276b36da8133f21dd0cc742 100644 (file)
@@ -711,6 +711,10 @@ table.spacedicon td {
   width: 1%;
 }
 
+.max-width {
+  width: 100%;
+}
+
 td.sep {
   width: 10px;
 }
@@ -1219,10 +1223,33 @@ div.comment-excerpt {
   font-size: 90%;
 }
 
-.max-width {
+/* ACTION PLANS */
+
+div.progress {
+  width: 400px;
+  margin: 4px;
+}
+
+div.progress div.bar {
   width: 100%;
+  background-color: #CC0000;
+  border: none;
+  margin-bottom: 4px;
 }
 
+div.progress > div > div {
+  height: 10px;
+  background-color: #078C00;
+}
+
+div.progress div.note {
+  color: #777777;
+  font-size: 93%;
+  font-weight: normal;
+  white-space: nowrap;
+}
+
+
 /* AUTOCOMPLETE FIELDS */
 div.autocomplete {
   position: absolute;