]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3755 Add action plan WS
authorJulien Lancelot <julien.lancelot@gmail.com>
Mon, 13 May 2013 15:04:58 +0000 (17:04 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Mon, 13 May 2013 15:04:58 +0000 (17:04 +0200)
19 files changed:
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/main/java/org/sonar/core/issue/ActionPlanStats.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultActionPlan.java
sonar-core/src/main/java/org/sonar/core/issue/db/ActionPlanDto.java
sonar-core/src/main/java/org/sonar/core/issue/db/ActionPlanStatsDto.java
sonar-core/src/main/resources/org/sonar/core/issue/db/ActionPlanMapper.xml
sonar-core/src/main/resources/org/sonar/core/issue/db/ActionPlanStatsMapper.xml
sonar-core/src/test/java/org/sonar/core/issue/db/ActionPlanDaoTest.java
sonar-core/src/test/java/org/sonar/core/issue/db/ActionPlanStatsDaoTest.java
sonar-core/src/test/resources/org/sonar/core/issue/db/ActionPlanDaoTest/shared.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/ActionPlanStatsDaoTest/shared.xml [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/issue/ActionPlan.java
sonar-server/src/main/java/org/sonar/server/issue/ActionPlanService.java
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/api/action_plans_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/controllers/issues_action_plans_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/views/issues_action_plans/index.html.erb
sonar-server/src/test/java/org/sonar/server/issue/ActionPlanServiceTest.java
sonar-server/src/test/resources/org/sonar/server/issue/InternalRubyIssueServiceTest.java

index fe8962cb50f0a6158f84db8dcaafb2711b05e335..220bce0cfec1b7e3475e5bd955df3a10b89098ca 100644 (file)
@@ -656,7 +656,8 @@ issues_action_plans.closed_action_plan=Closed action plans
 issues_action_plans.status.OPEN=Open
 issues_action_plans.status.CLOSED=Closed
 issues_action_plans.errors.date.cant_be_in_past=The dead-line can't be in the past
-
+issues_action_plans.errors.action_plan_does_not_exists=Action plan with key {0} does not exists
+issues_action_plans.errors.project_does_not_exists=Project with key {0} does not exists
 
 
 #------------------------------------------------------------------------------
index ae6a533e8ca7dc5582187c4ab7de3137fe66bac4..328b618919154d958b54d514f4959992b5b9b8cb 100644 (file)
@@ -30,6 +30,7 @@ public class ActionPlanStats implements Serializable {
 
   private String key;
   private String name;
+  private String projectKey;
   private String description;
   private String userLogin;
   private String status;
@@ -71,6 +72,15 @@ public class ActionPlanStats implements Serializable {
     return this;
   }
 
+  public String projectKey() {
+    return projectKey;
+  }
+
+  public ActionPlanStats setProjectKey(String projectKey) {
+    this.projectKey = projectKey;
+    return this;
+  }
+
   public String description() {
     return description;
   }
index 8c48c5c5f2c4221186ed4b21defa110398ed2714..40f8797c816d09fbc0b0871328d2862851541618 100644 (file)
@@ -29,6 +29,7 @@ public class DefaultActionPlan implements ActionPlan {
 
   private String key;
   private String name;
+  private String projectKey;
   private String description;
   private String userLogin;
   private String status;
@@ -68,6 +69,15 @@ public class DefaultActionPlan implements ActionPlan {
     return this;
   }
 
+  public String projectKey() {
+    return projectKey;
+  }
+
+  public DefaultActionPlan setProjectKey(String projectKey) {
+    this.projectKey = projectKey;
+    return this;
+  }
+
   public String description() {
     return description;
   }
index d78f7415e9de46865b4d7966a99a8846b9cc4fe6..63c4f8859be86c796a7392ff3155e192f3777827 100644 (file)
@@ -43,6 +43,9 @@ public class ActionPlanDto {
   private Date createdAt;
   private Date updatedAt;
 
+  // joins
+  private transient String projectKey;
+
   public Long getId() {
     return id;
   }
@@ -133,6 +136,18 @@ public class ActionPlanDto {
     return this;
   }
 
+  public String getProjectKey() {
+    return projectKey;
+  }
+
+  /**
+   * Only for unit tests
+   */
+  public ActionPlanDto setProjectKey_unit_test_only(String projectKey) {
+    this.projectKey = projectKey;
+    return this;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
@@ -159,6 +174,7 @@ public class ActionPlanDto {
   public DefaultActionPlan toActionPlan() {
     return DefaultActionPlan.create(name)
       .setKey(kee)
+      .setProjectKey(projectKey)
       .setDescription(description)
       .setStatus(status)
       .setDeadLine(deadLine)
index e94d76c93cd6249bf07bb24cec747d97cd5dbdc7..cb4d04ef00c503ae066c65aa4b1c15c1d195f9d6 100644 (file)
@@ -41,10 +41,10 @@ public class ActionPlanStatsDto {
   private Date deadLine;
   private Date createdAt;
   private Date updatedAt;
-
   private int totalIssues;
   private int openIssues;
-
+  // joins
+  private transient String projectKey;
 
   public Integer getId() {
     return id;
@@ -154,22 +154,35 @@ public class ActionPlanStatsDto {
     return this;
   }
 
+  public String getProjectKey() {
+    return projectKey;
+  }
+
+  /**
+   * Only for unit tests
+   */
+  public ActionPlanStatsDto setProjectKey_unit_test_only(String projectKey) {
+    this.projectKey = projectKey;
+    return this;
+  }
+
   @Override
   public String toString() {
     return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
   }
 
-  public ActionPlanStats toActionPlanStat(){
+  public ActionPlanStats toActionPlanStat() {
     return ActionPlanStats.create(name)
-      .setKey(kee)
-      .setDescription(description)
-      .setStatus(status)
-      .setDeadLine(deadLine)
-      .setUserLogin(userLogin)
-      .setCreatedAt(createdAt)
-      .setUpdatedAt(updatedAt)
-      .setTotalIssues(totalIssues)
-      .setOpenIssues(openIssues);
+             .setKey(kee)
+             .setProjectKey(projectKey)
+             .setDescription(description)
+             .setStatus(status)
+             .setDeadLine(deadLine)
+             .setUserLogin(userLogin)
+             .setCreatedAt(createdAt)
+             .setUpdatedAt(updatedAt)
+             .setTotalIssues(totalIssues)
+             .setOpenIssues(openIssues);
   }
 
 }
index df71967afb2488059b48adb6cc4342e972e0bbef..663284e21ac89f8bcbfd7c0bcfe14e751b2139d0 100644 (file)
@@ -14,7 +14,8 @@
     ap.status as status,
     ap.deadline as deadLine,
     ap.created_at as createdAt,
-    ap.updated_at as updatedAt
+    ap.updated_at as updatedAt,
+    p.kee as projectKey
   </sql>
 
   <insert id="insert" parameterType="ActionPlanIssue" useGeneratedKeys="true" keyProperty="id">
 
   <select id="findByKey" parameterType="long" resultType="ActionPlanIssue">
     select <include refid="actionPlanColumns"/>
-    from action_plans ap
+    from action_plans ap, projects p
     <where>
-      ap.kee=#{key}
+      and ap.kee=#{key}
+      and ap.project_id=p.id
     </where>
   </select>
 
   <select id="findByKeys" parameterType="long" resultType="ActionPlanIssue">
     select <include refid="actionPlanColumns"/>
-    from action_plans ap
+    from action_plans ap, projects p
     <where>
-    <foreach collection="keys" open="ap.kee in (" close=")" item="list" separator=") or ap.kee in (" >
-      <foreach collection="list" item="element" separator=",">
-        #{element}
+      <foreach collection="keys" open="ap.kee in (" close=")" item="list" separator=") or ap.kee in (" >
+        <foreach collection="list" item="element" separator=",">
+          #{element}
+        </foreach>
       </foreach>
-    </foreach>
+      and ap.project_id=p.id
     </where>
   </select>
 
   <select id="findOpenByProjectId" parameterType="long" resultType="ActionPlanIssue">
     select <include refid="actionPlanColumns"/>
-    from action_plans ap
+    from action_plans ap, projects p
     <where>
       and ap.project_id=#{projectId}
       and ap.status='OPEN'
+      and ap.project_id=p.id
     </where>
   </select>
 
   <select id="findByNameAndProjectId" parameterType="long" resultType="ActionPlanIssue">
     select <include refid="actionPlanColumns"/>
-    from action_plans ap
+    from action_plans ap, projects p
     <where>
       and ap.project_id=#{projectId}
       and ap.name=#{name}
+      and ap.project_id=p.id
     </where>
   </select>
 
index 092e4747ddcfbaece6d49286131ea43aad5fec9a..bbe19b21f49c2fb577331de4581bda5c5c9572c2 100644 (file)
     ap.status as status,
     ap.deadline as deadLine,
     ap.created_at as createdAt,
-    ap.updated_at as updatedAt
+    ap.updated_at as updatedAt,
+    p.kee as projectKey
   </sql>
 
   <select id="findByProjectId" parameterType="map" resultType="ActionPlanStats">
     select <include refid="actionPlanColumns"/>, count(total_issues.id) as totalIssues, count(open_issues.id) as openIssues
     from action_plans ap
+    left join projects p on p.id = ap.project_id
     left join issues total_issues on total_issues.action_plan_key = ap.kee
     left join issues open_issues on open_issues.id = total_issues.id and open_issues.resolution is null
     <where>
       and ap.project_id = #{projectId}
     </where>
-    group by ap.id, ap.kee, ap.name, ap.description, ap.user_login, ap.project_id, ap.status, ap.deadline, ap.created_at, ap.updated_at
+    group by ap.id, ap.kee, ap.name, ap.description, ap.user_login, ap.project_id, ap.status, ap.deadline, ap.created_at, ap.updated_at, p.kee
     order by ap.deadline asc
   </select>
 
index 52382cd290079b04342408dec3881cd58a667133..37fb16915ff818a648417c9d12cfcdfd7a32cab8 100644 (file)
@@ -70,16 +70,17 @@ public class ActionPlanDaoTest extends AbstractDaoTestCase {
 
   @Test
   public void should_find_by_key() {
-    setupData("should_find_by_key");
+    setupData("shared", "should_find_by_key");
 
     ActionPlanDto result = dao.findByKey("ABC");
     assertThat(result).isNotNull();
     assertThat(result.getKey()).isEqualTo("ABC");
+    assertThat(result.getProjectKey()).isEqualTo("org.sonar.Sample");
   }
 
   @Test
   public void should_find_by_keys() {
-    setupData("should_find_by_keys");
+    setupData("shared", "should_find_by_keys");
 
     Collection<ActionPlanDto> result = dao.findByKeys(newArrayList("ABC", "ABD", "ABE"));
     assertThat(result).hasSize(3);
@@ -87,7 +88,7 @@ public class ActionPlanDaoTest extends AbstractDaoTestCase {
 
   @Test
   public void should_find_open_by_project_id() {
-    setupData("should_find_open_by_project_id");
+    setupData("shared", "should_find_open_by_project_id");
 
     Collection<ActionPlanDto> result = dao.findOpenByProjectId(1l);
     assertThat(result).hasSize(2);
@@ -95,7 +96,7 @@ public class ActionPlanDaoTest extends AbstractDaoTestCase {
 
   @Test
   public void should_find_by_name_and_project_id() {
-    setupData("should_find_by_name_and_project_id");
+    setupData("shared", "should_find_by_name_and_project_id");
 
     Collection<ActionPlanDto> result = dao.findByNameAndProjectId("SHORT_TERM", 1l);
     assertThat(result).hasSize(2);
index bb5aae2eb57a24a5f352f03e4859f91b47503fae..a346e4bfa44db11e8ce17d7ed983baa8226b0101 100644 (file)
@@ -39,12 +39,13 @@ public class ActionPlanStatsDaoTest extends AbstractDaoTestCase {
 
   @Test
   public void should_find_by_project() {
-    setupData("should_find_by_project");
+    setupData("shared", "should_find_by_project");
 
     Collection<ActionPlanStatsDto> result = dao.findByProjectId(1l);
     assertThat(result).isNotEmpty();
 
     ActionPlanStatsDto actionPlanStatsDto = result.iterator().next();
+    assertThat(actionPlanStatsDto.getProjectKey()).isEqualTo("org.sonar.Sample");
     assertThat(actionPlanStatsDto.getTotalIssues()).isEqualTo(3);
     assertThat(actionPlanStatsDto.getOpenIssues()).isEqualTo(1);
   }
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/ActionPlanDaoTest/shared.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/ActionPlanDaoTest/shared.xml
new file mode 100644 (file)
index 0000000..396db55
--- /dev/null
@@ -0,0 +1,5 @@
+<dataset>
+
+  <projects id="1" kee="org.sonar.Sample" root_id="[null]" />
+
+</dataset>
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/ActionPlanStatsDaoTest/shared.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/ActionPlanStatsDaoTest/shared.xml
new file mode 100644 (file)
index 0000000..396db55
--- /dev/null
@@ -0,0 +1,5 @@
+<dataset>
+
+  <projects id="1" kee="org.sonar.Sample" root_id="[null]" />
+
+</dataset>
index 464622807f9db007f30f3f2252127e83db6415c6..b0b2ee5ceea56afb49e9cb44d59bbeda255957f3 100644 (file)
@@ -40,6 +40,8 @@ public interface ActionPlan extends Serializable {
 
   String name();
 
+  String projectKey();
+
   @CheckForNull
   String description();
 
index 476312684338c28ed0a3989e6dd8604f3244abc4..cfe588fd80083fa16ac4e657e9aa80189c34b680 100644 (file)
@@ -57,13 +57,13 @@ public class ActionPlanService implements ServerComponent {
     this.resourceDao = resourceDao;
   }
 
-  public ActionPlan create(ActionPlan actionPlan, String projectKey){
-    actionPlanDao.save(ActionPlanDto.toActionDto(actionPlan, findProject(projectKey).getId()));
+  public ActionPlan create(ActionPlan actionPlan){
+    actionPlanDao.save(ActionPlanDto.toActionDto(actionPlan, findProject(actionPlan.projectKey()).getId()));
     return actionPlan;
   }
 
-  public ActionPlan update(ActionPlan actionPlan, String projectKey){
-    actionPlanDao.update(ActionPlanDto.toActionDto(actionPlan, findProject(projectKey).getId()));
+  public ActionPlan update(ActionPlan actionPlan){
+    actionPlanDao.update(ActionPlanDto.toActionDto(actionPlan, findProject(actionPlan.projectKey()).getId()));
     return actionPlan;
   }
 
index 8018b6487190d5f156f6d40425d03a7c884e40d7..d7dcd93c7aba137580fd3a8d7f9a7f98cc6cbddd 100644 (file)
@@ -31,6 +31,9 @@ import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.DefaultIssueBuilder;
 import org.sonar.core.issue.workflow.Transition;
+import org.sonar.core.resource.ResourceDao;
+import org.sonar.core.resource.ResourceDto;
+import org.sonar.core.resource.ResourceQuery;
 import org.sonar.server.platform.UserSession;
 
 import java.text.SimpleDateFormat;
@@ -47,13 +50,16 @@ public class InternalRubyIssueService implements ServerComponent {
   private final IssueService issueService;
   private final IssueCommentService commentService;
   private final ActionPlanService actionPlanService;
+  private final ResourceDao resourceDao;
 
   public InternalRubyIssueService(IssueService issueService,
                                   IssueCommentService commentService,
-                                  ActionPlanService actionPlanService) {
+                                  ActionPlanService actionPlanService,
+                                  ResourceDao resourceDao) {
     this.issueService = issueService;
     this.commentService = commentService;
     this.actionPlanService = actionPlanService;
+    this.resourceDao = resourceDao;
   }
 
   public List<Transition> listTransitions(String issueKey) {
@@ -120,44 +126,59 @@ public class InternalRubyIssueService implements ServerComponent {
 
   public Result<ActionPlan> createActionPlan(Map<String, String> parameters) {
     // TODO verify authorization
-    // TODO check existence of projectKey
 
     Result<ActionPlan> result = createActionPlanResult(parameters);
     if (result.ok()) {
-      result.setObject(actionPlanService.create(result.get(), parameters.get("projectKey")));
+      result.setObject(actionPlanService.create(result.get()));
     }
     return result;
   }
 
   public Result<ActionPlan> updateActionPlan(String key, Map<String, String> parameters) {
     // TODO verify authorization
-    // TODO check existence of projectKey
 
     DefaultActionPlan existingActionPlan = (DefaultActionPlan) actionPlanService.findByKey(key);
-    Result<ActionPlan> result = createActionPlanResult(parameters, existingActionPlan.name());
-    if (result.ok()) {
-      String projectKey = parameters.get("projectKey");
-      DefaultActionPlan actionPlan = (DefaultActionPlan) result.get();
-      actionPlan.setKey(existingActionPlan.key());
-      actionPlan.setUserLogin(existingActionPlan.userLogin());
-      result.setObject(actionPlanService.update(actionPlan, projectKey));
+    if (existingActionPlan == null) {
+      Result<ActionPlan> result = new Result<ActionPlan>();
+      result.addError("issues_action_plans.errors.action_plan_does_not_exists", key);
+      return result;
+    } else {
+      Result<ActionPlan> result = createActionPlanResult(parameters, existingActionPlan.name());
+      if (result.ok()) {
+        DefaultActionPlan actionPlan = (DefaultActionPlan) result.get();
+        actionPlan.setKey(existingActionPlan.key());
+        actionPlan.setUserLogin(existingActionPlan.userLogin());
+        result.setObject(actionPlanService.update(actionPlan));
+      }
+      return result;
     }
-    return result;
   }
 
-  public ActionPlan closeActionPlan(String actionPlanKey) {
+  public Result<ActionPlan> closeActionPlan(String actionPlanKey) {
     // TODO verify authorization
-    return actionPlanService.setStatus(actionPlanKey, ActionPlan.STATUS_CLOSED);
+    Result<ActionPlan> result = createResultForExistingActionPlan(actionPlanKey);
+    if (result.ok()) {
+      result.setObject(actionPlanService.setStatus(actionPlanKey, ActionPlan.STATUS_CLOSED));
+    }
+    return result;
   }
 
-  public ActionPlan openActionPlan(String actionPlanKey) {
+  public Result<ActionPlan> openActionPlan(String actionPlanKey) {
     // TODO verify authorization
-    return actionPlanService.setStatus(actionPlanKey, ActionPlan.STATUS_OPEN);
+    Result<ActionPlan> result = createResultForExistingActionPlan(actionPlanKey);
+    if (result.ok()) {
+      result.setObject(actionPlanService.setStatus(actionPlanKey, ActionPlan.STATUS_OPEN));
+    }
+    return result;
   }
 
-  public void deleteActionPlan(String actionPlanKey) {
+  public Result<ActionPlan> deleteActionPlan(String actionPlanKey) {
     // TODO verify authorization
-    actionPlanService.delete(actionPlanKey);
+    Result<ActionPlan> result = createResultForExistingActionPlan(actionPlanKey);
+    if (result.ok()) {
+      actionPlanService.delete(actionPlanKey);
+    }
+    return result;
   }
 
   @VisibleForTesting
@@ -172,7 +193,7 @@ public class InternalRubyIssueService implements ServerComponent {
     String name = parameters.get("name");
     String description = parameters.get("description");
     String deadLineParam = parameters.get("deadLine");
-    String projectParam = parameters.get("projectKey");
+    String projectParam = parameters.get("project");
     Date deadLine = null;
 
     if (Strings.isNullOrEmpty(name)) {
@@ -187,8 +208,13 @@ public class InternalRubyIssueService implements ServerComponent {
       result.addError("errors.is_too_long", "description", 1000);
     }
 
-    if (Strings.isNullOrEmpty(projectParam)) {
+    if (Strings.isNullOrEmpty(projectParam) && oldName == null) {
       result.addError("errors.cant_be_empty", "project");
+    } else {
+      ResourceDto project = resourceDao.getResource(ResourceQuery.create().setKey(projectParam));
+      if (project == null) {
+        result.addError("issues_action_plans.errors.project_does_not_exists", projectParam);
+      }
     }
 
     if (!Strings.isNullOrEmpty(deadLineParam)) {
@@ -208,12 +234,22 @@ public class InternalRubyIssueService implements ServerComponent {
       result.addError("issues_action_plans.same_name_in_same_project");
     }
 
-    DefaultActionPlan actionPlan = DefaultActionPlan.create(name)
-                                     .setDescription(description)
-                                     .setUserLogin(UserSession.get().login())
-                                     .setDeadLine(deadLine);
-    result.setObject(actionPlan);
+    if (result.ok()) {
+      DefaultActionPlan actionPlan = DefaultActionPlan.create(name)
+                                       .setProjectKey(projectParam)
+                                       .setDescription(description)
+                                       .setUserLogin(UserSession.get().login())
+                                       .setDeadLine(deadLine);
+      result.setObject(actionPlan);
+    }
     return result;
   }
 
-}
+  private Result<ActionPlan> createResultForExistingActionPlan(String actionPlanKey) {
+    Result<ActionPlan> result = new Result<ActionPlan>();
+    if (findActionPlan(actionPlanKey) == null) {
+      result.addError("issues_action_plans.errors.action_plan_does_not_exists", actionPlanKey);
+    }
+    return result;
+  }
+}
\ No newline at end of file
index 752fccaf8676c95d60519201eaf9259f1c158c1c..aff192b569f3f634b065d473ae7d784d143401ba 100644 (file)
 
 class Api::ActionPlansController < Api::ApiController
 
+  before_filter :admin_required, :only => [ :create, :delete, :update, :close, :open ]
+
   #
-  # GET /api/issues/search?<parameters>
+  # GET /api/action_plan/show?key=<key>
   #
   # -- Example
-  # curl -v -u admin:admin 'http://localhost:9000/api/issues/search?statuses=OPEN,RESOLVED'
+  # curl -v -u 'http://localhost:9000/api/action_plans/show?key=9b6f89c0-3347-46f6-a6d1-dd6c761240e0'
+  #
+  def show
+    require_parameters :key
+
+    action_plan = Internal.issues.findActionPlan(params[:key])
+    hash = {}
+    hash[:actionPlans] = action_plan_to_hash(action_plan) if action_plan
+
+    respond_to do |format|
+      format.json { render :json => jsonp(hash) }
+      format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'actionPlans') }
+    end
+  end
+
+
+  #
+  # GET /api/action_plans/search?project=<project>
+  #
+  # -- Example
+  # curl -v -u 'http://localhost:9000/api/action_plans/search?project=org.sonar.Sample'
   #
   def search
+    require_parameters :project
+
+    action_plans = Internal.issues.findActionPlanStats(params[:project])
+    hash = {:actionPlans => action_plans.map { |plan| action_plan_to_hash(plan)}}
+
+    respond_to do |format|
+      format.json { render :json => jsonp(hash) }
+      format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'actionPlans') }
+    end
+  end
+
+  #
+  # POST /api/action_plans/create
+  #
+  # -- Mandatory parameters
+  # 'name' is the action plan name
+  # 'project' is the project key to link the action plan to
+  #
+  # -- Optional parameters
+  # 'description' is the plain-text description
+  # 'deadLine' is the due date of the action plan. Format is 'day/month/year', for instance, '31/12/2013'.
+  #
+  # -- Example
+  # curl -X POST -v -u admin:admin 'http://localhost:9000/api/action_plans/create?name=Current&project=org.sonar.Sample'
+  #
+  def create
+    verify_post_request
+    require_parameters :project, :name
+
+    result = Internal.issues.createActionPlan(params)
+    if result.ok()
+      action_plan = result.get()
+      render :json => jsonp({:actionPlan => action_plan_to_hash(action_plan)})
+    else
+      render_error(result)
+    end
+  end
+
+  #
+  # POST /api/action_plans/delete
+  #
+  # -- Mandatory parameters
+  # 'key' is the action plan key
+  #
+  # -- Example
+  # curl -X POST -v -u admin:admin 'http://localhost:9000/api/action_plans/delete?key=9b6f89c0-3347-46f6-a6d1-dd6c761240e0'
+  #
+  def delete
+    verify_post_request
+    require_parameters :key
+
+    result = Internal.issues.deleteActionPlan(params[:key])
+    if result.ok()
+      render_success('Action plan deleted')
+    else
+      render_error(result)
+    end
+  end
+
+  #
+  # POST /api/action_plans/update
+  #
+  # -- Optional parameters
+  # 'name' is the action plan name
+  # 'project' is the project key to link the action plan to
+  # 'description' is the plain-text description
+  # 'deadLine' is the due date of the action plan. Format is 'day/month/year', for instance, '31/12/2013'.
+  #
+  # -- Example
+  # curl -X POST -v -u admin:admin 'http://localhost:9000/api/action_plans/update?key=9b6f89c0-3347-46f6-a6d1-dd6c761240e0&name=Current'
+  #
+  def update
+    verify_post_request
+    require_parameters :key
+
+    result = Internal.issues.updateActionPlan(params[:key], params)
+    if result.ok()
+      action_plan = result.get()
+      render :json => jsonp({:actionPlan => action_plan_to_hash(action_plan)})
+    else
+      render_error(result)
+    end
+  end
+
+  #
+  # POST /api/action_plans/close
+  #
+  # -- Mandatory parameters
+  # 'key' is the action plan key
+  #
+  # -- Example
+  # curl -X POST -v -u admin:admin 'http://localhost:9000/api/action_plans/close?key=9b6f89c0-3347-46f6-a6d1-dd6c761240e0'
+  #
+  def close
+    verify_post_request
+    require_parameters :key
+
+    result = Internal.issues.closeActionPlan(params[:key])
+    if result.ok()
+      action_plan = result.get()
+      render :json => jsonp({:actionPlan => action_plan_to_hash(action_plan)})
+    else
+      render_error(result)
+    end
+  end
+
+  #
+  # POST /api/action_plans/open
+  #
+  # -- Mandatory parameters
+  # 'key' is the action plan key
+  #
+  # -- Example
+  # curl -X POST -v -u admin:admin 'http://localhost:9000/api/action_plans/open?key=9b6f89c0-3347-46f6-a6d1-dd6c761240e0'
+  #
+  def open
+    verify_post_request
+    require_parameters :key
+
+    result = Internal.issues.openActionPlan(params[:key])
+    if result.ok()
+      action_plan = result.get()
+      render :json => jsonp({:actionPlan => action_plan_to_hash(action_plan)})
+    else
+      render_error(result)
+    end
+  end
+
+
+  private
+
+  def action_plan_to_hash(action_plan)
+    hash = {:key => action_plan.key(), :name => action_plan.name(), :status => action_plan.status()}
+    hash[:project] = action_plan.projectKey() if action_plan.projectKey() && !action_plan.projectKey().blank?
+    hash[:desc] = action_plan.description() if action_plan.description() && !action_plan.description().blank?
+    hash[:userLogin] = action_plan.userLogin() if action_plan.userLogin()
+    hash[:deadLine] = Api::Utils.format_datetime(action_plan.deadLine()) if action_plan.deadLine()
+    hash[:totalIssues] = action_plan.totalIssues() if action_plan.respond_to?('totalIssues')
+    hash[:openIssues] = action_plan.openIssues() if action_plan.respond_to?('openIssues')
+    hash[:createdAt] = Api::Utils.format_datetime(action_plan.createdAt()) if action_plan.createdAt()
+    hash[:updatedAt] = Api::Utils.format_datetime(action_plan.updatedAt()) if action_plan.updatedAt()
+    hash
+  end
+
+  def error_to_hash(msg)
+    {:msg => message(msg.text(), {:params => msg.params()}).capitalize}
+  end
 
+  def render_error(result)
+    hash = {:errors => result.errors().map { |error| error_to_hash(error) }}
+    respond_to do |format|
+      format.json { render :json => jsonp(hash), :status => 400}
+      format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'errors', :status => 400)}
+    end
   end
 
 end
\ No newline at end of file
index e336377b6a5c3237bc10f177f0e60bc320374f6a..f4b79293ebfec2f7a48e03f8100126a229ec5d6e 100644 (file)
@@ -35,7 +35,7 @@ class IssuesActionPlansController < ApplicationController
   end
 
   def save
-    options = {'projectKey' => @resource.key, 'name' => params[:name], 'description' => params[:description], 'deadLine' => params[:deadline]}
+    options = {'project' => @resource.key, 'name' => params[:name], 'description' => params[:description], 'deadLine' => params[:deadline]}
 
     exiting_action_plan = find_by_key(params[:plan_key]) unless params[:plan_key].blank?
     if exiting_action_plan
index 534239ded10010638a823911a167c92df9430578..62a38dc67b263b2b5545d65dc1640397cc3bd1d5 100644 (file)
@@ -81,7 +81,7 @@
           <%
              @closed_action_plans.each do |plan|
                deadline = Api::Utils.java_to_ruby_datetime(plan.deadLine()) if plan.deadLine()
-               updated_at = Api::Utils.java_to_ruby_datetime(plan.updateDate())
+               updated_at = Api::Utils.java_to_ruby_datetime(plan.updatedAt())
           %>
             <tr>
               <td class="thin nowrap center"><img src="<%= ApplicationController.root_context -%>/images/status/<%= plan.status() -%>.png" title="<%= message(plan.status()) -%>"/></td>
index c33adff4d1754902ccc845e5d9716d3961950539..203460b97d6bd72f52e67c0c4a1e1ae704e395ba 100644 (file)
@@ -57,7 +57,7 @@ public class ActionPlanServiceTest {
     when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(new ResourceDto().setKey("org.sonar.Sample").setId(1l));
     ActionPlan actionPlan = DefaultActionPlan.create("Long term");
 
-    actionPlanService.create(actionPlan, "org.sonar.Sample");
+    actionPlanService.create(actionPlan);
     verify(actionPlanDao).save(any(ActionPlanDto.class));
   }
 
index 08fedad68e692575a873e83888b68048691e1921..d13d50de187295d4141118432555a9c67157ba78 100644 (file)
@@ -24,6 +24,10 @@ import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.sonar.api.issue.ActionPlan;
+import org.sonar.core.issue.DefaultActionPlan;
+import org.sonar.core.resource.ResourceDao;
+import org.sonar.core.resource.ResourceDto;
+import org.sonar.core.resource.ResourceQuery;
 
 import java.util.Map;
 
@@ -35,29 +39,31 @@ import static org.mockito.Mockito.*;
 public class InternalRubyIssueServiceTest {
 
   private InternalRubyIssueService internalRubyIssueService;
-
   private IssueService issueService = mock(IssueService.class);
   private IssueCommentService commentService = mock(IssueCommentService.class);
   private ActionPlanService actionPlanService = mock(ActionPlanService.class);
+  private ResourceDao resourceDao = mock(ResourceDao.class);
 
   @Before
-  public void before(){
-    internalRubyIssueService = new InternalRubyIssueService(issueService, commentService, actionPlanService);
+  public void before() {
+    ResourceDto project = new ResourceDto().setKey("org.sonar.Sample");
+    when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(project);
+    internalRubyIssueService = new InternalRubyIssueService(issueService, commentService, actionPlanService, resourceDao);
   }
 
   @Test
-  public void should_create_action_plan(){
+  public void should_create_action_plan() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", "Long term");
     parameters.put("description", "Long term issues");
-    parameters.put("projectKey", "org.sonar.Sample");
+    parameters.put("project", "org.sonar.Sample");
     parameters.put("deadLine", "13/05/2113");
 
     ArgumentCaptor<ActionPlan> actionPlanCaptor = ArgumentCaptor.forClass(ActionPlan.class);
     Result result = internalRubyIssueService.createActionPlan(parameters);
     assertThat(result.ok()).isTrue();
 
-    verify(actionPlanService).create(actionPlanCaptor.capture(), eq("org.sonar.Sample"));
+    verify(actionPlanService).create(actionPlanCaptor.capture());
     ActionPlan actionPlan = actionPlanCaptor.getValue();
 
     assertThat(actionPlan).isNotNull();
@@ -68,7 +74,101 @@ public class InternalRubyIssueServiceTest {
   }
 
   @Test
-  public void should_get_error_on_action_plan_result_when_no_project(){
+  public void should_update_action_plan() {
+    when(actionPlanService.findByKey("ABCD")).thenReturn(DefaultActionPlan.create("Long term"));
+
+    Map<String, String> parameters = newHashMap();
+    parameters.put("name", "New Long term");
+    parameters.put("description", "New Long term issues");
+    parameters.put("deadLine", "13/05/2113");
+
+    ArgumentCaptor<ActionPlan> actionPlanCaptor = ArgumentCaptor.forClass(ActionPlan.class);
+    Result result = internalRubyIssueService.updateActionPlan("ABCD", parameters);
+    assertThat(result.ok()).isTrue();
+
+    verify(actionPlanService).update(actionPlanCaptor.capture());
+    ActionPlan actionPlan = actionPlanCaptor.getValue();
+
+    assertThat(actionPlan).isNotNull();
+    assertThat(actionPlan.key()).isNotNull();
+    assertThat(actionPlan.name()).isEqualTo("New Long term");
+    assertThat(actionPlan.description()).isEqualTo("New Long term issues");
+    assertThat(actionPlan.deadLine()).isNotNull();
+  }
+
+  @Test
+  public void should_update_action_plan_with_new_project() {
+    when(actionPlanService.findByKey("ABCD")).thenReturn(DefaultActionPlan.create("Long term"));
+
+    Map<String, String> parameters = newHashMap();
+    parameters.put("name", "New Long term");
+    parameters.put("description", "New Long term issues");
+    parameters.put("deadLine", "13/05/2113");
+    parameters.put("project", "org.sonar.MultiSample");
+
+    ArgumentCaptor<ActionPlan> actionPlanCaptor = ArgumentCaptor.forClass(ActionPlan.class);
+    Result result = internalRubyIssueService.updateActionPlan("ABCD", parameters);
+    assertThat(result.ok()).isTrue();
+
+    verify(actionPlanService).update(actionPlanCaptor.capture());
+    ActionPlan actionPlan = actionPlanCaptor.getValue();
+
+    assertThat(actionPlan).isNotNull();
+    assertThat(actionPlan.key()).isNotNull();
+    assertThat(actionPlan.name()).isEqualTo("New Long term");
+    assertThat(actionPlan.description()).isEqualTo("New Long term issues");
+    assertThat(actionPlan.deadLine()).isNotNull();
+    assertThat(actionPlan.projectKey()).isEqualTo("org.sonar.MultiSample");
+  }
+
+  @Test
+  public void should_not_update_action_plan_when_action_plan_is_not_found() {
+    when(actionPlanService.findByKey("ABCD")).thenReturn(null);
+
+    Result result = internalRubyIssueService.updateActionPlan("ABCD", null);
+    assertThat(result.ok()).isFalse();
+    assertThat(result.errors()).contains(new Result.Message("issues_action_plans.errors.action_plan_does_not_exists", "ABCD"));
+  }
+
+  @Test
+  public void should_delete_action_plan() {
+    when(actionPlanService.findByKey("ABCD")).thenReturn(DefaultActionPlan.create("Long term"));
+
+    Result result = internalRubyIssueService.deleteActionPlan("ABCD");
+    verify(actionPlanService).delete("ABCD");
+    assertThat(result.ok()).isTrue();
+  }
+
+  @Test
+  public void should_not_delete_action_plan_if_action_plan_not_found() {
+    when(actionPlanService.findByKey("ABCD")).thenReturn(null);
+
+    Result result = internalRubyIssueService.deleteActionPlan("ABCD");
+    verify(actionPlanService, never()).delete("ABCD");
+    assertThat(result.ok()).isFalse();
+    assertThat(result.errors()).contains(new Result.Message("issues_action_plans.errors.action_plan_does_not_exists", "ABCD"));
+  }
+
+  @Test
+  public void should_close_action_plan() {
+    when(actionPlanService.findByKey("ABCD")).thenReturn(DefaultActionPlan.create("Long term"));
+
+    Result result = internalRubyIssueService.closeActionPlan("ABCD");
+    verify(actionPlanService).setStatus(eq("ABCD"), eq("CLOSED"));
+    assertThat(result.ok()).isTrue();
+  }
+
+  @Test
+  public void should_open_action_plan() {
+    when(actionPlanService.findByKey("ABCD")).thenReturn(DefaultActionPlan.create("Long term"));
+
+    Result result = internalRubyIssueService.openActionPlan("ABCD");
+    verify(actionPlanService).setStatus(eq("ABCD"), eq("OPEN"));
+    assertThat(result.ok()).isTrue();
+  }
+
+  @Test
+  public void should_get_error_on_action_plan_result_when_no_project() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", "Long term");
     parameters.put("description", "Long term issues");
@@ -79,11 +179,11 @@ public class InternalRubyIssueServiceTest {
   }
 
   @Test
-  public void should_get_error_on_action_plan_result_when_no_name(){
+  public void should_get_error_on_action_plan_result_when_no_name() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", null);
     parameters.put("description", "Long term issues");
-    parameters.put("projectKey", "org.sonar.Sample");
+    parameters.put("project", "org.sonar.Sample");
 
     Result result = internalRubyIssueService.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
@@ -91,11 +191,11 @@ public class InternalRubyIssueServiceTest {
   }
 
   @Test
-  public void should_get_error_on_action_plan_result_when_name_is_too_long(){
+  public void should_get_error_on_action_plan_result_when_name_is_too_long() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", createLongString(201));
     parameters.put("description", "Long term issues");
-    parameters.put("projectKey", "org.sonar.Sample");
+    parameters.put("project", "org.sonar.Sample");
 
     Result result = internalRubyIssueService.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
@@ -103,11 +203,11 @@ public class InternalRubyIssueServiceTest {
   }
 
   @Test
-  public void should_get_error_on_action_plan_result_when_description_is_too_long(){
+  public void should_get_error_on_action_plan_result_when_description_is_too_long() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", "Long term");
     parameters.put("description", createLongString(1001));
-    parameters.put("projectKey", "org.sonar.Sample");
+    parameters.put("project", "org.sonar.Sample");
 
     Result result = internalRubyIssueService.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
@@ -115,11 +215,11 @@ public class InternalRubyIssueServiceTest {
   }
 
   @Test
-  public void should_get_error_on_action_plan_result_when_dead_line_use_wrong_format(){
+  public void should_get_error_on_action_plan_result_when_dead_line_use_wrong_format() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", "Long term");
     parameters.put("description", "Long term issues");
-    parameters.put("projectKey", "org.sonar.Sample");
+    parameters.put("project", "org.sonar.Sample");
     parameters.put("deadLine", "2013-05-18");
 
     Result result = internalRubyIssueService.createActionPlanResult(parameters);
@@ -128,11 +228,11 @@ public class InternalRubyIssueServiceTest {
   }
 
   @Test
-  public void should_get_error_on_action_plan_result_when_dead_line_is_in_the_past(){
+  public void should_get_error_on_action_plan_result_when_dead_line_is_in_the_past() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", "Long term");
     parameters.put("description", "Long term issues");
-    parameters.put("projectKey", "org.sonar.Sample");
+    parameters.put("project", "org.sonar.Sample");
     parameters.put("deadLine", "01/01/2000");
 
     Result result = internalRubyIssueService.createActionPlanResult(parameters);
@@ -141,11 +241,11 @@ public class InternalRubyIssueServiceTest {
   }
 
   @Test
-  public void should_get_error_on_action_plan_result_when_name_is_already_used_for_project(){
+  public void should_get_error_on_action_plan_result_when_name_is_already_used_for_project() {
     Map<String, String> parameters = newHashMap();
     parameters.put("name", "Long term");
     parameters.put("description", "Long term issues");
-    parameters.put("projectKey", "org.sonar.Sample");
+    parameters.put("project", "org.sonar.Sample");
 
     when(actionPlanService.isNameAlreadyUsedForProject(anyString(), anyString())).thenReturn(true);
 
@@ -154,9 +254,22 @@ public class InternalRubyIssueServiceTest {
     assertThat(result.errors()).contains(new Result.Message("issues_action_plans.same_name_in_same_project"));
   }
 
-  public String createLongString(int size){
+  @Test
+  public void should_get_error_on_action_plan_result_when_project_not_found() {
+    Map<String, String> parameters = newHashMap();
+    parameters.put("name", "Long term");
+    parameters.put("description", "Long term issues");
+    parameters.put("project", "org.sonar.Sample");
+
+    when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(null);
+
+    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    assertThat(result.ok()).isFalse();
+    assertThat(result.errors()).contains(new Result.Message("issues_action_plans.errors.project_does_not_exists", "org.sonar.Sample"));
+  }
+  public String createLongString(int size) {
     String result = "";
-    for (int i = 0; i<size; i++) {
+    for (int i = 0; i < size; i++) {
       result += "c";
     }
     return result;