]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4383 Save issue filter
authorJulien Lancelot <julien.lancelot@gmail.com>
Thu, 13 Jun 2013 14:19:11 +0000 (16:19 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Thu, 13 Jun 2013 14:19:11 +0000 (16:19 +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/DefaultIssueFilter.java
sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueFilterTest.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/issue/db/IssueFilterDaoTest.java
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/java/org/sonar/server/issue/IssueFilterService.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/issues_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/models/issue_filter.rb [deleted file]
sonar-server/src/main/webapp/WEB-INF/app/views/issues/_action_links.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/issues/_list.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/issues/_save_as_form.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/issues/_sidebar.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/issues/search.html.erb
sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueFilterServiceTest.java
sonar-ws-client/src/main/java/org/sonar/wsclient/issue/ActionPlan.java
sonar-ws-client/src/main/java/org/sonar/wsclient/issue/Issue.java
sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueComment.java
sonar-ws-client/src/main/java/org/sonar/wsclient/issue/Issues.java

index ada129c357d570a5df05721726219f6205cf5e61..6caab7f672aa8704f84d72b6243fb1290dbcc8c8 100644 (file)
@@ -530,6 +530,10 @@ issue_filter.criteria.severity=Severity
 issue_filter.criteria.status=Status
 issue_filter.max_results_reached=Only the first {0} issues matching the search criteria have been retrieved. Add some additional criteria to get fewer results to be able to sort this list.
 issue_filter.no_result=No matching issues found.
+issue_filter.save_filter=Save Filter
+issue_filter.form.name=Name
+issue_filter.form.description=Description
+issue_filter.form.share=Shared with all users
 
 
 #------------------------------------------------------------------------------
index e5047917679027176487f491c32c2b6b881f3cbc..020f89b1ae2947b750881280aecf8fb02812063b 100644 (file)
@@ -37,14 +37,14 @@ import static com.google.common.collect.Maps.newHashMap;
 
 public class DefaultIssueFilter {
 
-  public static String SEPARATOR = "|";
-  public static String KEY_VALUE_SEPARATOR = "=";
-  public static String LIST_SEPARATOR = ",";
+  public final static String SEPARATOR = "|";
+  public final static String KEY_VALUE_SEPARATOR = "=";
+  public final static String LIST_SEPARATOR = ",";
 
   private Long id;
   private String name;
   private String user;
-  private Boolean shared;
+  private Boolean shared = false;
   private String description;
   private String data;
   private Date createdAt;
@@ -54,6 +54,18 @@ public class DefaultIssueFilter {
 
   }
 
+  public static DefaultIssueFilter create(String name) {
+    DefaultIssueFilter issueFilter = new DefaultIssueFilter();
+    Date now = new Date();
+    issueFilter.setName(name);
+    issueFilter.setCreatedAt(now).setUpdatedAt(now);
+    return issueFilter;
+  }
+
+  public DefaultIssueFilter(Map<String, Object> mapData) {
+    this.data = mapAsdata(mapData);
+  }
+
   public Long id() {
     return id;
   }
@@ -127,6 +139,9 @@ public class DefaultIssueFilter {
     return this;
   }
 
+  /**
+   * Used by ui
+   */
   public Map<String, Object> dataAsMap(){
     return dataAsMap(data);
   }
@@ -138,16 +153,15 @@ public class DefaultIssueFilter {
     Iterable<String> keyValues = Splitter.on(DefaultIssueFilter.SEPARATOR).split(data);
     for (String keyValue : keyValues) {
       String[] keyValueSplit = StringUtils.split(keyValue, DefaultIssueFilter.KEY_VALUE_SEPARATOR);
-      if (keyValueSplit.length != 2) {
-        throw new IllegalArgumentException("Key value should be separate by a '"+ DefaultIssueFilter.KEY_VALUE_SEPARATOR + "'");
-      }
-      String key = keyValueSplit[0];
-      String value = keyValueSplit[1];
-      String[] listValues = StringUtils.split(value, DefaultIssueFilter.LIST_SEPARATOR);
-      if (listValues.length > 1) {
-        map.put(key, newArrayList(listValues));
-      } else {
-        map.put(key, value);
+      if (keyValueSplit.length == 2) {
+        String key = keyValueSplit[0];
+        String value = keyValueSplit[1];
+        String[] listValues = StringUtils.split(value, DefaultIssueFilter.LIST_SEPARATOR);
+        if (listValues.length > 1) {
+          map.put(key, newArrayList(listValues));
+        } else {
+          map.put(key, value);
+        }
       }
     }
     return map;
@@ -160,27 +174,32 @@ public class DefaultIssueFilter {
     for (Map.Entry<String, Object> entries : map.entrySet()){
       String key = entries.getKey();
       Object value = entries.getValue();
-
-      stringBuilder.append(key);
-      stringBuilder.append(DefaultIssueFilter.KEY_VALUE_SEPARATOR);
-
-      List valuesList = newArrayList();
-      if (value instanceof List) {
-        // assume that it contains only strings
-        valuesList = (List) value;
-      } else if (value instanceof CharSequence) {
-        valuesList = Lists.newArrayList(Splitter.on(',').omitEmptyStrings().split((CharSequence) value));
-      } else {
-        stringBuilder.append(value);
-      }
-      for (Iterator<Object> valueListIter = valuesList.iterator(); valueListIter.hasNext();) {
-        Object valueList = valueListIter.next();
-        stringBuilder.append(valueList);
-        if (valueListIter.hasNext()) {
-          stringBuilder.append(DefaultIssueFilter.LIST_SEPARATOR);
+      if (value != null) {
+        List valuesList = newArrayList();
+        if (value instanceof List) {
+          // assume that it contains only strings
+          valuesList = (List) value;
+        } else if (value instanceof CharSequence) {
+          valuesList = Lists.newArrayList(Splitter.on(',').omitEmptyStrings().split((CharSequence) value));
+        } else {
+          stringBuilder.append(key);
+          stringBuilder.append(DefaultIssueFilter.KEY_VALUE_SEPARATOR);
+          stringBuilder.append(value);
+          stringBuilder.append(DefaultIssueFilter.SEPARATOR);
+        }
+        if (!valuesList.isEmpty()) {
+          stringBuilder.append(key);
+          stringBuilder.append(DefaultIssueFilter.KEY_VALUE_SEPARATOR);
+          for (Iterator<Object> valueListIter = valuesList.iterator(); valueListIter.hasNext();) {
+            Object valueList = valueListIter.next();
+            stringBuilder.append(valueList);
+            if (valueListIter.hasNext()) {
+              stringBuilder.append(DefaultIssueFilter.LIST_SEPARATOR);
+            }
+          }
+          stringBuilder.append(DefaultIssueFilter.SEPARATOR);
         }
       }
-      stringBuilder.append(DefaultIssueFilter.SEPARATOR);
     }
 
     if (stringBuilder.length() > 0) {
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueFilterTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueFilterTest.java
new file mode 100644 (file)
index 0000000..4bf9d65
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.core.issue;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newLinkedHashMap;
+import static org.fest.assertions.Assertions.assertThat;
+
+public class DefaultIssueFilterTest {
+
+  DefaultIssueFilter issueFilter = new DefaultIssueFilter();
+
+  @Test
+  public void should_convert_data_to_map() {
+    String data = "issues=ABCDE1234|severities=MAJOR,MINOR|resolved=true|pageSize=10|pageIndex=50";
+
+    Map<String, Object> map = issueFilter.dataAsMap(data);
+
+    assertThat(map).hasSize(5);
+    assertThat(map.get("issues")).isEqualTo("ABCDE1234");
+    assertThat(map.get("severities")).isInstanceOf(List.class);
+    assertThat((List<String>) map.get("severities")).contains("MAJOR", "MINOR");
+    assertThat(map.get("resolved")).isEqualTo("true");
+    assertThat(map.get("pageSize")).isEqualTo("10");
+    assertThat(map.get("pageIndex")).isEqualTo("50");
+  }
+
+  @Test
+  public void should_remove_empty_value_when_converting_data_to_map() {
+    String data = "issues=ABCDE1234|severities=";
+
+    Map<String, Object> map = issueFilter.dataAsMap(data);
+
+    assertThat(map).hasSize(1);
+    assertThat(map.get("issues")).isEqualTo("ABCDE1234");
+  }
+
+  @Test
+  public void should_convert_map_to_data() {
+    Map<String, Object> map = newLinkedHashMap();
+    map.put("issues", newArrayList("ABCDE1234"));
+    map.put("severities", newArrayList("MAJOR", "MINOR"));
+    map.put("resolved", true);
+    map.put("pageSize", 10l);
+    map.put("pageIndex", 50);
+
+    String result = issueFilter.mapAsdata(map);
+
+    assertThat(result).isEqualTo("issues=ABCDE1234|severities=MAJOR,MINOR|resolved=true|pageSize=10|pageIndex=50");
+  }
+
+  @Test
+  public void should_remove_empty_value_when_converting_convert_map_to_data() {
+    Map<String, Object> map = newLinkedHashMap();
+    map.put("issues", newArrayList("ABCDE1234"));
+    map.put("resolved", null);
+    map.put("pageSize", "");
+
+    String result = issueFilter.mapAsdata(map);
+
+    assertThat(result).isEqualTo("issues=ABCDE1234");
+  }
+
+}
index b8619fae88b45d19aa393edcfe7d221da47e9823..de841318b98e2608a8dae6348a4449ebb7eebb5e 100644 (file)
@@ -72,6 +72,8 @@ public class IssueFilterDaoTest extends AbstractDaoTestCase {
 
     dao.insert(filterDto);
 
+    assertThat(filterDto.getId()).isNotNull();
+
     checkTables("should_insert", new String[]{"created_at", "updated_at"}, "issue_filters");
   }
 
index e944d5a325beb3667c8cdae28b4a9748cb2f5456..c8c2967d2f5a5747f823cedcd6417d40acf2c9d6 100644 (file)
@@ -280,41 +280,14 @@ public class InternalRubyIssueService implements ServerComponent {
     String projectParam = parameters.get("project");
     Date deadLine = null;
 
-    if (Strings.isNullOrEmpty(name)) {
-      result.addError(Result.Message.ofL10n("errors.cant_be_empty", "name"));
-    } else {
-      if (name.length() > 200) {
-        result.addError(Result.Message.ofL10n("errors.is_too_long", "name", 200));
-      }
-    }
-
-    if (!Strings.isNullOrEmpty(description) && description.length() > 1000) {
-      result.addError(Result.Message.ofL10n("errors.is_too_long", "description", 1000));
-    }
+    checkMandatorySizeParameter(name, "name", 200, result);
+    checkOptionnalSizeParameter(description, "description",  1000, result);
 
     // Can only set project on creation
     if (existingActionPlan == null) {
-      if (Strings.isNullOrEmpty(projectParam)) {
-        result.addError(Result.Message.ofL10n("errors.cant_be_empty", "project"));
-      } else {
-        ResourceDto project = resourceDao.getResource(ResourceQuery.create().setKey(projectParam));
-        if (project == null) {
-          result.addError(Result.Message.ofL10n("action_plans.errors.project_does_not_exist", projectParam));
-        }
-      }
-    }
-
-    if (!Strings.isNullOrEmpty(deadLineParam)) {
-      try {
-        deadLine = RubyUtils.toDate(deadLineParam);
-        Date today = new Date();
-        if (deadLine != null && deadLine.before(today) && !org.apache.commons.lang.time.DateUtils.isSameDay(deadLine, today)) {
-          result.addError(Result.Message.ofL10n("action_plans.date_cant_be_in_past"));
-        }
-      } catch (SonarException e) {
-        result.addError(Result.Message.ofL10n("errors.is_not_valid", "date"));
-      }
+      checkProject(projectParam, result);
     }
+    deadLine = checkAndReturnDeadline(deadLineParam, result);
 
     if (!Strings.isNullOrEmpty(projectParam) && !Strings.isNullOrEmpty(name) && (existingActionPlan == null || !name.equals(existingActionPlan.name()))
       && actionPlanService.isNameAlreadyUsedForProject(name, projectParam)) {
@@ -339,6 +312,33 @@ public class InternalRubyIssueService implements ServerComponent {
     return result;
   }
 
+  private void checkProject(String projectParam, Result<ActionPlan> result){
+    if (Strings.isNullOrEmpty(projectParam)) {
+      result.addError(Result.Message.ofL10n("errors.cant_be_empty", "project"));
+    } else {
+      ResourceDto project = resourceDao.getResource(ResourceQuery.create().setKey(projectParam));
+      if (project == null) {
+        result.addError(Result.Message.ofL10n("action_plans.errors.project_does_not_exist", projectParam));
+      }
+    }
+  }
+
+  private Date checkAndReturnDeadline(String deadLineParam, Result<ActionPlan> result){
+    Date deadLine = null;
+    if (!Strings.isNullOrEmpty(deadLineParam)) {
+      try {
+        deadLine = RubyUtils.toDate(deadLineParam);
+        Date today = new Date();
+        if (deadLine != null && deadLine.before(today) && !org.apache.commons.lang.time.DateUtils.isSameDay(deadLine, today)) {
+          result.addError(Result.Message.ofL10n("action_plans.date_cant_be_in_past"));
+        }
+      } catch (SonarException e) {
+        result.addError(Result.Message.ofL10n("errors.is_not_valid", "date"));
+      }
+    }
+    return deadLine;
+  }
+
   private Result<ActionPlan> createResultForExistingActionPlan(String actionPlanKey) {
     Result<ActionPlan> result = Result.of();
     if (findActionPlan(actionPlanKey) == null) {
@@ -369,6 +369,14 @@ public class InternalRubyIssueService implements ServerComponent {
     return PublicRubyIssueService.toQuery(props);
   }
 
+  public DefaultIssueFilter findIssueFilter(Long id) {
+    return issueFilterService.findById(id, UserSession.get());
+  }
+
+  public DefaultIssueFilter createFilterFromMap(Map<String, Object> mapData) {
+    return issueFilterService.createEmptyFilter(mapData);
+  }
+
   /**
    * Execute issue filter
    */
@@ -379,29 +387,69 @@ public class InternalRubyIssueService implements ServerComponent {
   /**
    * Create issue filter
    */
-  public void createIssueFilter(Map<String, String> params) {
+  public Result<DefaultIssueFilter> createIssueFilter(Map<String, String> params) {
     Result<DefaultIssueFilter> result = Result.of();
     try {
       // mandatory parameters
       String name = params.get("name");
       String description = params.get("description");
       String data = params.get("data");
+      Boolean shared = RubyUtils.toBoolean(params.get("shared"));
 
       if (result.ok()) {
-        DefaultIssueFilter defaultIssueFilter = new DefaultIssueFilter()
-          .setName(name)
+        DefaultIssueFilter defaultIssueFilter = DefaultIssueFilter.create(name)
           .setDescription(description)
-          .setData(data)
-        ;
-        defaultIssueFilter = issueFilterService.create(defaultIssueFilter, UserSession.get());
+          .setShared(shared)
+          .setData(data);
+        defaultIssueFilter = issueFilterService.save(defaultIssueFilter, UserSession.get());
         result.set(defaultIssueFilter);
       }
 
     } catch (Exception e) {
       result.addError(e.getMessage());
     }
+    return result;
+  }
 
+  @VisibleForTesting
+  Result<DefaultIssueFilter> createIssueFilterResult(Map<String, String> params) {
+    Result<DefaultIssueFilter> result = Result.of();
+
+    // mandatory parameters
+    String name = params.get("name");
+    String description = params.get("description");
+    String data = params.get("data");
+    Boolean shared = RubyUtils.toBoolean(params.get("shared"));
+
+    checkMandatorySizeParameter(name, "name",  100, result);
+    checkOptionnalSizeParameter(description, "description",  4000, result);
+
+     // TODO check name uniquness
+
+    if (result.ok()) {
+      DefaultIssueFilter defaultIssueFilter = DefaultIssueFilter.create(name)
+        .setDescription(description)
+        .setShared(shared)
+        .setData(data);
+      result.set(defaultIssueFilter);
+    }
+    return result;
   }
 
+  private void checkMandatorySizeParameter(String value, String paramName, Integer size, Result result){
+    if (Strings.isNullOrEmpty(value)) {
+      result.addError(Result.Message.ofL10n("errors.cant_be_empty", paramName));
+    } else {
+      if (value.length() > size) {
+        result.addError(Result.Message.ofL10n("errors.is_too_long", paramName, size));
+      }
+    }
+  }
+
+  private void checkOptionnalSizeParameter(String value, String paramName, Integer size, Result result){
+    if (!Strings.isNullOrEmpty(value) && value.length() > size) {
+      result.addError(Result.Message.ofL10n("errors.is_too_long", paramName, size));
+    }
+  }
 
 }
\ No newline at end of file
index d57b32537eb1d894962d729cff88b4cf27fe4caa..61dd367abc3f6ac03151dc84de5ec36fb001616e 100644 (file)
 
 package org.sonar.server.issue;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
-import com.google.common.collect.Lists;
-import org.apache.commons.lang.StringUtils;
 import org.sonar.api.ServerComponent;
 import org.sonar.api.issue.IssueFinder;
 import org.sonar.api.issue.IssueQuery;
@@ -33,12 +29,9 @@ import org.sonar.core.issue.db.IssueFilterDao;
 import org.sonar.core.issue.db.IssueFilterDto;
 import org.sonar.server.user.UserSession;
 
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import javax.annotation.CheckForNull;
 
-import static com.google.common.collect.Lists.newArrayList;
-import static com.google.common.collect.Maps.newHashMap;
+import java.util.Map;
 
 public class IssueFilterService implements ServerComponent {
 
@@ -50,11 +43,32 @@ public class IssueFilterService implements ServerComponent {
     this.issueFinder = issueFinder;
   }
 
-  public DefaultIssueFilter create(DefaultIssueFilter issueFilter, UserSession userSession) {
+  @CheckForNull
+  public DefaultIssueFilter createEmptyFilter(Map<String, Object> mapData) {
+    return new DefaultIssueFilter(mapData);
+  }
+
+  @CheckForNull
+  public DefaultIssueFilter findById(Long id, UserSession userSession) {
     // TODO
 //    checkAuthorization(userSession, project, UserRole.ADMIN);
-    issueFilterDao.insert(IssueFilterDto.toIssueFilter(issueFilter));
-    return issueFilter;
+    verifyLoggedIn(userSession);
+//    access_denied unless filter.shared || filter.owner?(current_user)
+
+    IssueFilterDto issueFilterDto = issueFilterDao.selectById(id);
+    if (issueFilterDto == null) {
+      return null;
+    }
+    return issueFilterDto.toIssueFilter();
+  }
+
+  public DefaultIssueFilter save(DefaultIssueFilter issueFilter, UserSession userSession) {
+    issueFilter.setUser(userSession.login());
+    // TODO
+//    checkAuthorization(userSession, project, UserRole.ADMIN);
+    IssueFilterDto issueFilterDto = IssueFilterDto.toIssueFilter(issueFilter);
+    issueFilterDao.insert(issueFilterDto);
+    return issueFilterDto.toIssueFilter();
   }
 
   public DefaultIssueFilter update(DefaultIssueFilter issueFilter, UserSession userSession) {
@@ -89,64 +103,11 @@ public class IssueFilterService implements ServerComponent {
     return null;
   }
 
-  @VisibleForTesting
-  Map<String, Object> dataAsMap(String data) {
-    Map<String, Object> map = newHashMap();
-
-    Iterable<String> keyValues = Splitter.on(DefaultIssueFilter.SEPARATOR).split(data);
-    for (String keyValue : keyValues) {
-      String[] keyValueSplit = StringUtils.split(keyValue, DefaultIssueFilter.KEY_VALUE_SEPARATOR);
-      if (keyValueSplit.length != 2) {
-        throw new IllegalArgumentException("Key value should be separate by a '"+ DefaultIssueFilter.KEY_VALUE_SEPARATOR + "'");
-      }
-      String key = keyValueSplit[0];
-      String value = keyValueSplit[1];
-      String[] listValues = StringUtils.split(value, DefaultIssueFilter.LIST_SEPARATOR);
-      if (listValues.length > 1) {
-        map.put(key, newArrayList(listValues));
-      } else {
-        map.put(key, value);
-      }
+  private void verifyLoggedIn(UserSession userSession) {
+    if (!userSession.isLoggedIn()) {
+      // must be logged
+      throw new IllegalStateException("User is not logged in");
     }
-    return map;
-  }
-
-  @VisibleForTesting
-  String mapAsdata(Map<String, Object> map) {
-    StringBuilder stringBuilder = new StringBuilder();
-
-    for (Map.Entry<String, Object> entries : map.entrySet()){
-      String key = entries.getKey();
-      Object value = entries.getValue();
-
-      stringBuilder.append(key);
-      stringBuilder.append(DefaultIssueFilter.KEY_VALUE_SEPARATOR);
-
-      List valuesList = newArrayList();
-      if (value instanceof List) {
-        // assume that it contains only strings
-        valuesList = (List) value;
-      } else if (value instanceof CharSequence) {
-        valuesList = Lists.newArrayList(Splitter.on(',').omitEmptyStrings().split((CharSequence) value));
-      } else {
-        stringBuilder.append(value);
-      }
-      for (Iterator<Object> valueListIter = valuesList.iterator(); valueListIter.hasNext();) {
-        Object valueList = valueListIter.next();
-        stringBuilder.append(valueList);
-        if (valueListIter.hasNext()) {
-          stringBuilder.append(DefaultIssueFilter.LIST_SEPARATOR);
-        }
-      }
-      stringBuilder.append(DefaultIssueFilter.SEPARATOR);
-    }
-
-    if (stringBuilder.length() > 0) {
-      // Delete useless last separator character
-      stringBuilder.deleteCharAt(stringBuilder.length() - 1);
-    }
-
-    return stringBuilder.toString();
   }
 
 }
index da523e067fabcce977b7863e2fa9521c52e9e36a..cafdd7f75a7803425251756beb55b86372a4b3f8 100644 (file)
 
 class IssuesController < ApplicationController
 
-  before_filter :init
+  before_filter :init_options
 
+  # GET /issues/index
   def index
     redirect_to :action => 'search'
   end
 
+  # GET /issues/search
   def search
-    init_results
+    if params[:id]
+      @filter = find_filter(params[:id].to_i)
+    else
+      @filter = Internal.issues.createFilterFromMap(criteria_params)
+    end
 
-    @criteria_params = params.merge({:controller => nil, :action => nil, :search => nil, :widget_id => nil, :edit => nil})
-    @criteria_params['pageSize'] = 100
-    @issue_query = Internal.issues.toQuery(@criteria_params)
+    @issue_query = Internal.issues.toQuery(@filter.dataAsMap)
     @issues_result = Internal.issues.execute(@issue_query)
-    @paging = @issues_result.paging
-    @issues = @issues_result.issues
   end
 
-  private
+  # Load existing filter
+  # GET /issues/filter/<filter id>
+  def filter
+    require_parameters :id
+
+    @filter = find_filter(params[:id].to_i)
+    @issue_query = Internal.issues.toQuery(@filter.dataAsMap)
+
+    # criteria can be overridden
+    # TODO ?
+    #@filter.override_criteria(criteria_params)
+
+    @issues_result = Internal.issues.execute(@issue_query)
+    @unchanged = true
+    render :action => 'search'
+  end
 
-  def init_results
-    @issues_result = nil
-    @paging = nil
-    @issues = nil
-    #criteria['pageSize'] = 100
-    self
+  # GET /issues/save_as_form?[id=<id>][&criteria]
+  def save_as_form
+    if params[:id].present?
+      @filter = find_filter(params[:id])
+    else
+      @filter = Internal.issues.createFilterFromMap(criteria_params)
+    end
+    render :partial => 'issues/save_as_form'
   end
 
-  def init
+  # POST /issues/save_as?[id=<id>]&name=<name>[&parameters]
+  def save_as
+    verify_post_request
+    access_denied unless logged_in?
+
+    options = {'name' => params[:name], 'description' => params[:description], 'data' => URI.unescape(params[:data]), 'shared' => params[:shared]=='true' }
+    add_to_favourites=false
+    if params[:id].present?
+      # TODO
+      #@filter = Internal.issues.updateIssueFilter(params[:id], options)
+    else
+      filter_result = Internal.issues.createIssueFilter(options)
+      add_to_favourites=true
+    end
+
+    if filter_result.ok
+      @filter = filter_result.get()
+      puts "#### "+ @filter.id.to_s
+      #current_user.favourited_measure_filters<<@filter if add_to_favourites
+      render :text => @filter.id.to_s, :status => 200
+    else
+      @errors = filter_result.errors
+      render :partial => 'issues/save_as_form', :status => 400
+    end
+  end
+
+  # POST /issues/save?id=<id>&[criteria]
+  # TODO
+  def save
+    verify_post_request
+    require_parameters :id
+    access_denied unless logged_in?
+
+    @filter = find_filter(params[:id])
+
+    #@filter = Internal.issues.updateIssueFilter(params[:id], options)
+
+    #@filter.criteria=criteria_params_without_page_id
+    #@filter.convert_criteria_to_data
+    #unless @filter.save
+    #  flash[:error]='Error'
+    #end
+    redirect_to :action => 'filter', :id => @filter.id
+  end
+
+  private
+
+  def init_options
     @options_for_statuses = Internal.issues.listStatus().map {|s| [message('issue.status.' + s), s]}
     @options_for_resolutions = Internal.issues.listResolutions().map {|s| [message('issue.resolution.' + s), s]}
   end
 
+  # TODO
+  def find_filter(id)
+    filter = Internal.issues.findIssueFilter(id)
+    # TODO
+    #access_denied unless filter.shared || filter.owner?(current_user)
+    filter
+  end
+
+  def criteria_params
+    criteria = params
+    criteria.delete('controller')
+    criteria.delete('action')
+    criteria.delete('search')
+    criteria.delete('edit')
+    criteria.delete('pageSize')
+    criteria
+  end
+
 end
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/issue_filter.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/issue_filter.rb
deleted file mode 100644 (file)
index 09423ff..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-#
-# Sonar, entreprise quality control tool.
-# Copyright (C) 2008-2013 SonarSource
-# mailto:contact AT sonarsource DOT com
-#
-# SonarQube is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 3 of the License, or (at your option) any later version.
-#
-# SonarQube is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-require 'set'
-class IssueFilter
-
-  attr_reader :paging, :issues, :issues_result
-
-  def criteria(key=nil)
-    @criteria ||= HashWithIndifferentAccess.new
-    if key
-      @criteria[key]
-    else
-      @criteria
-    end
-  end
-
-  def criteria=(hash)
-    @criteria = HashWithIndifferentAccess.new
-    hash.each_pair do |k, v|
-      set_criteria_value(k, v)
-    end
-  end
-
-  def override_criteria(hash)
-    @criteria ||= HashWithIndifferentAccess.new
-    hash.each_pair do |k, v|
-      set_criteria_value(k, v)
-    end
-  end
-
-  # API used by Displays
-  def set_criteria_value(key, value)
-    @criteria ||= HashWithIndifferentAccess.new
-    if key
-      if value!=nil && value!='' && value!=['']
-        value = (value.kind_of?(Array) ? value : value.to_s)
-        @criteria[key]=value
-      else
-        @criteria.delete(key)
-      end
-    end
-  end
-
-  def execute
-    init_results
-    @issues_result = Api.issues.find(criteria)
-    @paging = @issues_result.paging
-    @issues = @issues_result.issues
-    self
-  end
-
-  private
-
-  def init_results
-    @issues_result = nil
-    @paging = nil
-    @issues = nil
-    criteria['pageSize'] = 100
-    self
-  end
-
-end
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_action_links.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_action_links.html.erb
new file mode 100644 (file)
index 0000000..32cfd27
--- /dev/null
@@ -0,0 +1,18 @@
+<ul class="operations">
+  <% if logged_in? %>
+    <% if @filter.id %>
+      <!--TODO-->
+      <li class="hidden"><a id="copy" href="<%= url_for :action => 'copy_form', :id => @filter.id -%>" class="link-action open-modal"><%= message('copy') -%></a></li>
+    <% end %>
+    <% if !defined?(@unchanged) && @filter.id && @filter.user == current_user.login %>
+      <li>
+        <%= link_to message('save'), params.merge({:action => 'save', :id => @filter.id}), :class => 'link-action', :id => 'save', :method => :post -%>
+      </li>
+    <% end %>
+    <% unless @filter.id %>
+      <li>
+        <a id="save-as" href="<%= url_for params.merge({:action => 'save_as_form', :id => @filter.id}) -%>" class="link-action open-modal"><%= message('save_as') -%></a>
+      </li>
+    <% end %>
+<% end %>
+</ul>
\ No newline at end of file
index 9b93373ad25f96e0a51f44091fefdd088ba518ec..6f1e0ba429e66b13ad8c9caab95b15aaaf414c92 100644 (file)
 <% end %>
 
 <%
-   if @issues && !@issues.empty?
+   if @issues_result.issues && !@issues_result.issues.empty?
     colspan = 9
 %>
-  <div>
+  <div id="issues-list">
     <table class="data width100">
       <thead>
         <tr>
@@ -59,7 +59,7 @@
       </thead>
       <tbody>
       <%
-         @issues.each do |issue|
+         @issues_result.issues.each do |issue|
       %>
         <tr class="<%= cycle('even', 'odd') -%>">
           <td width="1%" nowrap>
       <%
          end
       %>
-      <% if @issues.empty? %>
+      <% if @issues_result.issues.empty? %>
         <tr class="even">
           <td colspan="<%= colspan -%>"><%= message 'no_data' -%></td>
         </tr>
       <% end %>
       </tbody>
 
-      <%= paginate_java(@paging, :colspan => colspan, :id => 'issue-filter-foot', :include_loading_icon => true) { |label, page_id|
+      <%= paginate_java(@issues_result.paging, :colspan => colspan, :id => 'issue-filter-foot', :include_loading_icon => true) { |label, page_id|
         link_to(label, params.merge({:pageIndex => page_id}))
       } -%>
 
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_save_as_form.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_save_as_form.html.erb
new file mode 100644 (file)
index 0000000..10dcaf7
--- /dev/null
@@ -0,0 +1,40 @@
+<form id="save-as-filter-form" method="post" action="<%= ApplicationController.root_context -%>/issues/save_as">
+  <input type="hidden" name="id" value="<%= @filter.id -%>">
+  <input type="hidden" name="data" value="<%= u(@filter.data) -%>">
+  <fieldset>
+    <div class="modal-head">
+      <h2><%= message('issue_filter.save_filter') -%></h2>
+    </div>
+
+    <div class="modal-body">
+      <%
+         if @errors
+         @errors.each do |msg| %>
+        <div class="error"><%= h (msg.text ? msg.text : Api::Utils.message(msg.l10nKey, :params => msg.l10nParams)) -%></div>
+      <% end
+        end
+      %>
+      <div class="modal-field">
+        <label for="name"><%= message('issue_filter.form.name') -%> <em class="mandatory">*</em></label>
+        <input id="name" name="name" type="text" size="50" maxlength="100" value="<%= h @filter.name -%>" autofocus="autofocus"/>
+      </div>
+      <div class="modal-field">
+        <label for="description"><%= message('issue_filter.form.description') -%></label>
+        <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= h @filter.description -%>"/>
+      </div>
+      <div class="modal-field">
+        <label for="shared"><%= message('issue_filter.form.share') -%></label>
+        <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if @filter.shared -%>/>
+      </div>
+    </div>
+    <div class="modal-foot">
+      <input type="submit" value="<%= h message('save') -%>" id="save-as-submit"/>
+      <a href="#" onclick="return closeModalWindow()" id="save-as-cancel"><%= h message('cancel') -%></a>
+    </div>
+  </fieldset>
+</form>
+<script>
+  $j("#save-as-filter-form").modalForm({success:function (data) {
+    window.location = baseUrl + '/issues/filter/' + data;
+  }});
+</script>
\ No newline at end of file
index dc762c5b1f5b0310261c892be4b520acd481b220..01e6a7831159b18cf11cba65c54d1b16145a9325 100644 (file)
@@ -1,6 +1,9 @@
 <ul class="sidebar gray-sidebar">
   <form method="GET" action="<%= ApplicationController.root_context -%>/issues/search" >
 
+    <% if @filter.id %>
+      <input type="hidden" name="id" value="<%= @filter.id.to_s -%>">
+    <% end %>
     <input type="hidden" name="sort" value="<%= @issue_query.sort -%>"/>
     <input type="hidden" name="asc" value="<%= @issue_query.asc -%>"/>
 
@@ -9,7 +12,7 @@
     </li>
     <li id="criteria-project" class="marginbottom5">
       <%= message 'issue_filter.criteria.project' -%>:
-      <% selected_componentRoot = Internal.component_api.findByKey(@issue_query.componentRoots.get(0)) if @issue_query.componentRoots and @issue_query.componentRoots.size == 1 %>
+      <% selected_componentRoot = Internal.component_api.findByKey(@issue_query.componentRoots.to_a.first) if @issue_query.componentRoots and @issue_query.componentRoots.size == 1 %>
       <%= component_select_tag 'componentRoots', :resource_type_property => 'supportsGlobalDashboards', :width => '100%',
                               :selected_resource => selected_componentRoot,
                               :display_key => true,
     </li>
     <li id="criteria-assignee" class="marginbottom5">
       <%= message 'issue_filter.criteria.assignee' -%>:
-      <% selected_assignee = Api.users.findByLogin(@issue_query.assignees.get(0)) if @issue_query.assignees && @issue_query.assignees.size == 1 %>
+      <% selected_assignee = Api.users.findByLogin(@issue_query.assignees.to_a.first) if @issue_query.assignees && @issue_query.assignees.size == 1 %>
       <%= user_select_tag('assignees', {:selected_user => selected_assignee, :width => '100%', :placeholder => message('issue_filter.criteria.assignee'),
                         :html_id => 'select-assignee', :open => false, :allow_clear => true}) -%>
     </li>
     <li id="criteria-reporter" class="marginbottom5">
       <%= message 'issue_filter.criteria.reporter' -%>:
-      <% selected_reporter = Api.users.findByLogin(@issue_query.reporters.get(0)) if @issue_query.reporters && @issue_query.reporters.size == 1 %>
+      <% selected_reporter = Api.users.findByLogin(@issue_query.reporters.to_a.first) if @issue_query.reporters && @issue_query.reporters.size == 1 %>
       <%= user_select_tag('reporters', {:selected_user => selected_reporter, :width => '100%', :placeholder => message('issue_filter.criteria.reporter'),
                                         :html_id => 'select-reporter', :open => false, :allow_clear => true}) -%>
     </li>
index 1671e29b5dacb6954c76a2d80ed9dd6b2d89a717..7ffbc9df5d3f6c00432e4682297d67f7f9758901 100644 (file)
@@ -5,12 +5,14 @@
 
   <div class="page-split-right">
     <div id="content">
-      <div class="marginbottom10">
-        <% if @issues_result && @issues_result.maxResultsReached() %>
-          <p class="notes"><%= message('issue_filter.max_results_reached', :params => @paging.total()) -%></p>
-        <% end %>
-        <%= render :partial => 'list' -%>
+      <div class="line-block marginbottom10">
+        <%= render :partial => 'action_links' -%>
       </div>
+
+      <% if @issues_result && @issues_result.maxResultsReached() %>
+        <p class="notes"><%= message('issue_filter.max_results_reached', :params => @issues_result.paging.total()) -%></p>
+      <% end %>
+      <%= render :partial => 'list' -%>
     </div>
   </div>
 </div>
\ No newline at end of file
index 915b82d82af51f37c56bb289185049de73e99d12..eac0c78881984949f3d2ea42511eb8c4c957caa7 100644 (file)
@@ -293,4 +293,37 @@ public class InternalRubyIssueServiceTest {
 
     assertThat(result).isSameAs(changelog);
   }
+
+  @Test
+  public void should_get_error_on_issue_filter_result_when_no_name() {
+    Map<String, String> parameters = newHashMap();
+    parameters.put("name", null);
+    parameters.put("description", "Long term issues");
+
+    Result result = service.createIssueFilterResult(parameters);
+    assertThat(result.ok()).isFalse();
+    assertThat(result.errors()).contains(Result.Message.ofL10n("errors.cant_be_empty", "name"));
+  }
+
+  @Test
+  public void should_get_error_on_issue_filter_result_when_name_is_too_long() {
+    Map<String, String> parameters = newHashMap();
+    parameters.put("name", createLongString(101));
+    parameters.put("description", "Long term issues");
+
+    Result result = service.createIssueFilterResult(parameters);
+    assertThat(result.ok()).isFalse();
+    assertThat(result.errors()).contains(Result.Message.ofL10n("errors.is_too_long", "name", 100));
+  }
+
+  @Test
+  public void should_get_error_on_issue_filter_result_when_description_is_too_long() {
+    Map<String, String> parameters = newHashMap();
+    parameters.put("name", "Long term");
+    parameters.put("description", createLongString(4001));
+
+    Result result = service.createIssueFilterResult(parameters);
+    assertThat(result.ok()).isFalse();
+    assertThat(result.errors()).contains(Result.Message.ofL10n("errors.is_too_long", "description", 4000));
+  }
 }
index 8e00284b13f74c4988548785ed6e7be59d3a599b..6a608f25a479c78454f989c0bc8f1ca94767dff8 100644 (file)
 package org.sonar.server.issue;
 
 import org.junit.Before;
-import org.junit.Test;
 import org.sonar.api.issue.IssueFinder;
 import org.sonar.core.issue.db.IssueFilterDao;
 
-import java.util.List;
-import java.util.Map;
-
-import static com.google.common.collect.Lists.newArrayList;
-import static com.google.common.collect.Maps.newLinkedHashMap;
-import static org.fest.assertions.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 public class IssueFilterServiceTest {
@@ -47,32 +40,4 @@ public class IssueFilterServiceTest {
     service = new IssueFilterService(issueFilterDao, issueFinder);
   }
 
-  @Test
-  public void should_convert_data_to_map() {
-    String data = "issues=ABCDE1234|severities=MAJOR,MINOR|resolved=true|pageSize=10|pageIndex=50";
-
-    Map<String, Object> map = service.dataAsMap(data);
-
-    assertThat(map).hasSize(5);
-    assertThat(map.get("issues")).isEqualTo("ABCDE1234");
-    assertThat(map.get("severities")).isInstanceOf(List.class);
-    assertThat((List<String>) map.get("severities")).contains("MAJOR", "MINOR");
-    assertThat(map.get("resolved")).isEqualTo("true");
-    assertThat(map.get("pageSize")).isEqualTo("10");
-    assertThat(map.get("pageIndex")).isEqualTo("50");
-  }
-
-  @Test
-  public void should_convert_map_to_data() {
-    Map<String, Object> map = newLinkedHashMap();
-    map.put("issues", newArrayList("ABCDE1234"));
-    map.put("severities", newArrayList("MAJOR", "MINOR"));
-    map.put("resolved", true);
-    map.put("pageSize", 10l);
-    map.put("pageIndex", 50);
-
-    String result = service.mapAsdata(map);
-
-    assertThat(result).isEqualTo("issues=ABCDE1234|severities=MAJOR,MINOR|resolved=true|pageSize=10|pageIndex=50");
-  }
 }
index daf45ba7124bbb9093e52791ab662633d63b1fa8..bc4521dfcf1f2794fcc66967fe7bc07ea57acb1b 100644 (file)
  * SonarQube is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General License for more details.
+ * Lesser General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General License
+ * You should have received a copy of the GNU Lesser General Public License
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 package org.sonar.wsclient.issue;
 
 import javax.annotation.CheckForNull;
+
 import java.util.Date;
 
 /**
index 3294fa81f6588ceb6e8be9640beb43941270c915..93aec19c1487e1251bf0978e5930426f1f62fa20 100644 (file)
  * SonarQube is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General License for more details.
+ * Lesser General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General License
+ * You should have received a copy of the GNU Lesser General Public License
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 package org.sonar.wsclient.issue;
 
 import javax.annotation.CheckForNull;
+
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
index 2ce101d0ac7c689e251c4965f1e5a5d4f462a0f7..d843343695e61e76797ed6f3dff9a391f2aa453d 100644 (file)
@@ -11,9 +11,9 @@
  * SonarQube is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General License for more details.
+ * Lesser General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General License
+ * You should have received a copy of the GNU Lesser General Public License
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
index 43d5cf687ace9695d3f5715392269eb73b5f2154..549e8864b088135f5312e84ae78f4a8ea6b7cb8f 100644 (file)
@@ -11,9 +11,9 @@
  * SonarQube is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General License for more details.
+ * Lesser General Public License for more details.
  *
- * You should have received a copy of the GNU Lesser General License
+ * You should have received a copy of the GNU Lesser General Public License
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
@@ -25,6 +25,7 @@ import org.sonar.wsclient.rule.Rule;
 import org.sonar.wsclient.user.User;
 
 import javax.annotation.CheckForNull;
+
 import java.util.Collection;
 import java.util.List;