]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3754 API: ability to define a cardinality on a property
authorDavid Gageot <david@gageot.net>
Thu, 20 Sep 2012 15:05:20 +0000 (17:05 +0200)
committerDavid Gageot <david@gageot.net>
Thu, 20 Sep 2012 15:08:29 +0000 (17:08 +0200)
22 files changed:
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/Property.java
sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java
sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinitions.java
sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java
sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionTest.java
sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionsTest.java
sonar-plugin-api/src/test/java/org/sonar/api/config/SettingsTest.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/controllers/api/user_properties_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/project_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/settings_helper.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/models/property.rb
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_sidebar.html.erb [deleted file]
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/settings/index.html.erb
sonar-server/src/main/webapp/stylesheets/style.css

index 339e93dec12f2057fe47c1db35700ab9a9b792dc..a8210dbff9f9cbe5e33194b2d44d620e28acee28 100644 (file)
@@ -564,6 +564,7 @@ dashboard.username.default=[Sonar]
 # SETTINGS
 #
 #------------------------------------------------------------------------------
+settings.add=Add value
 settings.save_category=Save {0} Settings
 property.category.email=Email
 property.category.encryption=Encryption
index 7cff4f9b49f449c348eb3253d17dba4532e1ab7b..46f9d0ebe867f06650f2119186002645de051b53 100644 (file)
@@ -92,4 +92,10 @@ public @interface Property {
    */
   String[] options() default {};
 
+  /**
+   * Can the property take multiple values. Eg: list of email addresses.
+   *
+   * @since 3.3
+   */
+  boolean multiValues() default false;
 }
index 2e4b646429c6958aaee00e48cd809cb43d263a8b..69a8f16c87991559eccf966d810adc6b2b103fae 100644 (file)
@@ -66,6 +66,7 @@ public final class PropertyDefinition {
   private boolean onProject = false;
   private boolean onModule = false;
   private boolean isGlobal = true;
+  private boolean multiValues;
 
   private PropertyDefinition(Property annotation) {
     this.key = annotation.key();
@@ -78,20 +79,20 @@ public final class PropertyDefinition {
     this.category = annotation.category();
     this.type = fixType(annotation.key(), annotation.type());
     this.options = annotation.options();
+    this.multiValues = annotation.multiValues();
   }
 
-  private PropertyType fixType(String key, PropertyType type) {
+  private static PropertyType fixType(String key, PropertyType type) {
     // Auto-detect passwords and licenses for old versions of plugins that
     // do not declare property types
-    PropertyType fix = type;
     if (type == PropertyType.STRING) {
       if (StringUtils.endsWith(key, ".password.secured")) {
-        fix = PropertyType.PASSWORD;
+        return PropertyType.PASSWORD;
       } else if (StringUtils.endsWith(key, ".license.secured")) {
-        fix = PropertyType.LICENSE;
+        return PropertyType.LICENSE;
       }
     }
-    return fix;
+    return type;
   }
 
   private PropertyDefinition(String key, PropertyType type, String[] options) {
@@ -109,30 +110,28 @@ public final class PropertyDefinition {
   }
 
   public Result validate(@Nullable String value) {
-    // TODO REFACTORING REQUIRED HERE
-    Result result = Result.SUCCESS;
     if (StringUtils.isNotBlank(value)) {
       if (type == PropertyType.BOOLEAN) {
         if (!StringUtils.equalsIgnoreCase(value, "true") && !StringUtils.equalsIgnoreCase(value, "false")) {
-          result = Result.newError("notBoolean");
+          return Result.newError("notBoolean");
         }
       } else if (type == PropertyType.INTEGER) {
         if (!NumberUtils.isDigits(value)) {
-          result = Result.newError("notInteger");
+          return Result.newError("notInteger");
         }
       } else if (type == PropertyType.FLOAT) {
         try {
           Double.parseDouble(value);
         } catch (NumberFormatException e) {
-          result = Result.newError("notFloat");
+          return Result.newError("notFloat");
         }
       } else if (type == PropertyType.SINGLE_SELECT_LIST) {
         if (!ArrayUtils.contains(options, value)) {
-          result = Result.newError("notInOptions");
+          return Result.newError("notInOptions");
         }
       }
     }
-    return result;
+    return Result.SUCCESS;
   }
 
   public String getKey() {
@@ -174,4 +173,11 @@ public final class PropertyDefinition {
   public boolean isGlobal() {
     return isGlobal;
   }
+
+  /**
+   * @since 3.3
+   */
+  public boolean isMultiValues() {
+    return multiValues;
+  }
 }
index fb5415650563f29a00d886bd3d2aa2a5a09c906b..6d7144a65e04c07dd2eb535994c4e43412ebe650 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.api.config;
 
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.BatchComponent;
 import org.sonar.api.Properties;
@@ -97,12 +99,57 @@ public final class PropertyDefinitions implements BatchComponent, ServerComponen
     return definitions.values();
   }
 
+  /**
+   * since 3.3
+   */
+  public Map<String, Collection<PropertyDefinition>> getGlobalPropertiesByCategory() {
+    Multimap<String, PropertyDefinition> byCategory = ArrayListMultimap.create();
+
+    for (PropertyDefinition definition : getAll()) {
+      if (definition.isGlobal()) {
+        byCategory.put(getCategory(definition.getKey()), definition);
+      }
+    }
+
+    return byCategory.asMap();
+  }
+
+  /**
+   * since 3.3
+   */
+  public Map<String, Collection<PropertyDefinition>> getProjectPropertiesByCategory() {
+    Multimap<String, PropertyDefinition> byCategory = ArrayListMultimap.create();
+
+    for (PropertyDefinition definition : getAll()) {
+      if (definition.isOnProject()) {
+        byCategory.put(getCategory(definition.getKey()), definition);
+      }
+    }
+
+    return byCategory.asMap();
+  }
+
+  /**
+   * since 3.3
+   */
+  public Map<String, Collection<PropertyDefinition>> getModulePropertiesByCategory() {
+    Multimap<String, PropertyDefinition> byCategory = ArrayListMultimap.create();
+
+    for (PropertyDefinition definition : getAll()) {
+      if (definition.isOnModule()) {
+        byCategory.put(getCategory(definition.getKey()), definition);
+      }
+    }
+
+    return byCategory.asMap();
+  }
+
   public String getDefaultValue(String key) {
     PropertyDefinition def = get(key);
-    if (def != null) {
-      return StringUtils.defaultIfEmpty(def.getDefaultValue(), null);
+    if (def == null) {
+      return null;
     }
-    return null;
+    return StringUtils.defaultIfEmpty(def.getDefaultValue(), null);
   }
 
   public String getCategory(String key) {
index 5b118ecd4fccdbdcc77dfc0ebb6078edbd2cd42e..3ec41a5cf6e79594bb8f44c867db3635778902fc 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.api.config;
 
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -29,7 +31,12 @@ import org.sonar.api.ServerComponent;
 import org.sonar.api.utils.DateUtils;
 
 import javax.annotation.Nullable;
-import java.util.*;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
 
 /**
  * Project Settings on batch side, Global Settings on server side. This component does not access to database, so
@@ -160,6 +167,20 @@ public class Settings implements BatchComponent, ServerComponent {
    * </ul>
    */
   public final String[] getStringArray(String key) {
+    PropertyDefinition property = getDefinitions().get(key);
+    if ((null != property) && (property.isMultiValues())) {
+      String value = getString(key);
+      if (value == null) {
+        return ArrayUtils.EMPTY_STRING_ARRAY;
+      }
+
+      List<String> values = Lists.newArrayList();
+      for (String v : Splitter.on(",").trimResults().split(value)) {
+        values.add(v.replace("%2C", ","));
+      }
+      return values.toArray(new String[values.size()]);
+    }
+
     return getStringArrayBySeparator(key, ",");
   }
 
@@ -213,6 +234,32 @@ public class Settings implements BatchComponent, ServerComponent {
     return setProperty(key, newValue);
   }
 
+  public final Settings setProperty(String key, @Nullable String[] values) {
+    PropertyDefinition property = getDefinitions().get(key);
+    if ((null == property) || (!property.isMultiValues())) {
+      throw new IllegalStateException("Fail to set multiple values on a single value property " + key);
+    }
+
+    if (values == null) {
+      properties.remove(key);
+      doOnRemoveProperty(key);
+    } else {
+      List<String> escaped = Lists.newArrayList();
+      for (String value : values) {
+        if (null != value) {
+          escaped.add(value.replace(",", "%2C"));
+        } else {
+          escaped.add("");
+        }
+      }
+
+      String escapedValue = Joiner.on(',').join(escaped);
+      properties.put(key, StringUtils.trim(escapedValue));
+      doOnSetProperty(key, escapedValue);
+    }
+    return this;
+  }
+
   public final Settings setProperty(String key, @Nullable String value) {
     if (value == null) {
       properties.remove(key);
index 793b4077356093ebf4391f14117f8875773475f3..d65d3af3c861a065bd11d8af1aecc1d4da69f6eb 100644 (file)
  */
 package org.sonar.api.config;
 
-import org.hamcrest.core.Is;
 import org.junit.Test;
 import org.sonar.api.Properties;
 import org.sonar.api.Property;
 import org.sonar.api.PropertyType;
 import org.sonar.api.utils.AnnotationUtils;
 
-import java.util.Arrays;
-
-import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.assertThat;
-import static org.junit.matchers.JUnitMatchers.hasItems;
+import static org.fest.assertions.Assertions.assertThat;
 
 public class PropertyDefinitionTest {
-
   @Test
   public void createFromAnnotation() {
     Properties props = AnnotationUtils.getAnnotation(Init.class, Properties.class);
@@ -41,23 +35,22 @@ public class PropertyDefinitionTest {
 
     PropertyDefinition def = PropertyDefinition.create(prop);
 
-    assertThat(def.getKey(), Is.is("hello"));
-    assertThat(def.getName(), Is.is("Hello"));
-    assertThat(def.getDefaultValue(), Is.is("world"));
-    assertThat(def.getCategory(), Is.is("categ"));
-    assertThat(def.getOptions().length, Is.is(2));
-    assertThat(Arrays.asList(def.getOptions()), hasItems("de", "en"));
-    assertThat(def.getDescription(), Is.is("desc"));
-    assertThat(def.getType(), Is.is(PropertyType.FLOAT));
-    assertThat(def.isGlobal(), Is.is(false));
-    assertThat(def.isOnProject(), Is.is(true));
-    assertThat(def.isOnModule(), Is.is(true));
+    assertThat(def.getKey()).isEqualTo("hello");
+    assertThat(def.getName()).isEqualTo("Hello");
+    assertThat(def.getDefaultValue()).isEqualTo("world");
+    assertThat(def.getCategory()).isEqualTo("categ");
+    assertThat(def.getOptions()).hasSize(2);
+    assertThat(def.getOptions()).contains("de", "en");
+    assertThat(def.getDescription()).isEqualTo("desc");
+    assertThat(def.getType()).isEqualTo(PropertyType.FLOAT);
+    assertThat(def.isGlobal()).isFalse();
+    assertThat(def.isOnProject()).isTrue();
+    assertThat(def.isOnModule()).isTrue();
+    assertThat(def.isMultiValues()).isTrue();
   }
 
-  @Properties({
-    @Property(key = "hello", name = "Hello", defaultValue = "world", description = "desc",
-      options = {"de", "en"}, category = "categ", type = PropertyType.FLOAT, global = false, project = true, module = true)
-  })
+  @Properties(@Property(key = "hello", name = "Hello", defaultValue = "world", description = "desc",
+    options = {"de", "en"}, category = "categ", type = PropertyType.FLOAT, global = false, project = true, module = true, multiValues = true))
   static class Init {
   }
 
@@ -68,21 +61,20 @@ public class PropertyDefinitionTest {
 
     PropertyDefinition def = PropertyDefinition.create(prop);
 
-    assertThat(def.getKey(), Is.is("hello"));
-    assertThat(def.getName(), Is.is("Hello"));
-    assertThat(def.getDefaultValue(), Is.is(""));
-    assertThat(def.getCategory(), Is.is(""));
-    assertThat(def.getOptions().length, Is.is(0));
-    assertThat(def.getDescription(), Is.is(""));
-    assertThat(def.getType(), Is.is(PropertyType.STRING));
-    assertThat(def.isGlobal(), Is.is(true));
-    assertThat(def.isOnProject(), Is.is(false));
-    assertThat(def.isOnModule(), Is.is(false));
+    assertThat(def.getKey()).isEqualTo("hello");
+    assertThat(def.getName()).isEqualTo("Hello");
+    assertThat(def.getDefaultValue()).isEmpty();
+    assertThat(def.getCategory()).isEmpty();
+    assertThat(def.getOptions()).isEmpty();
+    assertThat(def.getDescription()).isEmpty();
+    assertThat(def.getType()).isEqualTo(PropertyType.STRING);
+    assertThat(def.isGlobal()).isTrue();
+    assertThat(def.isOnProject()).isFalse();
+    assertThat(def.isOnModule()).isFalse();
+    assertThat(def.isMultiValues()).isFalse();
   }
 
-  @Properties({
-    @Property(key = "hello", name = "Hello")
-  })
+  @Properties(@Property(key = "hello", name = "Hello"))
   static class DefaultValues {
   }
 
@@ -90,70 +82,68 @@ public class PropertyDefinitionTest {
   public void validate_string() {
     PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.STRING, new String[0]);
 
-    assertThat(def.validate(null).isValid(), is(true));
-    assertThat(def.validate("").isValid(), is(true));
-    assertThat(def.validate("   ").isValid(), is(true));
-    assertThat(def.validate("foo").isValid(), is(true));
+    assertThat(def.validate(null).isValid()).isTrue();
+    assertThat(def.validate("").isValid()).isTrue();
+    assertThat(def.validate("   ").isValid()).isTrue();
+    assertThat(def.validate("foo").isValid()).isTrue();
   }
 
   @Test
   public void validate_boolean() {
     PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.BOOLEAN, new String[0]);
 
-    assertThat(def.validate(null).isValid(), is(true));
-    assertThat(def.validate("").isValid(), is(true));
-    assertThat(def.validate("   ").isValid(), is(true));
-    assertThat(def.validate("true").isValid(), is(true));
-    assertThat(def.validate("false").isValid(), is(true));
+    assertThat(def.validate(null).isValid()).isTrue();
+    assertThat(def.validate("").isValid()).isTrue();
+    assertThat(def.validate("   ").isValid()).isTrue();
+    assertThat(def.validate("true").isValid()).isTrue();
+    assertThat(def.validate("false").isValid()).isTrue();
 
-    assertThat(def.validate("foo").isValid(), is(false));
-    assertThat(def.validate("foo").getErrorKey(), is("notBoolean"));
+    assertThat(def.validate("foo").isValid()).isFalse();
+    assertThat(def.validate("foo").getErrorKey()).isEqualTo("notBoolean");
   }
 
   @Test
   public void validate_integer() {
     PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.INTEGER, new String[0]);
 
-    assertThat(def.validate(null).isValid(), is(true));
-    assertThat(def.validate("").isValid(), is(true));
-    assertThat(def.validate("   ").isValid(), is(true));
-    assertThat(def.validate("123456").isValid(), is(true));
+    assertThat(def.validate(null).isValid()).isTrue();
+    assertThat(def.validate("").isValid()).isTrue();
+    assertThat(def.validate("   ").isValid()).isTrue();
+    assertThat(def.validate("123456").isValid()).isTrue();
 
-    assertThat(def.validate("foo").isValid(), is(false));
-    assertThat(def.validate("foo").getErrorKey(), is("notInteger"));
+    assertThat(def.validate("foo").isValid()).isFalse();
+    assertThat(def.validate("foo").getErrorKey()).isEqualTo("notInteger");
   }
 
   @Test
   public void validate_float() {
     PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.FLOAT, new String[0]);
 
-    assertThat(def.validate(null).isValid(), is(true));
-    assertThat(def.validate("").isValid(), is(true));
-    assertThat(def.validate("   ").isValid(), is(true));
-    assertThat(def.validate("123456").isValid(), is(true));
-    assertThat(def.validate("3.14").isValid(), is(true));
+    assertThat(def.validate(null).isValid()).isTrue();
+    assertThat(def.validate("").isValid()).isTrue();
+    assertThat(def.validate("   ").isValid()).isTrue();
+    assertThat(def.validate("123456").isValid()).isTrue();
+    assertThat(def.validate("3.14").isValid()).isTrue();
 
-    assertThat(def.validate("foo").isValid(), is(false));
-    assertThat(def.validate("foo").getErrorKey(), is("notFloat"));
+    assertThat(def.validate("foo").isValid()).isFalse();
+    assertThat(def.validate("foo").getErrorKey()).isEqualTo("notFloat");
   }
 
   @Test
   public void validate_single_select_list() {
-    PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.SINGLE_SELECT_LIST, new String[]{"de", "en"});
+    PropertyDefinition def = PropertyDefinition.create("foo", PropertyType.SINGLE_SELECT_LIST, new String[] {"de", "en"});
 
-    assertThat(def.validate(null).isValid(), is(true));
-    assertThat(def.validate("").isValid(), is(true));
-    assertThat(def.validate("   ").isValid(), is(true));
-    assertThat(def.validate("de").isValid(), is(true));
-    assertThat(def.validate("en").isValid(), is(true));
+    assertThat(def.validate(null).isValid()).isTrue();
+    assertThat(def.validate("").isValid()).isTrue();
+    assertThat(def.validate("   ").isValid()).isTrue();
+    assertThat(def.validate("de").isValid()).isTrue();
+    assertThat(def.validate("en").isValid()).isTrue();
 
-    assertThat(def.validate("fr").isValid(), is(false));
-    assertThat(def.validate("fr").getErrorKey(), is("notInOptions"));
+    assertThat(def.validate("fr").isValid()).isFalse();
+    assertThat(def.validate("fr").getErrorKey()).isEqualTo("notInOptions");
   }
 
-  @Properties({
-    @Property(key = "scm.password.secured", name = "SCM password")
-  })
+  @Properties(@Property(key = "scm.password.secured", name = "SCM password"))
   static class OldScmPlugin {
   }
 
@@ -164,13 +154,11 @@ public class PropertyDefinitionTest {
 
     PropertyDefinition def = PropertyDefinition.create(prop);
 
-    assertThat(def.getKey(), Is.is("scm.password.secured"));
-    assertThat(def.getType(), Is.is(PropertyType.PASSWORD));
+    assertThat(def.getKey()).isEqualTo("scm.password.secured");
+    assertThat(def.getType()).isEqualTo(PropertyType.PASSWORD);
   }
 
-  @Properties({
-    @Property(key = "views.license.secured", name = "Views license")
-  })
+  @Properties(@Property(key = "views.license.secured", name = "Views license"))
   static class ViewsPlugin {
   }
 
@@ -181,7 +169,7 @@ public class PropertyDefinitionTest {
 
     PropertyDefinition def = PropertyDefinition.create(prop);
 
-    assertThat(def.getKey(), Is.is("views.license.secured"));
-    assertThat(def.getType(), Is.is(PropertyType.LICENSE));
+    assertThat(def.getKey()).isEqualTo("views.license.secured");
+    assertThat(def.getType()).isEqualTo(PropertyType.LICENSE);
   }
 }
index 1320d9e376d930b9b9064c4a8188922e1da68d3a..9f30157e3bd3f05131c4893b7648437adf1ac5bd 100644 (file)
@@ -23,6 +23,7 @@ import org.junit.Test;
 import org.sonar.api.Properties;
 import org.sonar.api.Property;
 
+import static org.fest.assertions.Assertions.assertThat;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.junit.Assert.assertThat;
@@ -87,4 +88,23 @@ public class PropertyDefinitionsTest {
   })
   static final class Categories {
   }
+
+  @Test
+  public void should_group_by_category() {
+    PropertyDefinitions def = new PropertyDefinitions(ByCategory.class);
+
+    assertThat(def.getGlobalPropertiesByCategory().keySet()).containsOnly("catGlobal1", "catGlobal2");
+    assertThat(def.getProjectPropertiesByCategory().keySet()).containsOnly("catProject");
+    assertThat(def.getModulePropertiesByCategory().keySet()).containsOnly("catModule");
+  }
+
+  @Properties({
+    @Property(key = "global1", name = "Global1", category = "catGlobal1", global = true, project = false, module = false),
+    @Property(key = "global2", name = "Global2", category = "catGlobal1", global = true, project = false, module = false),
+    @Property(key = "global3", name = "Global3", category = "catGlobal2", global = true, project = false, module = false),
+    @Property(key = "project", name = "Project", category = "catProject", global = false, project = true, module = false),
+    @Property(key = "module", name = "Module", category = "catModule", global = false, project = false, module = true)
+  })
+  static final class ByCategory {
+  }
 }
index 00757d04646da2f724df97ad76104b7ab9bbd47a..cef3a3fbdfb3915b7e3553b4abfc6db1c9c5fd9f 100644 (file)
@@ -40,7 +40,8 @@ public class SettingsTest {
     @Property(key = "boolean", name = "Boolean", defaultValue = "true"),
     @Property(key = "falseboolean", name = "False Boolean", defaultValue = "false"),
     @Property(key = "integer", name = "Integer", defaultValue = "12345"),
-    @Property(key = "array", name = "Array", defaultValue = "one,two,three")
+    @Property(key = "array", name = "Array", defaultValue = "one,two,three"),
+    @Property(key = "multi_values", name = "Array", defaultValue = "1,2,3", multiValues = true)
   })
   static class Init {
   }
@@ -153,6 +154,52 @@ public class SettingsTest {
     assertThat(array).isEqualTo(new String[]{"one", "two", "three"});
   }
 
+  @Test
+  public void setStringArray() {
+    Settings settings = new Settings(definitions);
+    settings.setProperty("multi_values", new String[] {"A", "B"});
+    String[] array = settings.getStringArray("multi_values");
+    assertThat(array).isEqualTo(new String[] {"A", "B"});
+  }
+
+  @Test
+  public void setStringArrayTrimValues() {
+    Settings settings = new Settings(definitions);
+    settings.setProperty("multi_values", new String[] {" A ", " B "});
+    String[] array = settings.getStringArray("multi_values");
+    assertThat(array).isEqualTo(new String[] {"A", "B"});
+  }
+
+  @Test
+  public void setStringArrayEscapeCommas() {
+    Settings settings = new Settings(definitions);
+    settings.setProperty("multi_values", new String[] {"A,B", "C,D"});
+    String[] array = settings.getStringArray("multi_values");
+    assertThat(array).isEqualTo(new String[] {"A,B", "C,D"});
+  }
+
+  @Test
+  public void setStringArrayWithEmptyValues() {
+    Settings settings = new Settings(definitions);
+    settings.setProperty("multi_values", new String[] {"A,B", "", "C,D"});
+    String[] array = settings.getStringArray("multi_values");
+    assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"});
+  }
+
+  @Test
+  public void setStringArrayWithNullValues() {
+    Settings settings = new Settings(definitions);
+    settings.setProperty("multi_values", new String[] {"A,B", null, "C,D"});
+    String[] array = settings.getStringArray("multi_values");
+    assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"});
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void shouldFailToSetArrayValueOnSingleValueProperty() {
+    Settings settings = new Settings(definitions);
+    settings.setProperty("array", new String[] {"A", "B", "C"});
+  }
+
   @Test
   public void getStringArray_no_value() {
     Settings settings = new Settings();
index 92d1c85d95c34550ad98f87e0d59acdfcee88577..5554c8f472865a7a05c0b261f7d515b16b9cbf49 100644 (file)
@@ -49,7 +49,7 @@ class Api::AuthenticationController < Api::ApiController
   end
 
   def force_authentication?
-    property = Property.find(:first, :conditions => {:prop_key => org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY, :resource_id => nil, :user_id => nil})
+    property = Property.by_key(org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY)
     property ? property.value == 'true' : false
   end
 
index fb98e5b6f1f20a28d066dbc6b97d064910c859bb..4cab0f299cf26e003bf875c1af945583a0aaeca6 100644 (file)
@@ -43,7 +43,7 @@ class Api::UserPropertiesController < Api::ApiController
   # curl http://localhost:9000/api/user_properties/<key> -v -u admin:admin
   #
   def show
-    property = Property.find(:first, :conditions => ['user_id=? and prop_key=?', current_user.id, params[:id]])
+    property = Property.by_key(params[:id], nil, current_user.id)
     if property
       respond_to do |format|
         format.json { render :json => jsonp(properties_to_json([property])) }
@@ -65,7 +65,7 @@ class Api::UserPropertiesController < Api::ApiController
     value = params[:value]
     if key
       begin
-        Property.delete_all(['prop_key=? AND user_id=?', key,current_user.id])
+        Property.clear(key, nil, current_user.id)
         property=Property.create(:prop_key => key, :text_value => value, :user_id => current_user.id)
         respond_to do |format|
           format.json { render :json => jsonp(properties_to_json([property])) }
@@ -88,10 +88,9 @@ class Api::UserPropertiesController < Api::ApiController
   def destroy
     begin
       if params[:id]
-        Property.delete_all(['prop_key=? AND user_id=?', params[:id], current_user.id])
+        Property.clear(params[:id], nil, current_user.id)
       end
       render_success("Property deleted")
-
     rescue Exception => e
       logger.error("Fails to execute #{request.url} : #{e.message}")
       render_error(e.message)
index de19cb7b14fdc2d121cc855e0e7dc32739c74341..76918622d64fb1f2f9c45fb981f40a513ee603b0 100644 (file)
@@ -18,7 +18,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
 #
 class ProjectController < ApplicationController
-  verify :method => :post, :only => [:set_links, :set_exclusions, :delete_exclusions, :update_key, :perform_key_bulk_update, :update_quality_profile], 
+  verify :method => :post, :only => [:set_links, :set_exclusions, :delete_exclusions, :update_key, :perform_key_bulk_update, :update_quality_profile],
          :redirect_to => {:action => :index}
   verify :method => :delete, :only => [:delete], :redirect_to => {:action => :index}
 
@@ -33,11 +33,11 @@ class ProjectController < ApplicationController
 
     if java_facade.getResourceTypeBooleanProperty(@project.qualifier, 'deletable')
       deletion_manager = ResourceDeletionManager.instance
-      if deletion_manager.currently_deleting_resources? || 
+      if deletion_manager.currently_deleting_resources? ||
         (!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?)
         # a deletion is happening or it has just finished with errors => display the message from the Resource Deletion Manager
         render :template => 'project/pending_deletion'
-      else    
+      else
         @snapshot=@project.last_snapshot
       end
     else
@@ -47,19 +47,19 @@ class ProjectController < ApplicationController
 
   def delete
     @project = get_current_project(params[:id])
-    
+
     # Ask the resource deletion manager to start the migration
     # => this is an asynchronous AJAX call
     ResourceDeletionManager.instance.delete_resources([@project.id])
-    
+
     # and return some text that will actually never be displayed
     render :text => ResourceDeletionManager.instance.message
   end
-  
+
   def pending_deletion
     deletion_manager = ResourceDeletionManager.instance
-    
-    if deletion_manager.currently_deleting_resources? || 
+
+    if deletion_manager.currently_deleting_resources? ||
       (!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?)
       # display the same page again and again
       # => implicit render "pending_deletion.html.erb"
@@ -67,24 +67,24 @@ class ProjectController < ApplicationController
       redirect_to_default
     end
   end
-  
+
   def dismiss_deletion_message
     # It is important to reinit the ResourceDeletionManager so that the deletion screens can be available again
     ResourceDeletionManager.instance.reinit
-    
+
     redirect_to :action => 'deletion', :id => params[:id]
   end
-  
+
   def quality_profile
     @project = get_current_project(params[:id])
     @profiles = Profile.find(:all, :conditions => {:language => @project.language, :enabled => true})
   end
-  
+
   def update_quality_profile
     project = get_current_project(params[:id])
-    
+
     selected_profile = Profile.find(:first, :conditions => {:id => params[:quality_profile].to_i})
-    if selected_profile && selected_profile.language == project.language 
+    if selected_profile && selected_profile.language == project.language
       project.profile = selected_profile
       project.save!
       flash[:notice] = message('project_quality_profile.profile_successfully_updated')
@@ -92,17 +92,17 @@ class ProjectController < ApplicationController
       selected_profile_name = selected_profile ? selected_profile.name + "(" + selected_profile.language + ")" : "Unknown profile"
       flash[:error] = message('project_quality_profile.project_cannot_be_update_with_profile_x', :params => selected_profile_name)
     end
-    
+
     redirect_to :action => 'quality_profile', :id => project.id
   end
-  
+
   def key
     @project = get_current_project(params[:id])
   end
-  
+
   def update_key
     project = get_current_project(params[:id])
-    
+
     new_key = params[:new_key].strip
     if new_key.blank?
       flash[:error] = message('update_key.new_key_cant_be_blank_for_x', :params => project.key)
@@ -115,17 +115,17 @@ class ProjectController < ApplicationController
         java_facade.updateResourceKey(project.id, new_key)
         flash[:notice] = message('update_key.key_updated')
       rescue Exception => e
-        flash[:error] = message('update_key.error_occured_while_renaming_key_of_x', 
+        flash[:error] = message('update_key.error_occured_while_renaming_key_of_x',
                                 :params => [project.key, Api::Utils.exception_message(e, :backtrace => false)])
       end
     end
-    
+
     redirect_to :action => 'key', :id => project.root_project.id
   end
-  
+
   def prepare_key_bulk_update
     @project = get_current_project(params[:id])
-    
+
     @string_to_replace = params[:string_to_replace].strip
     @replacement_string = params[:replacement_string].strip
     if @string_to_replace.blank? || @replacement_string.blank?
@@ -148,20 +148,20 @@ class ProjectController < ApplicationController
 
   def perform_key_bulk_update
     project = get_current_project(params[:id])
-    
+
     string_to_replace = params[:string_to_replace].strip
     replacement_string = params[:replacement_string].strip
-      
+
     unless string_to_replace.blank? || replacement_string.blank?
       begin
         java_facade.bulkUpdateKey(project.id, string_to_replace, replacement_string)
         flash[:notice] = message('update_key.key_updated')
       rescue Exception => e
-        flash[:error] = message('update_key.error_occured_while_renaming_key_of_x', 
+        flash[:error] = message('update_key.error_occured_while_renaming_key_of_x',
                                 :params => [project.key, Api::Utils.exception_message(e, :backtrace => false)])
       end
     end
-    
+
     redirect_to :action => 'key', :id => project.id
   end
 
@@ -224,32 +224,34 @@ class ProjectController < ApplicationController
     redirect_to :action => 'links', :id => project.id
   end
 
-
   def settings
-    @project = get_current_project(params[:id])
+    @resource = get_current_project(params[:id])
 
-    @snapshot=@project.last_snapshot
-    if !@project.project? && !@project.module?
+    @snapshot = @resource.last_snapshot
+    if !@resource.project? && !@resource.module?
       redirect_to :action => 'index', :id => params[:id]
     end
 
-    @category=params[:category] ||= 'general'
-    @definitions_per_category={}
-    definitions = java_facade.getPropertyDefinitions()
-    properties = definitions.getAll().select { |property| (@project.module? && property.isOnModule()) || (@project.project? && property.isOnProject()) }
-    properties.each do |property|
-      category = definitions.getCategory(property.getKey())
-      @definitions_per_category[category]||=[]
-      @definitions_per_category[category]<<property
+    if @resource.nil?
+      definitions_per_category = java_facade.propertyDefinitions.globalPropertiesByCategory
+    elsif @resource.project?
+      definitions_per_category = java_facade.propertyDefinitions.projectPropertiesByCategory
+    elsif @resource.module?
+      definitions_per_category = java_facade.propertyDefinitions.modulePropertiesByCategory
     end
-  end
 
+    @category = params[:category] || 'general'
+    @categories = definitions_per_category.keys
+    @definitions = definitions_per_category[@category] || []
+
+    not_found('category') unless @categories.include? @category
+  end
 
   def events
     @categories = EventCategory.categories(true)
     @snapshot = Snapshot.find(params[:id])
     @category = params[:category]
-      
+
     conditions = "resource_id=:resource_id"
     values = {:resource_id => @snapshot.project_id}
     unless @category.blank?
@@ -260,7 +262,7 @@ class ProjectController < ApplicationController
     snapshots_to_be_deleted = Snapshot.find(:all, :conditions => ["status='U' AND project_id=?", @snapshot.project_id])
     unless snapshots_to_be_deleted.empty?
       conditions << " AND snapshot_id NOT IN (:sids)"
-      values[:sids] = snapshots_to_be_deleted.map {|s| s.id}
+      values[:sids] = snapshots_to_be_deleted.map { |s| s.id }
     end
 
     category_names=@categories.map { |cat| cat.name }
@@ -342,7 +344,7 @@ class ProjectController < ApplicationController
     snapshot=Snapshot.find(params[:sid])
     not_found("Snapshot not found") unless snapshot
     access_denied unless is_admin?(snapshot)
-    
+
     # We update all the related snapshots to have the same version as the next snapshot
     next_snapshot = Snapshot.find(:first, :conditions => ['created_at>? and project_id=?', snapshot.created_at, snapshot.project_id], :order => 'created_at asc')
     snapshots = find_project_snapshots(snapshot.id)
@@ -371,11 +373,11 @@ class ProjectController < ApplicationController
     else
       snapshots = find_project_snapshots(snapshot.id)
       snapshots.each do |s|
-      e = Event.new({:name => params[:event_name], 
-                     :category => EventCategory::KEY_OTHER,
-                     :snapshot => s,
-                     :resource_id => s.project_id,
-                     :event_date => s.created_at})
+        e = Event.new({:name => params[:event_name],
+                       :category => EventCategory::KEY_OTHER,
+                       :snapshot => s,
+                       :resource_id => s.project_id,
+                       :event_date => s.created_at})
         e.save!
       end
       flash[:notice] = message('project_history.event_created', :params => params[:event_name])
@@ -425,7 +427,7 @@ class ProjectController < ApplicationController
     access_denied unless is_admin?(project)
     project
   end
-  
+
   def find_project_snapshots(root_snapshot_id)
     snapshots = Snapshot.find(:all, :include => 'events', :conditions => ["(root_snapshot_id = ? OR id = ?) AND scope = 'PRJ'", root_snapshot_id, root_snapshot_id])
   end
index 949775f5e82034c97e19f83cc36ae3cc181fa822..50282117749c98fe229cb53f471539236b5fb000 100644 (file)
@@ -21,80 +21,54 @@ class SettingsController < ApplicationController
 
   SECTION=Navigation::SECTION_CONFIGURATION
 
-  SPECIAL_CATEGORIES=['email', 'encryption', 'server_id']
+  SPECIAL_CATEGORIES=%w(email encryption server_id)
 
-  verify :method => :post, :only => ['update'], :redirect_to => {:action => :index}
+  verify :method => :post, :only => %w(update), :redirect_to => {:action => :index}
+  before_filter :admin_required, :only => %w(index)
 
   def index
-    access_denied unless is_admin?
-    load_properties(nil)
-    @category ||= 'general'
+    load_properties()
   end
 
   def update
-    @project=nil
-    resource_id=nil
-    if params[:resource_id]
-      @project=Project.by_key(params[:resource_id])
-      access_denied unless (@project && is_admin?(@project))
-      resource_id=@project.id
-    else
-      access_denied unless is_admin?
-    end
-    is_global=(@project.nil?)
-
-    load_properties(@project)
-
-    @persisted_properties_per_key={}
-    if @category && @definitions_per_category[@category]
-      @definitions_per_category[@category].each do |property|
-        value=params[property.getKey()]
-        persisted_property = Property.find(:first, :conditions => {:prop_key=> property.key(), :resource_id => resource_id, :user_id => nil})
-
-        # update the property
-        if persisted_property
-          if value.empty?
-            Property.delete_all('prop_key' => property.key(), 'resource_id' => resource_id, 'user_id' => nil)
-            java_facade.setGlobalProperty(property.getKey(), nil) if is_global
-          elsif persisted_property.text_value != value.to_s
-            persisted_property.text_value = value.to_s
-            if persisted_property.save && is_global
-              java_facade.setGlobalProperty(property.getKey(), value.to_s)
-            end
-            @persisted_properties_per_key[persisted_property.key]=persisted_property
-          end
-
-        # create the property
-        elsif value.present?
-          persisted_property=Property.new(:prop_key => property.key(), :text_value => value.to_s, :resource_id => resource_id)
-          if persisted_property.save && is_global
-            java_facade.setGlobalProperty(property.getKey(), value.to_s)
-          end
-          @persisted_properties_per_key[persisted_property.key]=persisted_property
-        end
-      end
+    resource_id = params[:resource_id]
+    @resource = Project.by_key(resource_id) if resource_id
+
+    access_denied if (@resource && !is_admin?(@resource))
+    access_denied if (@resource.nil? && !is_admin?)
 
-      params[:layout]='false'
-      render :partial => 'settings/properties'
+    load_properties()
+
+    @definitions.map(&:key).each do |key|
+      value = params[key]
+
+      if value.blank?
+        Property.clear(key, resource_id)
+      else
+        Property.set(key, value, resource_id)
+      end
     end
+
+    render :partial => 'settings/properties'
   end
 
   private
 
-  def load_properties(project)
-    @category=params[:category]
-    @definitions_per_category={}
-    definitions = java_facade.getPropertyDefinitions()
-    definitions.getAll().select { |property_definition|
-      (project.nil? && property_definition.isGlobal()) || (project && project.module? && property_definition.isOnModule()) || (project && project.project? && property_definition.isOnProject())
-    }.each do |property_definition|
-      category = definitions.getCategory(property_definition.getKey())
-      @definitions_per_category[category]||=[]
-      @definitions_per_category[category]<<property_definition
-    end
+  def load_properties
+    @category = params[:category] || 'general'
 
-    SPECIAL_CATEGORIES.each do |category|
-      @definitions_per_category[category]=[]
+    if @resource.nil?
+      definitions_per_category = java_facade.propertyDefinitions.globalPropertiesByCategory
+    elsif @resource.project?
+      definitions_per_category = java_facade.propertyDefinitions.projectPropertiesByCategory
+    elsif @resource.module?
+      definitions_per_category = java_facade.propertyDefinitions.modulePropertiesByCategory
     end
+
+    @categories = definitions_per_category.keys + SPECIAL_CATEGORIES
+    @definitions = definitions_per_category[@category] || []
+
+    not_found('category') unless @categories.include? @category
   end
+
 end
index e4028760ddb4c6898c46b4f407fd9bba70aea76b..90ca275324de1ec8749852dc6c0c702f63b01d79 100644 (file)
@@ -19,6 +19,7 @@
  #
 module ProjectHelper
   include ActionView::Helpers::UrlHelper
+  include SettingsHelper
 
   def formatted_value(measure, default='')
     measure ? measure.formatted_value : default
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/helpers/settings_helper.rb b/sonar-server/src/main/webapp/WEB-INF/app/helpers/settings_helper.rb
new file mode 100644 (file)
index 0000000..b6dfedd
--- /dev/null
@@ -0,0 +1,56 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2012 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# Sonar is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# Sonar is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Sonar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+#
+module SettingsHelper
+  def category_name(category)
+    message_or_default("property.category.#{category}", category)
+  end
+
+  def property_name(property)
+    message_or_default("property.#{property.key()}.name", property.name())
+  end
+
+  def property_description(property)
+    message_or_default("property.#{property.key()}.description", property.description())
+  end
+
+  def property_value(property)
+    if property.multi_values
+      Property.values(property.key, @resource ? @resource.id : nil)
+    else
+      Property.value(property.key, @resource ? @resource.id : nil, '')
+    end
+  end
+
+  # for backward-compatibility with properties that do not define the type TEXT
+  def property_type(property, value)
+    if property.getType().to_s=='STRING' && value && value.include?('\n')
+      return 'TEXT'
+    end
+    property.getType()
+  end
+
+  def message_or_default(message_key, default)
+    message(message_key, :default => default)
+  end
+
+  def by_name(categories)
+    categories.sort_by { |category| category_name(category) }
+  end
+end
index 3f8dfb72de6bc1b4e44c8a39939dde947ab7331c..39f617657ee319281f6be8d849339f71f16d6856 100644 (file)
 class Property < ActiveRecord::Base
   validates_presence_of :prop_key
 
+  named_scope :with_key, lambda { |value| {:conditions => {:prop_key, value}} }
+  named_scope :with_resource, lambda { |value| {:conditions => {:resource_id => value}} }
+  named_scope :with_user, lambda { |value| {:conditions => {:user_id => value}} }
+
   def key
     prop_key
   end
@@ -28,33 +32,48 @@ class Property < ActiveRecord::Base
     text_value
   end
 
-  def self.hash(resource_id=nil)
-    hash={}
-    Property.find(:all, :conditions => {'resource_id' => resource_id, 'user_id' => nil}).each do |prop|
-      hash[prop.key]=prop.value
-    end
-    hash
+  def self.hash(resource_id=nil, user_id=nil)
+    properties = Property.with_resource(resource_id).with_user(user_id)
+
+    Hash[properties.map { |prop| [prop.key, prop.value] }]
   end
 
-  def self.value(key, resource_id=nil, default_value=nil)
-    prop=Property.find(:first, :conditions => {'prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil})
-    if prop
-      prop.text_value || default_value
-    else
-      default_value
-    end
+  def self.clear(key, resource_id=nil, user_id=nil)
+    all(key, resource_id, user_id).delete_all
+    Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, nil) unless resource_id
+  end
+
+  def self.by_key(key, resource_id=nil, user_id=nil)
+    all(key, resource_id, user_id).first
+  end
+
+  def self.by_key_prefix(prefix)
+    Property.find(:all, :conditions => ['prop_key like ?', prefix + '%'])
+  end
+
+  def self.value(key, resource_id=nil, default_value=nil, user_id=nil)
+    property = by_key(key, resource_id, user_id)
+    return default_value unless property
+
+    property.text_value || default_value
   end
 
-  def self.values(key, resource_id=nil)
-    Property.find(:all, :conditions => {'prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil}).collect { |p| p.text_value }
+  def self.values(key, resource_id=nil, user_id=nil)
+    value = value(key, resource_id, '', user_id)
+    values = value.split(',')
+    values.empty? ? [nil] : values.map { |v| v.gsub('%2C', ',') }
   end
 
-  def self.set(key, value, resource_id=nil)
+  def self.set(key, value, resource_id=nil, user_id=nil)
+    if value.kind_of? Array
+      value = value.map { |v| v.gsub(',', '%2C') }.join(',')
+    end
+
     text_value = (value.nil? ? nil : value.to_s)
-    prop = Property.new(:prop_key => key, :text_value => text_value, :resource_id => resource_id)
+    prop = Property.new(:prop_key => key, :text_value => text_value, :resource_id => resource_id, :user_id => user_id)
     if prop.valid?
       Property.transaction do
-        Property.delete_all('prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil)
+        Property.delete_all(:prop_key => key, :resource_id => resource_id, :user_id => user_id)
         prop.save
       end
       Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, text_value) unless resource_id
@@ -62,25 +81,8 @@ class Property < ActiveRecord::Base
     prop
   end
 
-  def self.clear(key, resource_id=nil)
-    Property.delete_all('prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil)
-    Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, nil) unless resource_id
-  end
-
-  def self.by_key(key, resource_id=nil)
-    Property.find(:first, :conditions => {'prop_key' => key, 'resource_id' => resource_id, 'user_id' => nil})
-  end
-
-  def self.by_key_prefix(prefix)
-    Property.find(:all, :conditions => ["prop_key like ?", prefix + '%'])
-  end
-
-  def self.update(key, value)
-    property = Property.find(:first, :conditions => {:prop_key => key, :resource_id => nil, :user_id => nil});
-    property.text_value = value
-    property.save
-    Java::OrgSonarServerUi::JRubyFacade.getInstance().setGlobalProperty(key, value)
-    property
+  def self.update(key, value, resource_id=nil, user_id=nil)
+    set(key, value, resource_id, user_id)
   end
 
   def to_hash_json
@@ -112,6 +114,10 @@ class Property < ActiveRecord::Base
 
   private
 
+  def self.all(key, resource_id=nil, user_id=nil)
+    Property.with_key(key).with_resource(resource_id).with_user(user_id)
+  end
+
   def validate
     if java_definition
       validation_result=java_definition.validate(text_value)
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb
new file mode 100644 (file)
index 0000000..a571f93
--- /dev/null
@@ -0,0 +1,5 @@
+<div class="multi_value">
+  <%= render "settings/type_#{property_type(property, value)}", :property => property, :value => value -%>
+  <a href="#" class="delete"></a>
+  <br/>
+</div>
index e77b7583790cc04c1ca6fa2e565f94f7143b87d9..3e9602d4820b4d7da70fae92200c45f60fb34c7c 100644 (file)
@@ -1,87 +1,67 @@
-<% if @category && @definitions_per_category[@category]
-     category_name = message("property.category.#{@category}", :default => @category)
-     if SettingsController::SPECIAL_CATEGORIES.include?(@category)
-%>
-    <%= render :partial => 'special', :locals => {:url => url_for(:controller => "#{@category}_configuration")} -%>
-  <%
-     elsif !@definitions_per_category[@category].empty?
-  %>
-    <% form_remote_tag :url => {:controller => 'settings', :action => 'update', :category => @category, :resource_id => @project ? @project.id : nil},
-                       :method => :post,
-                       :before => "$('submit_settings').hide();$('loading_settings').show()",
-                       :update => 'properties' do -%>
+<% if SettingsController::SPECIAL_CATEGORIES.include?(@category) -%>
+  <%= render 'special', :url => url_for(:controller => "#{@category}_configuration") -%>
+<% else -%>
+  <% form_remote_tag :url => {:controller => 'settings', :action => 'update', :category => @category, :resource_id => @resource ? @resource.id : nil},
+                     :method => :post,
+                     :before => "$('submit_settings').hide();$('loading_settings').show();",
+                     :update => 'properties',
+                     :script => false do -%>
 
-      <table class="data marginbottom10" style="margin: 10px">
-        <thead>
-        <tr>
-          <th>
-            <span><%= h(category_name) -%></span>
-          </th>
-        </tr>
-        </thead>
-        <tbody>
-        <%
-           if @definitions_per_category[@category]
-             @definitions_per_category[@category].each do |property|
-               value = nil
-
-               # set when form has been submitted but some errors have been raised
-               if @persisted_properties_per_key
-                 p = @persisted_properties_per_key[property.key]
-                 if p
-                   value = p.text_value
-                 end
-               end
+    <table class="data marginbottom10" style="margin: 10px;">
+      <thead>
+      <tr>
+        <th><%= h category_name(@category) -%></th>
+      </tr>
+      </thead>
+      <tbody>
+      <% @definitions.each do |property| -%>
+        <tr class="<%= cycle('even', 'odd', :name => 'properties') -%>">
+          <td style="padding: 10px" id="block_<%= property.key -%>">
+            <h3>
+              <div><%= property_name(property) -%></div>
+              <div class="note"><%= property.key -%></div>
+            </h3>
+            <% desc=property_description(property) -%>
+            <% unless desc.blank? %>
+              <p class="marginbottom10"><%= desc -%></p>
+            <% end -%>
 
+            <% value = property_value(property) -%>
+            <% if property.multi_values -%>
+              <% value.each do |sub_value| -%>
+                <%= render "settings/multi_value", :property => property, :value => sub_value -%>
+              <% end -%>
+              <div class="template" style="display:none;">
+                <%= render "settings/multi_value", :property => property, :value => nil -%>
+              </div>
+              <a href="#" class="add"><%= message('settings.add') -%></a>
+            <% else -%>
+              <%= render "settings/type_#{property_type(property, value)}", :property => property, :value => value -%>
+            <% end -%>
 
-               # if fresh form or no error, get the current value
-               value = Property.value(property.getKey(), (@project ? @project.id : nil), '') unless value
+            <% default_prop_value = (@resource ? Property.value(property.key, nil, property.defaultValue) : property.defaultValue) -%>
+            <% unless default_prop_value.blank? %>
+              <p class="note">Default: <%= property.type.to_s=='PASSWORD' ? '********' : h(default_prop_value) -%></p>
+            <% end -%>
+          </td>
+        </tr>
+      <% end -%>
+      </tbody>
+    </table>
+    <div style="padding-left: 16px;">
+      <%= submit_tag(message('settings.save_category', :params => [category_name(@category)]), :id => 'submit_settings') -%>
+      <img src="<%= ApplicationController.root_context -%>/images/loading.gif" id="loading_settings" style="display:none;">
+    </div>
+  <% end -%>
+<% end -%>
 
-               # for backward-compatibility with properties that do not define the type TEXT
-               property_type = property.getType()
-               if property_type.to_s=='STRING' && value.include?("\n")
-                 property_type = 'TEXT'
-               end
+<script type="text/javascript">
+  $j('.delete').live('click', function () {
+    $j(this).parent('.multi_value').remove();
+  });
 
-        %>
-            <tr class="<%= cycle('even', 'odd', :name => 'properties') -%>">
-              <td style="padding: 10px" id="block_<%= property.getKey() -%>">
-                <h3>
-                  <%= message("property.#{property.key()}.name", :default => property.name()) -%>
-                  <br/><span class="note"><%= property.getKey() -%></span>
-                </h3>
-                <%
-                   desc=message("property.#{property.key()}.description", :default => property.description())
-                   if desc.present? %>
-                  <p class="marginbottom10"><%= desc -%></p>
-                <% end %>
-                <div><%= render :partial => "settings/type_#{property_type}", :locals => {:property => property, :value => value} -%></div>
-                <%
-                   if p && !p.valid?
-                %>
-                    <div class="error"><%= p.validation_error_message -%></div>
-                  <%
-                     end
-                  %>
-                <p>
-                  <%
-                     default_prop_value = (@project ? Property.value(property.key(), nil, property.defaultValue()) : property.defaultValue())
-                     unless default_prop_value.blank? %>
-                    <span class="note">Default : <%= property.getType().to_s=='PASSWORD' ? '********' : h(default_prop_value) -%></span>
-                  <% end %>
-                </p>
-              </td>
-            </tr>
-          <% end
-             end
-          %>
-        </tbody>
-      </table>
-      <div style="padding-left: 16px">
-      <%= submit_tag(message('settings.save_category', :params => [category_name]), :id => 'submit_settings') -%>
-      <img src="<%= ApplicationController.root_context -%>/images/loading.gif" id="loading_settings" style="display:none">
-      </div>
-    <% end %>
-  <% end
-     end
-  %>
+  $j('.add').live('click', function () {
+    var template = $j(this).siblings('.template');
+    template.before(template.html());
+  });
+</script>
\ No newline at end of file
index b9975d9d0abe319bbce8bbb6f4d99eb98e85a66c..3411c37143a0b51dc00f5a61d617ab9d2dcbfde0 100644 (file)
@@ -1,28 +1,33 @@
-<style type="text/css">
-  #plugins .plugin {
-    padding: 5px;
-    border: 1px solid #ddd;
-    background-color: #fff;
-  }
+<h1 class="marginbottom10"><%= message('settings.page') -%></h1>
 
-  #plugins .plugin h2 {
-    margin-left: 10px;
-    font-size: 122%;
-    color: #333;
-  }
+<table width="100%">
+  <tr>
+    <td width="1%" nowrap class="column first">
+      <table class="data selector">
+        <thead>
+        <tr>
+          <th><%= message('category') -%></th>
+        </tr>
+        </thead>
+        <tbody>
+        <% by_name(@categories).each do |category| -%>
+          <tr id="select_<%= category -%>" class="select <%= cycle('even', 'odd', :name => 'category') -%> <%= 'selected' if @category==category -%>">
+            <td><%= link_to category_name(category), :category => category -%></td>
+          </tr>
+        <% end -%>
+        </tbody>
+      </table>
+      <br/>
+    </td>
 
-  #plugins .plugin h3 {
-    margin-left: 5px;
-  }
+    <td class="column">
+      <div id="properties">
+        <%= render 'settings/properties' -%>
+      </div>
+    </td>
+  </tr>
+</table>
 
-  #plugins .plugin p {
-    padding: 5px 5px;
-  }
-
-  #plugins .plugin img {
-    padding: 5px 0 0 5px;
-  }
-</style>
 <script type="text/javascript">
   function enlargeTextInput(propertyKey) {
     var eltId = 'input_' + propertyKey;
     $(eltId).parentNode.replace(textArea);
   }
 </script>
-<div id="plugins">
-  <h1 class="marginbottom10"><%= message('settings.page') -%></h1>
-  <table width="100%">
-    <tr>
-      <td width="1%" nowrap class="column first">
-        <table class="data selector">
-          <thead>
-          <tr>
-            <th>
-              <span>Category</span>
-            </th>
-          </tr>
-          </thead>
-          <tbody>
-          <%
-             @definitions_per_category.keys.sort_by { |category| message("property.category.#{category}", :default => category).upcase }.each do |category|
-               if !@definitions_per_category[category].empty? || SettingsController::SPECIAL_CATEGORIES.include?(category)
-          %>
-              <tr class="select <%= cycle('even', 'odd', :name => 'category') -%> <%= 'selected' if @category==category -%>" id="select_<%= category -%>">
-                <td><%= link_to message("property.category.#{category}", :default => category), :overwrite_params => {:category => category} -%></td>
-              </tr>
-            <% end
-               end
-            %>
-          </tbody>
-        </table>
-        <br/>
-      </td>
-
-      <td class="column">
-        <div id="properties" style="width:99%">
-          <%= render :partial => 'settings/properties' -%>
-        </div>
-      </td>
-    </tr>
-  </table>
-</div>
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_sidebar.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_sidebar.html.erb
deleted file mode 100644 (file)
index e69de29..0000000
index b4c3b451f4d84f638cbc12374b107a43ecd3fdc9..7292d327860e1946b1eb9a6bfbc3f84874a1be9e 100644 (file)
@@ -1,2 +1,2 @@
-<input type="text" name="<%= h property.getKey() -%>" value="<%= h value if value -%>" size="50" id="input_<%= h property.getKey() -%>"/>
-<%= link_to_function(image_tag('zoom.png'), "enlargeTextInput('#{property.getKey()}')", :class => 'nolink') -%>
\ No newline at end of file
+<input type="text" name="<%= h property.key -%>[]" value="<%= h value if value -%>" size="50" id="input_<%= h property.key -%>"/>
+<%= link_to_function(image_tag('zoom.png'), "enlargeTextInput('#{property.key}')", :class => 'nolink') -%>
\ No newline at end of file
index 81fa7362a09615d5875bc5dac4801385ec7da155..d2c6ebcae2cb4a7bc5532d3ffa2e5016c104a0da 100644 (file)
@@ -1 +1 @@
-<%= render :partial => 'settings', :locals => {:project=>nil} %>
\ No newline at end of file
+<%= render 'settings', :project => nil %>
\ No newline at end of file
index 74584a1093b076e7909cddfe6171144ce5c7e10d..ff58b01e30153bd5ea1a8a57e3002aa42e922297 100644 (file)
@@ -2087,6 +2087,11 @@ table.nowrap td, td.nowrap, th.nowrap {
   padding: 2px 0 2px 20px;
 }
 
+.delete {
+  background: url("../images/cross.png") no-repeat scroll left 50% transparent;
+  padding: 2px 0 2px 20px;
+}
+
 .restore {
   display: block;
   background: url("../images/restore.gif") no-repeat scroll left 50% transparent;