Browse Source

Merge remote-tracking branch 'origin/master'

tags/3.5
Simon Brandhof 11 years ago
parent
commit
e344cab899
50 changed files with 1242 additions and 524 deletions
  1. 366
    366
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
  2. 43
    8
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/GenerateAlertEvents.java
  3. 5
    6
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/HotspotMostViolatedRulesWidget.java
  4. 6
    6
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimelineWidget.java
  5. 4
    4
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java
  6. 2
    3
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/FalsePositiveReviewsWidget.java
  7. 2
    3
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/MyReviewsWidget.java
  8. 2
    3
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/PlannedReviewsWidget.java
  9. 2
    3
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ProjectReviewsWidget.java
  10. 2
    3
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/UnplannedReviewsWidget.java
  11. 76
    9
      plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
  12. 3
    3
      plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/action_plans.html.erb
  13. 56
    2
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/GenerateAlertEventsTest.java
  14. 11
    3
      plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java
  15. 107
    0
      plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/alerts/AlertsEmailTemplate.java
  16. 53
    0
      plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/alerts/AlertsOnMyFavouriteProject.java
  17. 1
    1
      plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/EmailNotificationsPluginTest.java
  18. 131
    0
      plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/alerts/AlertsEmailTemplateTest.java
  19. 62
    0
      plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/alerts/AlertsOnMyFavouriteProjectTest.java
  20. 1
    1
      pom.xml
  21. 2
    2
      sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchSettings.java
  22. 7
    1
      sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java
  23. 3
    1
      sonar-batch/src/main/java/org/sonar/batch/bootstrap/TaskBootstrapModule.java
  24. 3
    1
      sonar-batch/src/main/java/org/sonar/batch/tasks/Tasks.java
  25. 1
    0
      sonar-plugin-api/src/main/java/org/sonar/api/Property.java
  26. 4
    2
      sonar-plugin-api/src/main/java/org/sonar/api/web/WidgetProperty.java
  27. 8
    1
      sonar-plugin-api/src/main/java/org/sonar/api/web/WidgetPropertyType.java
  28. 0
    5
      sonar-server/src/main/webapp/WEB-INF/app/controllers/application_controller.rb
  29. 58
    24
      sonar-server/src/main/webapp/WEB-INF/app/controllers/bulk_deletion_controller.rb
  30. 6
    2
      sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb
  31. 81
    27
      sonar-server/src/main/webapp/WEB-INF/app/helpers/properties_helper.rb
  32. 2
    2
      sonar-server/src/main/webapp/WEB-INF/app/helpers/rules_configuration_helper.rb
  33. 1
    5
      sonar-server/src/main/webapp/WEB-INF/app/helpers/settings_helper.rb
  34. 12
    2
      sonar-server/src/main/webapp/WEB-INF/app/helpers/widget_properties_helper.rb
  35. 1
    1
      sonar-server/src/main/webapp/WEB-INF/app/models/property_type.rb
  36. 65
    0
      sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/ghosts.html.erb
  37. 3
    0
      sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/index.html.erb
  38. 10
    3
      sonar-server/src/main/webapp/WEB-INF/app/views/dashboard/_widget_properties.html.erb
  39. 1
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/project_reviews/index.html.erb
  40. 1
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb
  41. 1
    5
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb
  42. 5
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb
  43. 5
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb
  44. 4
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb
  45. 5
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb
  46. 5
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb
  47. 2
    6
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb
  48. 5
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb
  49. 5
    1
      sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb
  50. 1
    1
      sonar-server/src/main/webapp/WEB-INF/config/routes.rb

+ 366
- 366
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java View File

@@ -116,389 +116,389 @@ import org.sonar.plugins.core.widgets.reviews.UnplannedReviewsWidget;
import java.util.List;

@Properties({
@Property(
key = CoreProperties.SERVER_BASE_URL,
defaultValue = CoreProperties.SERVER_BASE_URL_DEFAULT_VALUE,
name = "Server base URL",
description = "HTTP URL of this Sonar server, such as <i>http://yourhost.yourdomain/sonar</i>. This value is used i.e. to create links in emails.",
project = false,
global = true,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_HOME_PAGE,
defaultValue = "",
name = "Project Home Page",
description = "HTTP URL of the home page of the project.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_CI,
defaultValue = "",
name = "CI server",
description = "HTTP URL of the continuous integration server.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_ISSUE_TRACKER,
defaultValue = "",
name = "Issue Tracker",
description = "HTTP URL of the issue tracker.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_SOURCES,
defaultValue = "",
name = "SCM server",
description = "HTTP URL of the server which hosts the sources of the project.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_SOURCES_DEV,
defaultValue = "",
name = "SCM connection for developers",
description = "HTTP URL used by developers to connect to the SCM server for the project.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.PROJECT_LANGUAGE_PROPERTY,
defaultValue = Java.KEY,
name = "Default language",
description = "Default language of the source code to analyse.",
project = false,
global = true,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.GLOBAL_EXCLUSIONS_PROPERTY,
name = "Global source exclusions",
description = "Exclude sources from code analysis. Applies to every project. Cannot be overriden. Changes will be applied during next code analysis.",
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.GLOBAL_TEST_EXCLUSIONS_PROPERTY,
name = "Global test exclusions",
description = "Exclude tests from code analysis. Applies to every project. Cannot be overriden. Changes will be applied during next code analysis.",
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS,
defaultValue = CoreProperties.GLOBAL_TEST_EXCLUSIONS_DEFAULT),
@Property(
key = CoreProperties.PROJECT_EXCLUSIONS_PROPERTY,
name = "Exclusions",
description = "Exclude sources from code analysis. Changes will be applied during next code analysis.",
project = true,
global = true,
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.PROJECT_TEST_EXCLUSIONS_PROPERTY,
name = "Test Exclusions",
description = "Exclude tests from code analysis. Changes will be applied during next code analysis.",
project = true,
global = true,
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.CORE_IMPORT_SOURCES_PROPERTY,
defaultValue = "" + CoreProperties.CORE_IMPORT_SOURCES_DEFAULT_VALUE,
name = "Import sources",
description = "Set to false if sources should not be displayed, e.g. for security reasons.",
project = true,
module = true,
global = true,
category = CoreProperties.CATEGORY_SECURITY,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_TENDENCY_DEPTH_PROPERTY,
defaultValue = "" + CoreProperties.CORE_TENDENCY_DEPTH_DEFAULT_VALUE,
name = "Tendency period",
description = TendencyDecorator.PROP_DAYS_DESCRIPTION,
project = false,
global = true,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS,
type = PropertyType.INTEGER),
@Property(
key = CoreProperties.SKIP_TENDENCIES_PROPERTY,
defaultValue = "" + CoreProperties.SKIP_TENDENCIES_DEFAULT_VALUE,
name = "Skip tendencies",
description = "Skip calculation of measure tendencies",
project = true,
module = false,
global = true,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_SKIPPED_MODULES_PROPERTY,
name = "Exclude modules",
description = "Maven artifact ids of modules to exclude.",
project = true,
global = false,
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY,
defaultValue = "" + CoreProperties.CORE_FORCE_AUTHENTICATION_DEFAULT_VALUE,
name = "Force user authentication",
description = "Forcing user authentication stops un-logged users to access Sonar.",
project = false,
global = true,
category = CoreProperties.CATEGORY_SECURITY,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_ALLOW_USERS_TO_SIGNUP_PROPERTY,
defaultValue = "" + CoreProperties.CORE_ALLOW_USERS_TO_SIGNUP_DEAULT_VALUE,
name = "Allow users to sign up online",
description = "Users can sign up online.",
project = false,
global = true,
category = CoreProperties.CATEGORY_SECURITY,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_DEFAULT_GROUP,
defaultValue = CoreProperties.CORE_DEFAULT_GROUP_DEFAULT_VALUE,
name = "Default user group",
description = "Any new users will automatically join this group.",
project = false,
global = true,
category = CoreProperties.CATEGORY_SECURITY),
@Property(
key = CoreProperties.CORE_VIOLATION_LOCALE_PROPERTY,
defaultValue = "en",
name = "Locale used for violation messages",
description = "Locale to be used when generating violation messages. It's up to each rule engine to support this global internationalization property",
project = true,
global = true,
category = CoreProperties.CATEGORY_L10N),
@Property(
key = "sonar.timemachine.period1",
name = "Period 1",
description = "Period used to compare measures and track new violations. Values are : <ul class='bullet'><li>Number of days before " +
"analysis, for example 5.</li><li>A custom date. Format is yyyy-MM-dd, for example 2010-12-25</li><li>'previous_analysis' to " +
"compare to previous analysis</li><li>'previous_version' to compare to the previous version in the project history</li></ul>" +
"<p>When specifying a number of days or a date, the snapshot selected for comparison is " +
" the first one available inside the corresponding time range. </p>" +
"<p>Changing this property only takes effect after subsequent project inspections.<p/>",
project = false,
global = true,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_1,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period2",
name = "Period 2",
description = "See the property 'Period 1'",
project = false,
global = true,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_2,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period3",
name = "Period 3",
description = "See the property 'Period 1'",
project = false,
global = true,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_3,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period4",
name = "Period 4",
description = "Period used to compare measures and track new violations. This property is specific to the project. Values are : " +
"<ul class='bullet'><li>Number of days before analysis, for example 5.</li><li>A custom date. Format is yyyy-MM-dd, " +
"for example 2010-12-25</li><li>'previous_analysis' to compare to previous analysis</li>" +
"<li>'previous_version' to compare to the previous version in the project history</li><li>A version, for example 1.2</li></ul>" +
"<p>When specifying a number of days or a date, the snapshot selected for comparison is the first one available inside the corresponding time range. </p>" +
"<p>Changing this property only takes effect after subsequent project inspections.<p/>",
project = true,
global = false,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_4,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period5",
name = "Period 5",
description = "See the property 'Period 4'",
project = true,
global = false,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_5,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = CoreProperties.DRY_RUN,
defaultValue = "false",
name = "Dry Run",
type = PropertyType.BOOLEAN,
global = false, project = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.DRY_RUN_INCLUDE_PLUGINS,
name = "Plugins accepted for dry run",
defaultValue = CoreProperties.DRY_RUN_INCLUDE_PLUGINS_DEFAULT_VALUE,
global = true, project = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.DRY_RUN_EXCLUDE_PLUGINS,
name = "Plugins excluded for dry run",
global = true, project = false,
defaultValue = CoreProperties.DRY_RUN_EXCLUDE_PLUGINS_DEFAULT_VALUE,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = "sonar.dryRun.export.path",
defaultValue = "dryRun.json",
name = "Dry Run Results Export File",
type = PropertyType.STRING,
global = false, project = false),
@Property(
key = CoreProperties.SERVER_BASE_URL,
defaultValue = CoreProperties.SERVER_BASE_URL_DEFAULT_VALUE,
name = "Server base URL",
description = "HTTP URL of this Sonar server, such as <i>http://yourhost.yourdomain/sonar</i>. This value is used i.e. to create links in emails.",
project = false,
global = true,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_HOME_PAGE,
defaultValue = "",
name = "Project Home Page",
description = "HTTP URL of the home page of the project.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_CI,
defaultValue = "",
name = "CI server",
description = "HTTP URL of the continuous integration server.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_ISSUE_TRACKER,
defaultValue = "",
name = "Issue Tracker",
description = "HTTP URL of the issue tracker.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_SOURCES,
defaultValue = "",
name = "SCM server",
description = "HTTP URL of the server which hosts the sources of the project.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.LINKS_SOURCES_DEV,
defaultValue = "",
name = "SCM connection for developers",
description = "HTTP URL used by developers to connect to the SCM server for the project.",
project = false,
global = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.PROJECT_LANGUAGE_PROPERTY,
defaultValue = Java.KEY,
name = "Default language",
description = "Default language of the source code to analyse.",
project = false,
global = true,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.GLOBAL_EXCLUSIONS_PROPERTY,
name = "Global source exclusions",
description = "Exclude sources from code analysis. Applies to every project. Cannot be overriden. Changes will be applied during next code analysis.",
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.GLOBAL_TEST_EXCLUSIONS_PROPERTY,
name = "Global test exclusions",
description = "Exclude tests from code analysis. Applies to every project. Cannot be overriden. Changes will be applied during next code analysis.",
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS,
defaultValue = CoreProperties.GLOBAL_TEST_EXCLUSIONS_DEFAULT),
@Property(
key = CoreProperties.PROJECT_EXCLUSIONS_PROPERTY,
name = "Exclusions",
description = "Exclude sources from code analysis. Changes will be applied during next code analysis.",
project = true,
global = true,
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.PROJECT_TEST_EXCLUSIONS_PROPERTY,
name = "Test Exclusions",
description = "Exclude tests from code analysis. Changes will be applied during next code analysis.",
project = true,
global = true,
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.CORE_IMPORT_SOURCES_PROPERTY,
defaultValue = "" + CoreProperties.CORE_IMPORT_SOURCES_DEFAULT_VALUE,
name = "Import sources",
description = "Set to false if sources should not be displayed, e.g. for security reasons.",
project = true,
module = true,
global = true,
category = CoreProperties.CATEGORY_SECURITY,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_TENDENCY_DEPTH_PROPERTY,
defaultValue = "" + CoreProperties.CORE_TENDENCY_DEPTH_DEFAULT_VALUE,
name = "Tendency period",
description = TendencyDecorator.PROP_DAYS_DESCRIPTION,
project = false,
global = true,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS,
type = PropertyType.INTEGER),
@Property(
key = CoreProperties.SKIP_TENDENCIES_PROPERTY,
defaultValue = "" + CoreProperties.SKIP_TENDENCIES_DEFAULT_VALUE,
name = "Skip tendencies",
description = "Skip calculation of measure tendencies",
project = true,
module = false,
global = true,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_SKIPPED_MODULES_PROPERTY,
name = "Exclude modules",
description = "Maven artifact ids of modules to exclude.",
project = true,
global = false,
multiValues = true,
category = CoreProperties.CATEGORY_EXCLUSIONS),
@Property(
key = CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY,
defaultValue = "" + CoreProperties.CORE_FORCE_AUTHENTICATION_DEFAULT_VALUE,
name = "Force user authentication",
description = "Forcing user authentication stops un-logged users to access Sonar.",
project = false,
global = true,
category = CoreProperties.CATEGORY_SECURITY,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_ALLOW_USERS_TO_SIGNUP_PROPERTY,
defaultValue = "" + CoreProperties.CORE_ALLOW_USERS_TO_SIGNUP_DEAULT_VALUE,
name = "Allow users to sign up online",
description = "Users can sign up online.",
project = false,
global = true,
category = CoreProperties.CATEGORY_SECURITY,
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_DEFAULT_GROUP,
defaultValue = CoreProperties.CORE_DEFAULT_GROUP_DEFAULT_VALUE,
name = "Default user group",
description = "Any new users will automatically join this group.",
project = false,
global = true,
category = CoreProperties.CATEGORY_SECURITY),
@Property(
key = CoreProperties.CORE_VIOLATION_LOCALE_PROPERTY,
defaultValue = "en",
name = "Locale used for violation messages",
description = "Locale to be used when generating violation messages. It's up to each rule engine to support this global internationalization property",
project = true,
global = true,
category = CoreProperties.CATEGORY_L10N),
@Property(
key = "sonar.timemachine.period1",
name = "Period 1",
description = "Period used to compare measures and track new violations. Values are : <ul class='bullet'><li>Number of days before " +
"analysis, for example 5.</li><li>A custom date. Format is yyyy-MM-dd, for example 2010-12-25</li><li>'previous_analysis' to " +
"compare to previous analysis</li><li>'previous_version' to compare to the previous version in the project history</li></ul>" +
"<p>When specifying a number of days or a date, the snapshot selected for comparison is " +
" the first one available inside the corresponding time range. </p>" +
"<p>Changing this property only takes effect after subsequent project inspections.<p/>",
project = false,
global = true,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_1,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period2",
name = "Period 2",
description = "See the property 'Period 1'",
project = false,
global = true,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_2,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period3",
name = "Period 3",
description = "See the property 'Period 1'",
project = false,
global = true,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_3,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period4",
name = "Period 4",
description = "Period used to compare measures and track new violations. This property is specific to the project. Values are : " +
"<ul class='bullet'><li>Number of days before analysis, for example 5.</li><li>A custom date. Format is yyyy-MM-dd, " +
"for example 2010-12-25</li><li>'previous_analysis' to compare to previous analysis</li>" +
"<li>'previous_version' to compare to the previous version in the project history</li><li>A version, for example 1.2</li></ul>" +
"<p>When specifying a number of days or a date, the snapshot selected for comparison is the first one available inside the corresponding time range. </p>" +
"<p>Changing this property only takes effect after subsequent project inspections.<p/>",
project = true,
global = false,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_4,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = "sonar.timemachine.period5",
name = "Period 5",
description = "See the property 'Period 4'",
project = true,
global = false,
defaultValue = CoreProperties.TIMEMACHINE_DEFAULT_PERIOD_5,
category = CoreProperties.CATEGORY_DIFFERENTIAL_VIEWS),
@Property(
key = CoreProperties.DRY_RUN,
defaultValue = "false",
name = "Dry Run",
type = PropertyType.BOOLEAN,
global = false, project = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.DRY_RUN_INCLUDE_PLUGINS,
name = "Plugins accepted for dry run",
defaultValue = CoreProperties.DRY_RUN_INCLUDE_PLUGINS_DEFAULT_VALUE,
global = true, project = false,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = CoreProperties.DRY_RUN_EXCLUDE_PLUGINS,
name = "Plugins excluded for dry run",
global = true, project = false,
defaultValue = CoreProperties.DRY_RUN_EXCLUDE_PLUGINS_DEFAULT_VALUE,
category = CoreProperties.CATEGORY_GENERAL),
@Property(
key = "sonar.dryRun.export.path",
defaultValue = "dryRun.json",
name = "Dry Run Results Export File",
type = PropertyType.STRING,
global = false, project = false),

// SERVER-SIDE TECHNICAL PROPERTIES
// SERVER-SIDE TECHNICAL PROPERTIES

@Property(
key = "sonar.security.realm",
name = "Security Realm",
project = false,
global = false
),
@Property(
key = "sonar.security.savePassword",
name = "Save external password",
project = false,
global = false
),
@Property(
key = "sonar.authenticator.downcase",
name = "Downcase login",
description = "Downcase login during user authentication, typically for Active Directory",
project = false,
global = false,
defaultValue = "false",
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_AUTHENTICATOR_CREATE_USERS,
name = "Create user accounts",
description = "Create accounts when authenticating users via an external system",
project = false,
global = false,
defaultValue = "true",
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_AUTHENTICATOR_IGNORE_STARTUP_FAILURE,
name = "Ignore failures during authenticator startup",
defaultValue = "false",
project = false,
global = false,
type = PropertyType.BOOLEAN)
@Property(
key = "sonar.security.realm",
name = "Security Realm",
project = false,
global = false
),
@Property(
key = "sonar.security.savePassword",
name = "Save external password",
project = false,
global = false
),
@Property(
key = "sonar.authenticator.downcase",
name = "Downcase login",
description = "Downcase login during user authentication, typically for Active Directory",
project = false,
global = false,
defaultValue = "false",
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_AUTHENTICATOR_CREATE_USERS,
name = "Create user accounts",
description = "Create accounts when authenticating users via an external system",
project = false,
global = false,
defaultValue = "true",
type = PropertyType.BOOLEAN),
@Property(
key = CoreProperties.CORE_AUTHENTICATOR_IGNORE_STARTUP_FAILURE,
name = "Ignore failures during authenticator startup",
defaultValue = "false",
project = false,
global = false,
type = PropertyType.BOOLEAN)
})
public final class CorePlugin extends SonarPlugin {

@SuppressWarnings("unchecked")
public List getExtensions() {
return ImmutableList.of(
DefaultResourceTypes.class,
UserManagedMetrics.class,
ProjectFileSystemLogger.class,
Periods.class,
DefaultResourceTypes.class,
UserManagedMetrics.class,
ProjectFileSystemLogger.class,
Periods.class,

// maven
MavenInitializer.class,
// maven
MavenInitializer.class,

// languages
Java.class,
// languages
Java.class,

// pages
Lcom4Viewer.class,
TestsViewer.class,
// pages
Lcom4Viewer.class,
TestsViewer.class,

// measure filters
ProjectFilter.class,
MyFavouritesFilter.class,
// measure filters
ProjectFilter.class,
MyFavouritesFilter.class,

// widgets
AlertsWidget.class,
CoverageWidget.class,
ItCoverageWidget.class,
CommentsDuplicationsWidget.class,
DescriptionWidget.class,
ComplexityWidget.class,
RulesWidget.class,
SizeWidget.class,
EventsWidget.class,
CustomMeasuresWidget.class,
TimelineWidget.class,
TimeMachineWidget.class,
HotspotMetricWidget.class,
HotspotMostViolatedResourcesWidget.class,
HotspotMostViolatedRulesWidget.class,
MyReviewsWidget.class,
ProjectReviewsWidget.class,
FalsePositiveReviewsWidget.class,
ReviewsPerDeveloperWidget.class,
PlannedReviewsWidget.class,
UnplannedReviewsWidget.class,
ActionPlansWidget.class,
ReviewsMetricsWidget.class,
TreemapWidget.class,
MeasureFilterListWidget.class,
MeasureFilterTreemapWidget.class,
WelcomeWidget.class,
// widgets
AlertsWidget.class,
CoverageWidget.class,
ItCoverageWidget.class,
CommentsDuplicationsWidget.class,
DescriptionWidget.class,
ComplexityWidget.class,
RulesWidget.class,
SizeWidget.class,
EventsWidget.class,
CustomMeasuresWidget.class,
TimelineWidget.class,
TimeMachineWidget.class,
HotspotMetricWidget.class,
HotspotMostViolatedResourcesWidget.class,
HotspotMostViolatedRulesWidget.class,
MyReviewsWidget.class,
ProjectReviewsWidget.class,
FalsePositiveReviewsWidget.class,
ReviewsPerDeveloperWidget.class,
PlannedReviewsWidget.class,
UnplannedReviewsWidget.class,
ActionPlansWidget.class,
ReviewsMetricsWidget.class,
TreemapWidget.class,
MeasureFilterListWidget.class,
MeasureFilterTreemapWidget.class,
WelcomeWidget.class,

// dashboards
ProjectDefaultDashboard.class,
ProjectHotspotDashboard.class,
ProjectReviewsDashboard.class,
ProjectTimeMachineDashboard.class,
GlobalDefaultDashboard.class,
// dashboards
ProjectDefaultDashboard.class,
ProjectHotspotDashboard.class,
ProjectReviewsDashboard.class,
ProjectTimeMachineDashboard.class,
GlobalDefaultDashboard.class,

// chart
XradarChart.class,
DistributionBarChart.class,
DistributionAreaChart.class,
// chart
XradarChart.class,
DistributionBarChart.class,
DistributionAreaChart.class,

// colorizers
JavaColorizerFormat.class,
// colorizers
JavaColorizerFormat.class,

// batch
ProfileSensor.class,
ProfileEventsSensor.class,
ProjectLinksSensor.class,
UnitTestDecorator.class,
VersionEventsSensor.class,
CheckAlertThresholds.class,
GenerateAlertEvents.class,
ViolationsDecorator.class,
WeightedViolationsDecorator.class,
ViolationsDensityDecorator.class,
LineCoverageDecorator.class,
CoverageDecorator.class,
BranchCoverageDecorator.class,
ItLineCoverageDecorator.class,
ItCoverageDecorator.class,
ItBranchCoverageDecorator.class,
OverallLineCoverageDecorator.class,
OverallCoverageDecorator.class,
OverallBranchCoverageDecorator.class,
ApplyProjectRolesDecorator.class,
ExcludedResourceFilter.class,
CommentDensityDecorator.class,
NoSonarFilter.class,
DirectoriesDecorator.class,
FilesDecorator.class,
ReviewNotifications.class,
ReviewWorkflowDecorator.class,
ReferenceAnalysis.class,
ManualMeasureDecorator.class,
ManualViolationInjector.class,
ViolationSeverityUpdater.class,
IndexProjectPostJob.class,
ReviewsMeasuresDecorator.class,
// batch
ProfileSensor.class,
ProfileEventsSensor.class,
ProjectLinksSensor.class,
UnitTestDecorator.class,
VersionEventsSensor.class,
CheckAlertThresholds.class,
GenerateAlertEvents.class,
ViolationsDecorator.class,
WeightedViolationsDecorator.class,
ViolationsDensityDecorator.class,
LineCoverageDecorator.class,
CoverageDecorator.class,
BranchCoverageDecorator.class,
ItLineCoverageDecorator.class,
ItCoverageDecorator.class,
ItBranchCoverageDecorator.class,
OverallLineCoverageDecorator.class,
OverallCoverageDecorator.class,
OverallBranchCoverageDecorator.class,
ApplyProjectRolesDecorator.class,
ExcludedResourceFilter.class,
CommentDensityDecorator.class,
NoSonarFilter.class,
DirectoriesDecorator.class,
FilesDecorator.class,
ReviewNotifications.class,
ReviewWorkflowDecorator.class,
ReferenceAnalysis.class,
ManualMeasureDecorator.class,
ManualViolationInjector.class,
ViolationSeverityUpdater.class,
IndexProjectPostJob.class,
ReviewsMeasuresDecorator.class,

// time machine
TendencyDecorator.class,
VariationDecorator.class,
ViolationTrackingDecorator.class,
ViolationPersisterDecorator.class,
NewViolationsDecorator.class,
TimeMachineConfigurationPersister.class,
NewCoverageFileAnalyzer.class,
NewItCoverageFileAnalyzer.class,
NewOverallCoverageFileAnalyzer.class,
NewCoverageAggregator.class);
// time machine
TendencyDecorator.class,
VariationDecorator.class,
ViolationTrackingDecorator.class,
ViolationPersisterDecorator.class,
NewViolationsDecorator.class,
TimeMachineConfigurationPersister.class,
NewCoverageFileAnalyzer.class,
NewItCoverageFileAnalyzer.class,
NewOverallCoverageFileAnalyzer.class,
NewCoverageAggregator.class);
}
}

+ 43
- 8
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/GenerateAlertEvents.java View File

@@ -19,10 +19,18 @@
*/
package org.sonar.plugins.core.sensors;

import org.sonar.api.batch.*;
import org.sonar.api.batch.Decorator;
import org.sonar.api.batch.DecoratorContext;
import org.sonar.api.batch.DependsUpon;
import org.sonar.api.batch.Event;
import org.sonar.api.batch.TimeMachine;
import org.sonar.api.batch.TimeMachineQuery;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.Metric;
import org.sonar.api.measures.Metric.Level;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Resource;
@@ -34,17 +42,18 @@ public class GenerateAlertEvents implements Decorator {

private final RulesProfile profile;
private final TimeMachine timeMachine;
private NotificationManager notificationManager;

public GenerateAlertEvents(RulesProfile profile, TimeMachine timeMachine) {
public GenerateAlertEvents(RulesProfile profile, TimeMachine timeMachine, NotificationManager notificationManager) {
this.profile = profile;
this.timeMachine = timeMachine;
this.notificationManager = notificationManager;
}

public boolean shouldExecuteOnProject(Project project) {
return profile != null && profile.getAlerts() != null && profile.getAlerts().size() > 0;
}


@DependsUpon
public Metric dependsUponAlertStatus() {
return CoreMetrics.ALERT_STATUS;
@@ -63,15 +72,41 @@ public class GenerateAlertEvents implements Decorator {
List<Measure> measures = timeMachine.getMeasures(query);

Measure pastStatus = (measures != null && measures.size() == 1 ? measures.get(0) : null);
if (pastStatus != null && pastStatus.getDataAsLevel() != currentStatus.getDataAsLevel()) {
createEvent(context, getName(pastStatus, currentStatus), currentStatus.getAlertText());

} else if (pastStatus == null && currentStatus.getDataAsLevel() != Metric.Level.OK) {
createEvent(context, getName(currentStatus), currentStatus.getAlertText());
String alertText = currentStatus.getAlertText();
Level alertLevel = currentStatus.getDataAsLevel();
String alertName = null;
boolean isNewAlert = true;
if (pastStatus != null && pastStatus.getDataAsLevel() != alertLevel) {
// The alert status has changed
alertName = getName(pastStatus, currentStatus);
if (pastStatus.getDataAsLevel() != Metric.Level.OK) {
// There was already a Orange/Red alert, so this is no new alert: it has just changed
isNewAlert = false;
}
createEvent(context, alertName, alertText);
notifyUsers(resource, alertName, alertText, alertLevel, isNewAlert);

} else if (pastStatus == null && alertLevel != Metric.Level.OK) {
// There were no defined alerts before, so this one is a new one
alertName = getName(currentStatus);
createEvent(context, alertName, alertText);
notifyUsers(resource, alertName, alertText, alertLevel, isNewAlert);
}

}

protected void notifyUsers(Resource<?> resource, String alertName, String alertText, Level alertLevel, boolean isNewAlert) {
Notification notification = new Notification("alerts")
.setFieldValue("projectName", resource.getLongName())
.setFieldValue("projectKey", resource.getKey())
.setFieldValue("projectId", String.valueOf(resource.getId()))
.setFieldValue("alertName", alertName)
.setFieldValue("alertText", alertText)
.setFieldValue("alertLevel", alertLevel.toString())
.setFieldValue("isNewAlert", Boolean.toString(isNewAlert));
notificationManager.scheduleForSending(notification);
}

private boolean shouldDecorateResource(Resource resource) {
return ResourceUtils.isRootProject(resource);
}

+ 5
- 6
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/HotspotMostViolatedRulesWidget.java View File

@@ -26,13 +26,12 @@ import org.sonar.api.web.WidgetPropertyType;

@WidgetCategory("Hotspots")
@WidgetProperties(
{
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5"),
@WidgetProperty(key = "defaultSeverity", type = WidgetPropertyType.STRING, description = "Values: BLOCKER, CRITICAL, MAJOR, MINOR, INFO")
}
)
{
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5"),
@WidgetProperty(key = "defaultSeverity", type = WidgetPropertyType.STRING)
})
public class HotspotMostViolatedRulesWidget extends CoreWidget {
public HotspotMostViolatedRulesWidget() {
super("hotspot_most_violated_rules", "Most violated rules", "/org/sonar/plugins/core/widgets/hotspots/hotspot_most_violated_rules.html.erb");
}
}
}

+ 6
- 6
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimelineWidget.java View File

@@ -26,12 +26,12 @@ import org.sonar.api.web.WidgetPropertyType;

@WidgetCategory("History")
@WidgetProperties({
@WidgetProperty(key = "chartTitle", type = WidgetPropertyType.STRING),
@WidgetProperty(key = "metric1", type = WidgetPropertyType.METRIC, defaultValue = "ncloc", options = {WidgetConstants.FILTER_OUT_NEW_METRICS}),
@WidgetProperty(key = "metric2", type = WidgetPropertyType.METRIC, options = {WidgetConstants.FILTER_OUT_NEW_METRICS}),
@WidgetProperty(key = "metric3", type = WidgetPropertyType.METRIC, options = {WidgetConstants.FILTER_OUT_NEW_METRICS}),
@WidgetProperty(key = "hideEvents", type = WidgetPropertyType.BOOLEAN),
@WidgetProperty(key = "chartHeight", type = WidgetPropertyType.INTEGER, defaultValue = "80")
@WidgetProperty(key = "chartTitle", type = WidgetPropertyType.STRING),
@WidgetProperty(key = "metric1", type = WidgetPropertyType.METRIC, defaultValue = "ncloc", options = {WidgetConstants.FILTER_OUT_NEW_METRICS}),
@WidgetProperty(key = "metric2", type = WidgetPropertyType.METRIC, options = {WidgetConstants.FILTER_OUT_NEW_METRICS}),
@WidgetProperty(key = "metric3", type = WidgetPropertyType.METRIC, options = {WidgetConstants.FILTER_OUT_NEW_METRICS}),
@WidgetProperty(key = "hideEvents", type = WidgetPropertyType.BOOLEAN),
@WidgetProperty(key = "chartHeight", type = WidgetPropertyType.INTEGER, defaultValue = "80")
})
public class TimelineWidget extends CoreWidget {
public TimelineWidget() {

+ 4
- 4
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java View File

@@ -25,13 +25,13 @@ import org.sonar.api.web.WidgetProperty;
import org.sonar.api.web.WidgetPropertyType;

@WidgetProperties({
@WidgetProperty(key = "sizeMetric", type = WidgetPropertyType.METRIC, defaultValue = CoreMetrics.NCLOC_KEY, description = "Metric used for square size"),
@WidgetProperty(key = "colorMetric", type = WidgetPropertyType.METRIC, defaultValue = CoreMetrics.VIOLATIONS_DENSITY_KEY, description = "Metric used for square color"),
@WidgetProperty(key = "heightInPercents", type = WidgetPropertyType.INTEGER, optional = true, defaultValue = "55", description = "Height in percents of width")
@WidgetProperty(key = "sizeMetric", type = WidgetPropertyType.METRIC, defaultValue = CoreMetrics.NCLOC_KEY),
@WidgetProperty(key = "colorMetric", type = WidgetPropertyType.METRIC, defaultValue = CoreMetrics.VIOLATIONS_DENSITY_KEY),
@WidgetProperty(key = "heightInPercents", type = WidgetPropertyType.INTEGER, optional = true, defaultValue = "55")
})
public class TreemapWidget extends CoreWidget {
public TreemapWidget() {
// do not use the id "treemap" to avoid conflict with the same CSS class
super("treemap-widget", "Treemap of Components", "/org/sonar/plugins/core/widgets/treemap.html.erb");
}
}
}

+ 2
- 3
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/FalsePositiveReviewsWidget.java View File

@@ -27,11 +27,10 @@ import org.sonar.plugins.core.widgets.CoreWidget;

@WidgetCategory({"Reviews"})
@WidgetProperties({
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5",
description = "Maximum number of reviews displayed at the same time.")
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5")
})
public class FalsePositiveReviewsWidget extends CoreWidget {
public FalsePositiveReviewsWidget() {
super("false_positive_reviews", "False positive open reviews", "/org/sonar/plugins/core/widgets/reviews/false_positive_reviews.html.erb");
}
}
}

+ 2
- 3
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/MyReviewsWidget.java View File

@@ -27,11 +27,10 @@ import org.sonar.plugins.core.widgets.CoreWidget;

@WidgetCategory({"Reviews"})
@WidgetProperties({
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5",
description = "Maximum number of reviews displayed at the same time.")
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5")
})
public class MyReviewsWidget extends CoreWidget {
public MyReviewsWidget() {
super("my_reviews", "My active reviews", "/org/sonar/plugins/core/widgets/reviews/my_reviews.html.erb");
}
}
}

+ 2
- 3
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/PlannedReviewsWidget.java View File

@@ -27,11 +27,10 @@ import org.sonar.plugins.core.widgets.CoreWidget;

@WidgetCategory({"Action plans", "Reviews"})
@WidgetProperties({
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5",
description = "Maximum number of reviews displayed at the same time.")
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5")
})
public class PlannedReviewsWidget extends CoreWidget {
public PlannedReviewsWidget() {
super("planned_reviews", "Planned reviews", "/org/sonar/plugins/core/widgets/reviews/planned_reviews.html.erb");
}
}
}

+ 2
- 3
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ProjectReviewsWidget.java View File

@@ -27,11 +27,10 @@ import org.sonar.plugins.core.widgets.CoreWidget;

@WidgetCategory({"Reviews"})
@WidgetProperties({
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5",
description = "Maximum number of reviews displayed at the same time.")
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5")
})
public class ProjectReviewsWidget extends CoreWidget {
public ProjectReviewsWidget() {
super("project_reviews", "Project active reviews", "/org/sonar/plugins/core/widgets/reviews/project_reviews.html.erb");
}
}
}

+ 2
- 3
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/UnplannedReviewsWidget.java View File

@@ -27,11 +27,10 @@ import org.sonar.plugins.core.widgets.CoreWidget;

@WidgetCategory({"Action plans", "Reviews"})
@WidgetProperties({
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5",
description = "Maximum number of reviews displayed at the same time.")
@WidgetProperty(key = "numberOfLines", type = WidgetPropertyType.INTEGER, defaultValue = "5")
})
public class UnplannedReviewsWidget extends CoreWidget {
public UnplannedReviewsWidget() {
super("unplanned_reviews", "Unplanned reviews", "/org/sonar/plugins/core/widgets/reviews/unplanned_reviews.html.erb");
}
}
}

+ 76
- 9
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties View File

@@ -835,6 +835,16 @@ widget.complexity.total=Total

widget.custom_measures.name=Custom Measures
widget.custom_measures.description=Display a list of selected measures.
widget.custom_measures.property.metric1.name=Metric 1
widget.custom_measures.property.metric2.name=Metric 2
widget.custom_measures.property.metric3.name=Metric 3
widget.custom_measures.property.metric4.name=Metric 4
widget.custom_measures.property.metric5.name=Metric 5
widget.custom_measures.property.metric6.name=Metric 6
widget.custom_measures.property.metric7.name=Metric 7
widget.custom_measures.property.metric8.name=Metric 8
widget.custom_measures.property.metric9.name=Metric 9
widget.custom_measures.property.metric10.name=Metric 10

widget.description.name=Description
widget.description.description=Displays general project information
@@ -880,9 +890,28 @@ widget.size.projects.suffix=\ projects
widget.timeline.name=Timeline
widget.timeline.description=Displays up to 3 metrics on a history chart.
widget.timeline.timeline_not_displayed=No history
widget.timeline.property.chartTitle.name=Chart title
widget.timeline.property.metric1.name=Metric 1
widget.timeline.property.metric2.name=Metric 2
widget.timeline.property.metric3.name=Metric 3
widget.timeline.property.hideEvents.name=Hide events
widget.timeline.property.chartHeight.name=Chart height

widget.time_machine.name=History Table
widget.time_machine.description=Displays up to 10 metrics in a table, showing their value for a specified number of past snapshots.
widget.time_machine.property.title.name=Title
widget.time_machine.property.numberOfColumns.name=Number of columns
widget.time_machine.property.displaySparkLine.name=Display spark line
widget.time_machine.property.metric1.name=Metric 1
widget.time_machine.property.metric2.name=Metric 2
widget.time_machine.property.metric3.name=Metric 3
widget.time_machine.property.metric4.name=Metric 4
widget.time_machine.property.metric5.name=Metric 5
widget.time_machine.property.metric6.name=Metric 6
widget.time_machine.property.metric7.name=Metric 7
widget.time_machine.property.metric8.name=Metric 8
widget.time_machine.property.metric9.name=Metric 9
widget.time_machine.property.metric10.name=Metric 10

widget.file_design.name=File design
widget.file_design.description=Reports on files dependency cycles and tangle index.
@@ -902,26 +931,39 @@ widget.hotspot_metric.name=Metric hotspot
widget.hotspot_metric.description=Shows the files that have the worst result for a specific metric.
widget.hotspot_metric.more=More
widget.hotspot_metric.hotspots_by_x=Hotspots by {0}
widget.hotspot_metric.property.title.name=Title
widget.hotspot_metric.property.metric.name=Metric
widget.hotspot_metric.property.numberOfLines.name=Number of lines

widget.hotspot_most_violated_rules.name=Most violated rules
widget.hotspot_most_violated_rules.name_when_period=Most new violated rules
widget.hotspot_most_violated_rules.description=Shows the rules that are the most violated.
widget.hotspot_most_violated_rules.no_violation_for_severity=No result
widget.hotspot_most_violated_rules.any_severity=Any severity
widget.hotspot_most_violated_rules.property.numberOfLines.name=Number of lines
widget.hotspot_most_violated_rules.property.defaultSeverity.name=Default severity
widget.hotspot_most_violated_rules.property.defaultSeverity.desc=Values: BLOCKER, CRITICAL, MAJOR, MINOR, INFO

widget.hotspot_most_violated_resources.name=Most violated resources
widget.hotspot_most_violated_resources.description=Shows the resources that have the most violations.
widget.hotspot_most_violated_resources.no_resource=No result
widget.hotspot_most_violated_resources.property.numberOfLines.name=Number of lines

widget.my_reviews.name=My active reviews
widget.my_reviews.description=Shows open/reopened reviews assigned to the current user.
widget.my_reviews.no_review=No review.
widget.my_reviews.property.numberOfLines.name=Number of lines
widget.my_reviews.property.numberOfLines.desc=Maximum number of reviews displayed at the same time.

widget.project_reviews.name=Active reviews
widget.project_reviews.description=Shows all the open/reopened reviews.
widget.project_reviews.property.numberOfLines.name=Number of lines
widget.project_reviews.property.numberOfLines.desc=Maximum number of reviews displayed at the same time.

widget.false_positive_reviews.name=False positives
widget.false_positive_reviews.description=Shows all the false positives found on the project.
widget.false_positive_reviews.property.numberOfLines.name=Number of lines
widget.false_positive_reviews.property.numberOfLines.desc=Maximum number of reviews displayed at the same time.

widget.reviews_per_developer.name=Active reviews per developer
widget.reviews_per_developer.description=Shows the number of open/reopened reviews per developer.
@@ -929,6 +971,7 @@ widget.reviews_per_developer.not_assigned=Not assigned

widget.action_plans.name=Action plans
widget.action_plans.description=Shows all the open action plans of the project
widget.action_plans.property.showClosedReviews.name=Show Closed Reviews
widget.action_plans.title=Open action plans
widget.action_plans.no_action_plan=No action plan
widget.action_plans.x_open_reviews={0} open reviews
@@ -936,9 +979,13 @@ widget.action_plans.x_open_reviews={0} open reviews
widget.planned_reviews.name=Planned reviews
widget.planned_reviews.description=Shows all the planned reviews of the project, gathered by action plan
widget.planned_reviews.no_action_plan=No action plan
widget.planned_reviews.property.numberOfLines.name=Number of lines
widget.planned_reviews.property.numberOfLines.desc=Maximum number of reviews displayed at the same time.

widget.unplanned_reviews.name=Unplanned reviews
widget.unplanned_reviews.description=Shows all the reviews of the project that are not planned yet in an action plan
widget.unplanned_reviews.property.numberOfLines.name=Number of lines
widget.unplanned_reviews.property.numberOfLines.desc=Maximum number of reviews displayed at the same time.

widget.reviews_metrics.name=Review Activity
widget.reviews_metrics.description=Reports metrics about reviews
@@ -952,13 +999,12 @@ widget.reviews_metrics.added_unreviewed_violations=Added:

widget.treemap-widget.name=Treemap of components
widget.treemap-widget.description=Displays a treemap of all direct components of the selected resource

widget.image.name=Image
widget.image.description=Shows an image with a link

widget.filter.name=Filter
widget.filter.description=Shows a pre-configured filter
widget.filter.edit=Edit my filters
widget.treemap-widget.property.sizeMetric.name=Size metric
widget.treemap-widget.property.sizeMetric.description=Metric used for square size
widget.treemap-widget.property.colorMetric.name=Color metric
widget.treemap-widget.property.colorMetric.description=Metric used for square color
widget.treemap-widget.property.heightInPercents.name=Height
widget.treemap-widget.property.heightInPercents.description=Height in percents of width

widget.lcom4.name=LCOM4
widget.lcom4.description=Reports on LCOM4 average and distribution.
@@ -971,8 +1017,6 @@ widget.rfc.description=Reports on RFC average and distribution.
widget.rfc.title=Response for Class
widget.rfc.per_class.suffix=\ /class

widget.resource_id=project

widget.welcome.name=Welcome
widget.welcome.description=Welcome message used to provide links to the most valuable resources like documentation and support
widget.welcome.html=<h3 class="marginbottom5">Welcome to Sonar Dashboard</h3>\
@@ -987,9 +1031,26 @@ widget.welcome.html=<h3 class="marginbottom5">Welcome to Sonar Dashboard</h3>\

widget.measure_filter_list.name=Measure Filter as List
widget.measure_filter_list.description=Displays the result of a pre-configured measure filter as a list
widget.measure_filter_list.property.filter.name=Filter
widget.measure_filter_list.property.pageSize.name=Page size
widget.measure_filter_list.property.displayFilterDescription.name=Display filter description

widget.measure_filter_treemap.name=Measure Filter as Treemap
widget.measure_filter_treemap.description=Displays the result of pre-configured measure filter as a Treemap
widget.measure_filter_treemap.property.filter.name=Filter
widget.measure_filter_treemap.property.sizeMetric.name=Size metric
widget.measure_filter_treemap.property.colorMetric.name=Color metric
widget.measure_filter_treemap.property.heightInPercents.name=Height
widget.measure_filter_treemap.property.heightInPercents.description=Height in percents of width
widget.measure_filter_treemap.property.displayFilterDescription.name=Display filter description

# Below are labels used in widget edition pages
widget.image.name=Image
widget.image.description=Shows an image with a link
widget.filter.name=Filter
widget.filter.description=Shows a pre-configured filter
widget.filter.edit=Edit my filters
widget.resource_id=Project


#------------------------------------------------------------------------------
@@ -1434,6 +1495,7 @@ server_id_configuration.generation_error=Organisation and/or IP address are not
notification.channel.EmailNotificationChannel=Email
notification.dispatcher.ChangesInReviewAssignedToMeOrCreatedByMe=Changes in review assigned to me or created by me
notification.dispatcher.NewViolationsOnMyFavouriteProject=New violations on my favourite projects introduced during the first differential view period
notification.dispatcher.AlertsOnMyFavouriteProject=Alerts on my favourite projects


#------------------------------------------------------------------------------
@@ -1519,6 +1581,11 @@ bulk_deletion.deletion_manager.currently_deleting_x_out_of_x=Currently deleting
bulk_deletion.deletion_manager.deletion_completed=Resource deletion completed.
bulk_deletion.deletion_manager.however_failures_occurred=However, some failures occurred.
bulk_deletion.started_since_x=Started {0} ago
bulk_deletion.ghosts=Ghosts
bulk_deletion.ghosts.description=A ghost is the result of constantly failed attempts to analyse a project. In such a case, the project is not linked to any successful analysis, and therefore cannot be displayed in Sonar.<br/>When the user authentication is forced, leaving a ghost can even prevent further analyses of the corresponding project.
bulk_deletion.no_ghosts=There is currently no ghost.
bulk_deletion.following_ghosts_can_be_deleted=The following ghosts can be safely deleted:
bulk_deletion.delete_all_ghosts=Delete all ghosts


#------------------------------------------------------------------------------

+ 3
- 3
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/action_plans.html.erb View File

@@ -36,7 +36,7 @@
end
%>

<table class="width100 actionPlans">
<table class="width100 <%= show_closed_reviews ? 'actionPlans' : 'data' -%>">
<thead>
<tr>
<th colspan="<%= show_closed_reviews ? '3' : '4' %>"></th>
@@ -46,7 +46,7 @@
<%
open_action_plans.each do |plan|
%>
<tr>
<tr class="<%= show_closed_reviews ? '' : cycle("even", "odd", :name => "action_plan_" + widget.id.to_s) -%>">
<td class="nowrap <%= line_class -%>"><%= h(plan.name) -%></td>
<td class="nowrap small <%= line_class -%> <%= 'over-due' if plan.over_due? -%>" style="text-align: right; padding-left:10px"><%= plan.deadline ? plan.deadline.strftime("%d %b %Y") : ' ' -%></td>
@@ -62,7 +62,7 @@
else
open_reviews_size = plan.open_reviews.size
%>
<td class="right note <%= line_class -%>" style="padding-left:20px">
<td class="right <%= line_class -%>" style="padding-left:20px">
<% if open_reviews_size == 0 %>
<%= open_reviews_size -%>
<% else %>

+ 56
- 2
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/GenerateAlertEventsTest.java View File

@@ -28,21 +28,27 @@ import org.sonar.api.batch.TimeMachineQuery;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.Metric;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.profiles.Alert;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.File;
import org.sonar.api.resources.Project;
import org.sonar.api.test.ProjectTestBuilder;

import java.util.Arrays;
import java.util.Date;

import static org.fest.assertions.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@@ -51,6 +57,7 @@ public class GenerateAlertEventsTest {
private DecoratorContext context;
private RulesProfile profile;
private TimeMachine timeMachine;
private NotificationManager notificationManager;
private Project project;

@Before
@@ -58,12 +65,18 @@ public class GenerateAlertEventsTest {
context = mock(DecoratorContext.class);
timeMachine = mock(TimeMachine.class);
profile = mock(RulesProfile.class);
decorator = new GenerateAlertEvents(profile, timeMachine);
notificationManager = mock(NotificationManager.class);
decorator = new GenerateAlertEvents(profile, timeMachine, notificationManager);
project = new ProjectTestBuilder().build();
}

@Test
public void doNotDecorateIfNoThresholds() {
public void shouldDependUponAlertStatus() {
assertThat(decorator.dependsUponAlertStatus()).isEqualTo(CoreMetrics.ALERT_STATUS);
}

@Test
public void shouldNotDecorateIfNoThresholds() {
assertThat(decorator.shouldExecuteOnProject(project), is(false));
}

@@ -73,18 +86,30 @@ public class GenerateAlertEventsTest {
assertThat(decorator.shouldExecuteOnProject(project), is(true));
}

@Test
public void shouldNotDecorateIfNotRootProject() {
decorator.decorate(new File("Foo"), context);
verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
}

@Test
public void shouldCreateEventWhenNewErrorAlert() {
when(context.getMeasure(CoreMetrics.ALERT_STATUS)).thenReturn(newAlertStatus(Metric.Level.ERROR, "desc"));

decorator.decorate(project, context);

verify(context).createEvent(Metric.Level.ERROR.getColorName(), "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Red", "desc", "ERROR", "true");
}

@Test
public void shouldCreateEventWhenNewWarnAlert() {
when(context.getMeasure(CoreMetrics.ALERT_STATUS)).thenReturn(newAlertStatus(Metric.Level.WARN, "desc"));

decorator.decorate(project, context);

verify(context).createEvent(Metric.Level.WARN.getColorName(), "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Orange", "desc", "WARN", "true");
}

@Test
@@ -95,6 +120,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);

verify(context).createEvent("Red (was Orange)", "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Red (was Orange)", "desc", "ERROR", "false");
}

@Test
@@ -105,6 +131,18 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);

verify(context).createEvent("Green (was Red)", null, Event.CATEGORY_ALERT, null);
verifyNotificationSent("Green (was Red)", null, "OK", "false");
}

@Test
public void shouldCreateEventWhenOkToError() {
when(timeMachine.getMeasures(any(TimeMachineQuery.class))).thenReturn(Arrays.asList(newAlertStatus(Metric.Level.OK, null)));
when(context.getMeasure(CoreMetrics.ALERT_STATUS)).thenReturn(newAlertStatus(Metric.Level.ERROR, "desc"));

decorator.decorate(project, context);

verify(context).createEvent("Red (was Green)", "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Red (was Green)", "desc", "ERROR", "true");
}

@Test
@@ -115,6 +153,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);

verify(context).createEvent("Orange (was Red)", "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Orange (was Red)", "desc", "WARN", "false");
}

@Test
@@ -122,6 +161,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);

verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
verify(notificationManager, never()).scheduleForSending(any(Notification.class));
}

@Test
@@ -132,6 +172,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);

verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
verify(notificationManager, never()).scheduleForSending(any(Notification.class));
}

@Test
@@ -142,6 +183,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);

verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
verify(notificationManager, never()).scheduleForSending(any(Notification.class));
}

private Measure newAlertStatus(Metric.Level level, String label) {
@@ -150,4 +192,16 @@ public class GenerateAlertEventsTest {
measure.setAlertText(label);
return measure;
}

private void verifyNotificationSent(String alertName, String alertText, String alertLevel, String isNewAlert) {
Notification notification = new Notification("alerts")
.setFieldValue("projectName", project.getLongName())
.setFieldValue("projectKey", project.getKey())
.setFieldValue("projectId", String.valueOf(project.getId()))
.setFieldValue("alertName", alertName)
.setFieldValue("alertText", alertText)
.setFieldValue("alertLevel", alertLevel)
.setFieldValue("isNewAlert", isNewAlert);
verify(notificationManager, times(1)).scheduleForSending(eq(notification));
}
}

+ 11
- 3
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/EmailNotificationsPlugin.java View File

@@ -22,6 +22,8 @@ package org.sonar.plugins.emailnotifications;
import com.google.common.collect.ImmutableList;
import org.sonar.api.ServerExtension;
import org.sonar.api.SonarPlugin;
import org.sonar.plugins.emailnotifications.alerts.AlertsEmailTemplate;
import org.sonar.plugins.emailnotifications.alerts.AlertsOnMyFavouriteProject;
import org.sonar.plugins.emailnotifications.newviolations.NewViolationsEmailTemplate;
import org.sonar.plugins.emailnotifications.newviolations.NewViolationsOnMyFavouriteProject;
import org.sonar.plugins.emailnotifications.reviews.ChangesInReviewAssignedToMeOrCreatedByMe;
@@ -32,10 +34,16 @@ import java.util.List;
public class EmailNotificationsPlugin extends SonarPlugin {
public List<Class<? extends ServerExtension>> getExtensions() {
return ImmutableList.of(
ChangesInReviewAssignedToMeOrCreatedByMe.class,
EmailNotificationChannel.class,
NewViolationsEmailTemplate.class,
// Notify incoming violations on my favourite projects
NewViolationsOnMyFavouriteProject.class,
ReviewEmailTemplate.class);
NewViolationsEmailTemplate.class,
// Notify reviews changes
ChangesInReviewAssignedToMeOrCreatedByMe.class,
ReviewEmailTemplate.class,
// Notify alerts on my favourite projects
AlertsOnMyFavouriteProject.class,
AlertsEmailTemplate.class
);
}
}

+ 107
- 0
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/alerts/AlertsEmailTemplate.java View File

@@ -0,0 +1,107 @@
/*
* Sonar, open source software quality management 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
*/
package org.sonar.plugins.emailnotifications.alerts;

import org.apache.commons.lang.StringUtils;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.measures.Metric;
import org.sonar.api.notifications.Notification;
import org.sonar.plugins.emailnotifications.api.EmailMessage;
import org.sonar.plugins.emailnotifications.api.EmailTemplate;

/**
* Creates email message for notification "alerts".
*
* @since 3.5
*/
public class AlertsEmailTemplate extends EmailTemplate {

private EmailSettings configuration;

public AlertsEmailTemplate(EmailSettings configuration) {
this.configuration = configuration;
}

@Override
public EmailMessage format(Notification notification) {
if (!"alerts".equals(notification.getType())) {
return null;
}

// Retrieve useful values
String projectId = notification.getFieldValue("projectId");
String projectKey = notification.getFieldValue("projectKey");
String projectName = notification.getFieldValue("projectName");
String alertName = notification.getFieldValue("alertName");
String alertText = notification.getFieldValue("alertText");
String alertLevel = notification.getFieldValue("alertLevel");
boolean isNewAlert = Boolean.valueOf(notification.getFieldValue("isNewAlert"));

// Generate text
String subject = generateSubject(projectName, alertLevel, isNewAlert);
String messageBody = generateMessageBody(projectName, projectKey, alertName, alertText, isNewAlert);

// And finally return the email that will be sent
return new EmailMessage()
.setMessageId("alerts/" + projectId)
.setSubject(subject)
.setMessage(messageBody);
}

private String generateSubject(String projectName, String alertLevel, boolean isNewAlert) {
StringBuilder subjectBuilder = new StringBuilder();
if (Metric.Level.OK.toString().equals(alertLevel)) {
subjectBuilder.append("\"").append(projectName).append("\" is back to green");
} else if (isNewAlert) {
subjectBuilder.append("New alert on \"").append(projectName).append("\"");
} else {
subjectBuilder.append("Alert level changed on \"").append(projectName).append("\"");
}
return subjectBuilder.toString();
}

private String generateMessageBody(String projectName, String projectKey, String alertName, String alertText, boolean isNewAlert) {
StringBuilder messageBody = new StringBuilder();
messageBody.append("Project: ").append(projectName).append("\n");
messageBody.append("Alert level: ").append(alertName).append("\n\n");

String[] alerts = StringUtils.split(alertText, ",");
if (alerts.length > 0) {
if (isNewAlert) {
messageBody.append("New alert");
} else {
messageBody.append("Alert");
}
if (alerts.length == 1) {
messageBody.append(": ").append(alerts[0].trim()).append("\n");
} else {
messageBody.append("s:\n");
for (String alert : alerts) {
messageBody.append(" - ").append(alert.trim()).append("\n");
}
}
}

messageBody.append("\n").append("See it in Sonar: ").append(configuration.getServerBaseURL()).append("/dashboard/index/").append(projectKey);

return messageBody.toString();
}

}

+ 53
- 0
plugins/sonar-email-notifications-plugin/src/main/java/org/sonar/plugins/emailnotifications/alerts/AlertsOnMyFavouriteProject.java View File

@@ -0,0 +1,53 @@
/*
* Sonar, open source software quality management 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
*/
package org.sonar.plugins.emailnotifications.alerts;

import org.apache.commons.lang.StringUtils;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.core.properties.PropertiesDao;

import java.util.List;

/**
* This dispatcher means: "notify me when alerts are raised on projects that I flagged as favourite".
*
* @since 3.5
*/
public class AlertsOnMyFavouriteProject extends NotificationDispatcher {

private PropertiesDao propertiesDao;

public AlertsOnMyFavouriteProject(PropertiesDao propertiesDao) {
this.propertiesDao = propertiesDao;
}

@Override
public void dispatch(Notification notification, Context context) {
if (StringUtils.equals(notification.getType(), "alerts")) {
Long projectId = Long.parseLong(notification.getFieldValue("projectId"));
List<String> userLogins = propertiesDao.findUserIdsForFavouriteResource(projectId);
for (String userLogin : userLogins) {
context.addUser(userLogin);
}
}
}

}

+ 1
- 1
plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/EmailNotificationsPluginTest.java View File

@@ -26,6 +26,6 @@ import static org.fest.assertions.Assertions.assertThat;
public class EmailNotificationsPluginTest {
@Test
public void should_get_extensions() {
assertThat(new EmailNotificationsPlugin().getExtensions()).hasSize(5);
assertThat(new EmailNotificationsPlugin().getExtensions()).hasSize(7);
}
}

+ 131
- 0
plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/alerts/AlertsEmailTemplateTest.java View File

@@ -0,0 +1,131 @@
/*
* Sonar, open source software quality management 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
*/
package org.sonar.plugins.emailnotifications.alerts;

import org.junit.Before;
import org.junit.Test;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
import org.sonar.plugins.emailnotifications.api.EmailMessage;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class AlertsEmailTemplateTest {

private AlertsEmailTemplate template;

@Before
public void setUp() {
EmailSettings configuration = mock(EmailSettings.class);
when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org");
template = new AlertsEmailTemplate(configuration);
}

@Test
public void shouldNotFormatIfNotCorrectNotification() {
Notification notification = new Notification("other-notif");
EmailMessage message = template.format(notification);
assertThat(message, nullValue());
}

@Test
public void shouldFormatAlertWithSeveralMessages() {
Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "false");

EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("Alert level changed on \"Foo\""));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Orange (was Red)\n" +
"\n" +
"Alerts:\n" +
" - violations > 4\n" +
" - coverage < 75%\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}

@Test
public void shouldFormatNewAlertWithSeveralMessages() {
Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "true");

EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("New alert on \"Foo\""));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Orange (was Red)\n" +
"\n" +
"New alerts:\n" +
" - violations > 4\n" +
" - coverage < 75%\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}

@Test
public void shouldFormatNewAlertWithOneMessage() {
Notification notification = createNotification("Orange (was Red)", "violations > 4", "WARN", "true");

EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("New alert on \"Foo\""));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Orange (was Red)\n" +
"\n" +
"New alert: violations > 4\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}

@Test
public void shouldFormatBackToGreenMessage() {
Notification notification = createNotification("Green (was Red)", "", "OK", "false");

EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("\"Foo\" is back to green"));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Green (was Red)\n" +
"\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}

private Notification createNotification(String alertName, String alertText, String alertLevel, String isNewAlert) {
Notification notification = new Notification("alerts")
.setFieldValue("projectName", "Foo")
.setFieldValue("projectKey", "org.sonar.foo:foo")
.setFieldValue("projectId", "45")
.setFieldValue("alertName", alertName)
.setFieldValue("alertText", alertText)
.setFieldValue("alertLevel", alertLevel)
.setFieldValue("isNewAlert", isNewAlert);
return notification;
}

}

+ 62
- 0
plugins/sonar-email-notifications-plugin/src/test/java/org/sonar/plugins/emailnotifications/alerts/AlertsOnMyFavouriteProjectTest.java View File

@@ -0,0 +1,62 @@
/*
* Sonar, open source software quality management 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
*/
package org.sonar.plugins.emailnotifications.alerts;

import com.google.common.collect.Lists;
import org.junit.Test;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.core.properties.PropertiesDao;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

public class AlertsOnMyFavouriteProjectTest {

@Test
public void shouldNotDispatchIfNotNewViolationsNotification() throws Exception {
NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class);
AlertsOnMyFavouriteProject dispatcher = new AlertsOnMyFavouriteProject(null);
Notification notification = new Notification("other-notif");
dispatcher.dispatch(notification, context);

verify(context, never()).addUser(any(String.class));
}

@Test
public void shouldDispatchToUsersWhoHaveFlaggedProjectAsFavourite() {
NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class);
PropertiesDao propertiesDao = mock(PropertiesDao.class);
when(propertiesDao.findUserIdsForFavouriteResource(34L)).thenReturn(Lists.newArrayList("user1", "user2"));
AlertsOnMyFavouriteProject dispatcher = new AlertsOnMyFavouriteProject(propertiesDao);

Notification notification = new Notification("alerts").setFieldValue("projectId", "34");
dispatcher.dispatch(notification, context);

verify(context).addUser("user1");
verify(context).addUser("user2");
verifyNoMoreInteractions(context);
}

}

+ 1
- 1
pom.xml View File

@@ -69,7 +69,7 @@
</prerequisites>

<properties>
<sonarJava.version>1.2-SNAPSHOT</sonarJava.version>
<sonarJava.version>1.1</sonarJava.version>
<sonarGwt.version>3.3.1</sonarGwt.version>
<h2.version>1.3.167</h2.version>
<jetty.version>6.1.25</jetty.version>

+ 2
- 2
sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchSettings.java View File

@@ -47,7 +47,7 @@ public class BatchSettings extends Settings {
this(bootstrapSettings, propertyDefinitions, null, client, deprecatedConfiguration, globalProperties);
}

public BatchSettings(BootstrapSettings bootstrapSettings, PropertyDefinitions propertyDefinitions, ProjectReactor reactor,
public BatchSettings(BootstrapSettings bootstrapSettings, PropertyDefinitions propertyDefinitions, @Nullable ProjectReactor reactor,
ServerClient client, Configuration deprecatedConfiguration, GlobalBatchProperties globalProperties) {
super(propertyDefinitions);
this.deprecatedConfiguration = deprecatedConfiguration;
@@ -58,7 +58,7 @@ public class BatchSettings extends Settings {
BatchSettings() {
}

private void init(BootstrapSettings bootstrapSettings, ProjectReactor reactor, ServerClient client,
private void init(BootstrapSettings bootstrapSettings, @Nullable ProjectReactor reactor, ServerClient client,
GlobalBatchProperties globalProperties) {
LoggerFactory.getLogger(BatchSettings.class).info("Load batch settings");


+ 7
- 1
sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java View File

@@ -27,6 +27,8 @@ import org.sonar.batch.FakeMavenPluginExecutor;
import org.sonar.batch.MavenPluginExecutor;
import org.sonar.core.config.Logback;

import javax.annotation.Nullable;

/**
* Level 1 components
*/
@@ -37,11 +39,15 @@ public class BootstrapModule extends Module {
private GlobalBatchProperties globalProperties;
private String taskCommand;

/**
* @deprecated Use {@link #BootstrapModule(GlobalBatchProperties, String, ProjectReactor, Object...)}
*/
@Deprecated
public BootstrapModule(ProjectReactor reactor, Object... boostrapperComponents) {
this(new GlobalBatchProperties(), null, reactor, boostrapperComponents);
}

public BootstrapModule(GlobalBatchProperties globalProperties, String taskCommand, ProjectReactor reactor,
public BootstrapModule(GlobalBatchProperties globalProperties, @Nullable String taskCommand, @Nullable ProjectReactor reactor,
Object... boostrapperComponents) {
this.globalProperties = globalProperties;
this.taskCommand = taskCommand;

+ 3
- 1
sonar-batch/src/main/java/org/sonar/batch/bootstrap/TaskBootstrapModule.java View File

@@ -26,6 +26,8 @@ import org.sonar.batch.tasks.InspectionTask;
import org.sonar.batch.tasks.ListTasksTask;
import org.sonar.batch.tasks.Tasks;

import javax.annotation.Nullable;

/**
* Level-2 components. Collect tasks definitions.
*/
@@ -33,7 +35,7 @@ public class TaskBootstrapModule extends Module {

private String taskCommand;

public TaskBootstrapModule(String taskCommand) {
public TaskBootstrapModule(@Nullable String taskCommand) {
this.taskCommand = taskCommand;
}


+ 3
- 1
sonar-batch/src/main/java/org/sonar/batch/tasks/Tasks.java View File

@@ -29,6 +29,8 @@ import org.sonar.api.task.TaskComponent;
import org.sonar.api.task.TaskDefinition;
import org.sonar.api.utils.SonarException;

import javax.annotation.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@@ -49,7 +51,7 @@ public class Tasks implements TaskComponent {
this.taskDefinitions = taskDefinitions;
}

public TaskDefinition getTaskDefinition(String command) {
public TaskDefinition getTaskDefinition(@Nullable String command) {
String finalCommand = command;
if (StringUtils.isBlank(finalCommand)) {
// Try with a property

+ 1
- 0
sonar-plugin-api/src/main/java/org/sonar/api/Property.java View File

@@ -89,6 +89,7 @@ public @interface Property {
* Options for *_LIST types
*
* @since 3.0 Options for property of type PropertyType.SINGLE_SELECT_LIST</code>
* For example {"property_1", "property_3", "property_3"}).
*
* @since 3.3 Options for property of type PropertyType.METRIC</code>.
* If no option is specified, any metric will match.

+ 4
- 2
sonar-plugin-api/src/main/java/org/sonar/api/web/WidgetProperty.java View File

@@ -39,7 +39,7 @@ public @interface WidgetProperty {
boolean optional() default true;

/**
* Options for property of type WidgetPropertyType.METRIC</code>.
* @since 3.3 Options for property of type WidgetPropertyType.METRIC</code>.
*
* If no option is specified, any metric will match.
* If options are specified, all must match for the metric to be displayed.
@@ -48,7 +48,9 @@ public @interface WidgetProperty {
* For example <code>type:INT,FLOAT</code> will match any metric of type <code>INT</code> or <code>FLOAT</code>.
* For example <code>type:NUMERIC</code> will match any metric of numerictype.
*
* @since 3.3
* @since 3.5 Options for property of type WidgetPropertyType.SINGLE_SELECT_LIST</code>
* For example {"property_1", "property_3", "property_3"}).
*
*/
String[] options() default {};
}

+ 8
- 1
sonar-plugin-api/src/main/java/org/sonar/api/web/WidgetPropertyType.java View File

@@ -73,5 +73,12 @@ public enum WidgetPropertyType {
*
* @since 3.2
*/
REGULAR_EXPRESSION
REGULAR_EXPRESSION,

/**
* Single select list with a list of options
*
* @since 3.5
*/
SINGLE_SELECT_LIST
}

+ 0
- 5
sonar-server/src/main/webapp/WEB-INF/app/controllers/application_controller.rb View File

@@ -213,9 +213,4 @@ class ApplicationController < ActionController::Base
message("property.category.#{category}", :default => category)
end

# Force url_to to use relative path
def default_url_options(options)
{:only_path => true}
end

end

+ 58
- 24
sonar-server/src/main/webapp/WEB-INF/app/controllers/bulk_deletion_controller.rb View File

@@ -25,34 +25,56 @@ class BulkDeletionController < ApplicationController
verify :method => :post, :only => [:delete_resources], :redirect_to => { :action => :index }

def index
deletion_manager = ResourceDeletionManager.instance
if pending_mass_deletion?
render :template => 'bulk_deletion/pending_deletions'
return
end
@tabs = deletable_qualifiers
if deletion_manager.currently_deleting_resources? ||
(!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?)
# a mass deletion is happening or it has just finished with errors => display the message from the Resource Deletion Manager
@deletion_manager = deletion_manager
@selected_tab = params[:resource_type]
@selected_tab = 'TRK' unless @tabs.include?(@selected_tab)
# Search for resources
conditions = "resource_index.qualifier=:qualifier"
values = {:qualifier => @selected_tab}
if params[:name_filter] && !params[:name_filter].blank?
conditions += " AND resource_index.kee LIKE :kee"
values[:kee] = params[:name_filter].strip.downcase + '%'
end

conditions += " AND projects.enabled=:enabled"
values[:enabled] = true
@resources = Project.find(:all,
:select => 'distinct(resource_index.resource_id),projects.id,projects.name,projects.kee,projects.long_name',
:conditions => [conditions, values],
:joins => :resource_index)
@resources = Api::Utils.insensitive_sort!(@resources){|r| r.name}
end

def ghosts
if pending_mass_deletion?
render :template => 'bulk_deletion/pending_deletions'
else
@tabs = Java::OrgSonarServerUi::JRubyFacade.getInstance().getQualifiersWithProperty('deletable')
@selected_tab = params[:resource_type]
@selected_tab = 'TRK' unless @tabs.include?(@selected_tab)
return
end
# Search for resources
conditions = "resource_index.qualifier=:qualifier"
values = {:qualifier => @selected_tab}
if params[:name_filter] && !params[:name_filter].blank?
conditions += " AND resource_index.kee LIKE :kee"
values[:kee] = params[:name_filter].strip.downcase + '%'
@tabs = deletable_qualifiers
conditions = "scope=:scope AND qualifier IN (:qualifiers) AND status=:status"
values = {:scope => 'PRJ', :qualifiers => @tabs}
unprocessed_project_ids = Snapshot.find(:all, :select => 'project_id', :conditions => [conditions, values.merge({:status => Snapshot::STATUS_UNPROCESSED})]).map(&:project_id).uniq
already_processed_project_ids = Snapshot.find(:all, :select => 'project_id', :conditions => [conditions + " AND project_id IN (:pids)", values.merge({:status => Snapshot::STATUS_PROCESSED, :pids => unprocessed_project_ids})]).map(&:project_id).uniq
@ghosts = Project.find(:all, :conditions => ["id IN (?)", unprocessed_project_ids - already_processed_project_ids])
@ghosts_by_qualifier = {}
@ghosts.each do |p|
qualifier = p.qualifier
if @ghosts_by_qualifier[qualifier]
@ghosts_by_qualifier[qualifier] << p
else
@ghosts_by_qualifier[qualifier] = [p]
end

conditions += " AND projects.enabled=:enabled"
values[:enabled] = true
@resources = Project.find(:all,
:select => 'distinct(resource_index.resource_id),projects.id,projects.name,projects.kee,projects.long_name',
:conditions => [conditions, values],
:joins => :resource_index)
@resources = Api::Utils.insensitive_sort!(@resources){|r| r.name}
end
end
@@ -86,5 +108,17 @@ class BulkDeletionController < ApplicationController
redirect_to :action => 'index'
end
private
# Tells if a mass deletion is happening or if it has finished with errors
def pending_mass_deletion?
deletion_manager = ResourceDeletionManager.instance
deletion_manager.currently_deleting_resources? || (!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?)
end
def deletable_qualifiers
Java::OrgSonarServerUi::JRubyFacade.getInstance().getQualifiersWithProperty('deletable')
end

end

+ 6
- 2
sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb View File

@@ -189,11 +189,15 @@ class DashboardController < ApplicationController
end

def load_widget_definitions(filter_on_category)
@widget_definitions=java_facade.getWidgets()
@widget_definitions=java_facade.getWidgets().sort {|w1,w2| widgetL10nName(w1) <=> widgetL10nName(w2)}
@widget_categories=@widget_definitions.map(&:getWidgetCategories).flatten.uniq.sort
unless filter_on_category.blank?
@widget_definitions=@widget_definitions.select { |definition| definition.getWidgetCategories().to_a.include?(filter_on_category) }
end
end
def widgetL10nName(widget)
Api::Utils.message('widget.' + widget.id + '.name')
end
end

+ 81
- 27
sonar-server/src/main/webapp/WEB-INF/app/helpers/properties_helper.rb View File

@@ -19,45 +19,81 @@
#
module PropertiesHelper

# html_options support :
# :html_id - the id of the generated HTML element
SCREEN_SETTINGS = 'SETTINGS'
SCREEN_WIDGET = 'WIDGET'
SCREEN_RULES = 'RULES'

#
# screen is SETTINGS, WIDGET or RULES
# ==== Options
# * <tt>:id</tt> - html id to use if the name should not be used
# * <tt>:size</tt>
# * <tt>:values</tt> - list of values for metric and single select list
# * <tt>:default</tt> - default value
# * <tt>:disabled</tt>
# * <tt>:extra_values</tt>
#
def property_input_field(key, type, value, options, html_options = {})
if type==PropertyType::TYPE_STRING
text_field_tag key, value, {:size => 25}.update(html_options)
def property_input_field(name, type, value, screen, options = {})

elsif type==PropertyType::TYPE_TEXT
text_area_tag key, value, {:rows => '6', :style => 'width: 100%'}.update(html_options)
html_options = {:id => options[:id] || name}
html_options[:disabled] = 'disabled' if options[:disabled]

elsif type==PropertyType::TYPE_PASSWORD
password_field_tag key, value, {:size => 25}.update(html_options)
case type

elsif type==PropertyType::TYPE_BOOLEAN
(hidden_field_tag key, 'false', html_options) + (check_box_tag key, 'true', value=='true', html_options)
when PropertyType::TYPE_STRING
text_field_tag name, value, {:size => options[:size] || 50}.update(html_options)

elsif type==PropertyType::TYPE_INTEGER
text_field_tag key, value, {:size => 10}.update(html_options)
when PropertyType::TYPE_TEXT
cols = options[:size] || nil
html_class = cols.nil? ? ' width100' : ''
text_area_tag name, value, {:class => html_class, :rows => '5', :cols => cols}.update(html_options)

elsif type==PropertyType::TYPE_FLOAT
text_field_tag key, value, {:size => 10}.update(html_options)
when PropertyType::TYPE_PASSWORD
password_field_tag name, value, {:size => options[:size] || 50}.update(html_options)

elsif type==PropertyType::TYPE_METRIC
metric_select_tag key, metrics_filtered_by(options), {:selected_key => value, :allow_empty => true}.update(html_options)
when PropertyType::TYPE_BOOLEAN
select_options = "<option value='' #{ 'selected' if value.blank? }>#{ message('default') }</option>"
select_options += "<option value='true' #{ 'selected' if value=='true' }>#{ message('true') }</option>"
select_options += "<option value='false' #{ 'selected' if value=='false' }>#{ message('false') }</option>"
select_tag name, select_options, html_options

elsif type==PropertyType::TYPE_REGULAR_EXPRESSION
text_field_tag key, value, {:size => 25}.update(html_options)
when PropertyType::TYPE_INTEGER
size = options[:size] || 10
text_field_tag name, value, {:size => size}.update(html_options)

elsif type==PropertyType::TYPE_FILTER
user_filters = options_id(value, current_user.measure_filters)
shared_filters = options_id(value, MeasureFilter.find(:all, :conditions => ['(user_id<>? or user_id is null) and shared=?', current_user.id, true]).sort_by(&:name))
when PropertyType::TYPE_FLOAT
size = options[:size] || 10
text_field_tag name, value, {:size => size}.update(html_options)

filters_combo = select_tag key, option_group('My Filters', user_filters) + option_group('Shared Filters', shared_filters), html_options
filter_link = link_to message('widget.filter.edit'), {:controller => 'measures', :action => 'manage'}, :class => 'link-action'
when PropertyType::TYPE_METRIC
metric_select_tag name, metrics_filtered_by(options[:values]), {:html_id => options[:id], :selected_key => value, :allow_empty => true,
:placeholder => !options[:default].blank? ? message('default') : nil}

"#{filters_combo} #{filter_link}"
when PropertyType::TYPE_REGULAR_EXPRESSION
size = options[:size] || 50
text_field_tag name, value, {:size => size}.update(html_options)

else
hidden_field_tag key, html_options
when PropertyType::TYPE_FILTER
user_filters = options_id(value, current_user.measure_filters)
shared_filters = options_id(value, MeasureFilter.find(:all, :conditions => ['(user_id<>? or user_id is null) and shared=?', current_user.id, true]).sort_by(&:name))

filters_combo = select_tag name, option_group('My Filters', user_filters) + option_group('Shared Filters', shared_filters), html_options
filter_link = link_to message('widget.filter.edit'), {:controller => 'measures', :action => 'manage'}, :class => 'link-action'

"#{filters_combo} #{filter_link}"

when PropertyType::TYPE_SINGLE_SELECT_LIST
default_value = options[:default].blank? ? '' : message('default')
select_options = "<option value=''>#{ default_value }</option>"
options[:values].each do |option|
message = screen == SCREEN_WIDGET ? option_name_with_key(name, nil, option, 'widget.'+ options[:extra_values][:widget_key]) :
option_name(options[:extra_values][:property], options[:extra_values][:field], option)
select_options += "<option value='#{ html_escape option }' #{ 'selected' if value && value==option }>#{ message }</option>"
end
select_tag name, select_options, html_options

else
hidden_field_tag id, html_options
end
end

@@ -92,4 +128,22 @@ module PropertiesHelper
end
end

def option_name_with_key(property_key, field_key, option, key_prefix = '')
prefix = ''
prefix = key_prefix + "." if !key_prefix.blank?
if field_key
# Old key used for retro-compatibility
message = message(prefix +"option.#{property_key}.#{field_key}.#{option}.name", :default => '')
message = message(prefix +"property.#{property_key}.#{field_key}.option.#{option}.name", :default => option) unless message != ''
message
else
# Old key used for retro-compatibility
puts "#### "+ prefix.to_s

message = message(prefix +"option.#{property_key}.#{option}.name", :default => '')
message = message(prefix +"property.#{property_key}.option.#{option}.name", :default => option) unless message != ''
message
end
end

end

+ 2
- 2
sonar-server/src/main/webapp/WEB-INF/app/helpers/rules_configuration_helper.rb View File

@@ -49,8 +49,8 @@ module RulesConfigurationHelper

def param_value_input(parameter, value, options = {})
type=type_with_compatibility(parameter.param_type)
property_input_field 'value', type, value, nil, {:id => parameter.id}.update(options)
name = options[:name] || 'value'
property_input_field name, type, value, 'WIDGET', {:id => parameter.id, :size => options[:size] }.update(options)
end

def is_set(type)

+ 1
- 5
sonar-server/src/main/webapp/WEB-INF/app/helpers/settings_helper.rb View File

@@ -45,11 +45,7 @@ module SettingsHelper
end

def option_name(property, field, option)
if field
message("option.#{property.key}.#{field.key}.#{option}.name", :default => option)
else
message("option.#{property.key}.#{option}.name", :default => option)
end
option_name_with_key(property.key, field ? field.key : nil, option, nil)
end

def category_help(category)

+ 12
- 2
sonar-server/src/main/webapp/WEB-INF/app/helpers/widget_properties_helper.rb View File

@@ -21,8 +21,18 @@ module WidgetPropertiesHelper
include PropertiesHelper

def property_value_field(definition, value, widget)
property_input_field definition.key, definition.type.name, value.nil? ? definition.defaultValue : value, definition.options,
{:html_id => "prop-#{widget.id}-#{widget.key.parameterize}-#{definition.key.parameterize}"}
id = definition.type.name != PropertyType::TYPE_METRIC ? definition.key : "prop-#{widget.id}-#{widget.key.parameterize}-#{definition.key.parameterize}"
options = {:values => definition.options, :id => id, :default => definition.defaultValue}
options[:extra_values] = {:widget_key => widget.key} if definition.type.name == PropertyType::TYPE_SINGLE_SELECT_LIST
property_input_field definition.key, definition.type.name, value, 'WIDGET', options
end

def default_value(property_def)
defaultValue = property_def.defaultValue
# Boolean type should always have a default value, if no one is provided it's force to false
defaultValue = property_def.type.name == PropertyType::TYPE_BOOLEAN ? 'false' : property_def.defaultValue if defaultValue.blank?
defaultValue = '********' if property_def.type.name == PropertyType::TYPE_PASSWORD
defaultValue
end

end

+ 1
- 1
sonar-server/src/main/webapp/WEB-INF/app/models/property_type.rb View File

@@ -24,7 +24,7 @@ class PropertyType
TYPE_BOOLEAN = 'BOOLEAN'
TYPE_INTEGER = 'INTEGER'
TYPE_FLOAT = 'FLOAT'
#TYPE_SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST'
TYPE_SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST'
TYPE_METRIC = 'METRIC'
TYPE_LICENSE = 'LICENSE'
TYPE_REGULAR_EXPRESSION = 'REGULAR_EXPRESSION'

+ 65
- 0
sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/ghosts.html.erb View File

@@ -0,0 +1,65 @@
<h1 class="marginbottom10"><%= message('bulk_deletion.page') -%></h1>

<ul class="tabs">
<% @tabs.each do |tab| %>
<li>
<a href="<%= url_for :action => 'index', :resource_type => tab %>" id="tab-<%= u tab -%>"><%= message('qualifiers.' + tab) -%></a>
</li>
<% end %>
<li>
<a href="<%= url_for :action => 'ghosts' %>" id="tab-ghosts" class="selected"><%= message('bulk_deletion.ghosts') -%></a>
</li>
</ul>

<div class="tabs-panel marginbottom10">

<p style="margin: 0 0 15px 0">
<%= message('bulk_deletion.ghosts.description') -%>
</p>
<% unless @ghosts.size > 0 %>
<b><%= message('bulk_deletion.no_ghosts') -%></b>
<% else %>
<p style="margin: 0 0 10px 0">
<%= message('bulk_deletion.following_ghosts_can_be_deleted') -%>
</p>

<table>
<%
@tabs.each do |qualifier|
ghosts = @ghosts_by_qualifier[qualifier]
if ghosts
%>
<tr>
<td class="thin nowrap top" style="padding-right: 20px">
<b><%= message('qualifiers.' + qualifier) -%> :</b>
</td>
<td>
<ul>
<%
ghosts.sort {|x,y| x.name <=> y.name}.each_with_index do |resource, index|
%>
<li><%= h resource.name -%> <span class="small gray">( <%= resource.key -%> )</span></li>
<%
end
%>
</ul>
<%
end
end
%>
</table>
<p style="margin: 15px 0">
<% form_remote_tag( :url => {:action => 'delete_resources'}, :loading => "window.location='#{url_for :action => 'pending_deletions'}';") do %>
<input type="hidden" id="all_resources" name="all_resources" value="<%= @ghosts.map {|r| r.id.to_s}.join(',') -%>"/>
<input id="delete_resources" class="action red-button" type="submit" value="<%= message('bulk_deletion.delete_all_ghosts') -%>" name="commit">
<% end %>
</p>

<% end %>

</div>

+ 3
- 0
sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/index.html.erb View File

@@ -6,6 +6,9 @@
<a href="<%= url_for :action => 'index', :resource_type => tab %>" <%= "class='selected'" if @selected_tab==tab -%> id="tab-<%= u tab -%>"><%= message('qualifiers.' + tab) -%></a>
</li>
<% end %>
<li>
<a href="<%= url_for :action => 'ghosts' %>" id="tab-ghosts"><%= message('bulk_deletion.ghosts') -%></a>
</li>
</ul>

<div class="tabs-panel marginbottom10">

+ 10
- 3
sonar-server/src/main/webapp/WEB-INF/app/views/dashboard/_widget_properties.html.erb View File

@@ -24,15 +24,22 @@

<% widget.java_definition.getWidgetProperties().each do |property_def| %>
<tr>
<td class="form-key-cell"><%= message("widget." + widget.key + ".prop." + property_def.key() + ".key", :default => property_def.key()) -%><%= " *" unless property_def.optional() -%></td>
<td class="form-key-cell"><%= message("widget." + widget.key + ".property." + property_def.key() + ".name", :default => property_def.key()) -%><%= " *" unless property_def.optional() -%></td>
<td class="form-val-cell" id="row_<%= property_def.key() -%>">
<%= property_value_field(property_def, widget.property_text_value(property_def.key()), widget) -%>
<div class="form-val-note">
<%
# Old key used for retro-compatibility
property_description = message("widget." + widget.key + ".param." + property_def.key(), :default => '')
property_description = message("widget." + widget.key + ".prop." + property_def.key() + ".desc", :default => property_def.description()) unless property_description != ''
property_description = message("widget." + widget.key + ".property." + property_def.key() + ".desc", :default => property_def.description()) unless property_description != ''
-%>
<%= property_description -%>

<% unless property_description.blank? -%>
<div><%= property_description -%></div>
<% end %>
<% if !property_def.defaultValue.blank? || property_def.type.name == PropertyType::TYPE_BOOLEAN -%>
<div><%= message('default') %>: <%= h(default_value property_def) -%></div>
<% end -%>
</div>
</td>
</tr>

+ 1
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/project_reviews/index.html.erb View File

@@ -19,7 +19,7 @@
url += '?sort=' + columnName;
}
if (!document.URL.contains('&asc=true')) {
if (document.URL.indexOf('&asc=true') == -1) {
url += '&asc=true';
}
}

+ 1
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb View File

@@ -44,7 +44,7 @@

<% default_prop_value = (@resource ? Property.value(property.key, nil, property.defaultValue) : property.defaultValue) -%>
<% unless default_prop_value.blank? -%>
<div class="note">Default: <%= property.type.to_s=='PASSWORD' ? '********' : h(default_prop_value) -%></div>
<div class="note"><%= message('default') %>: <%= property.type.to_s=='PASSWORD' ? '********' : h(default_prop_value) -%></div>
<% end -%>
</td>
</tr>

+ 1
- 5
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb View File

@@ -1,5 +1 @@
<select name="<%= name -%>" id="<%= id -%>">
<option value="" <%= 'selected' if value.blank? -%>><%= message('default') -%></option>
<option value="true" <%= 'selected' if value=='true' -%>><%= message('true') -%></option>
<option value="false" <%= 'selected' if value=='false' -%>><%= message('false') -%></option>
</select>
<%= property_input_field(name, PropertyType::TYPE_BOOLEAN, value, PropertiesHelper::SCREEN_SETTINGS, {:id => id, :default => (defined? property.defaultValue) ? property.defaultValue : nil }) %>

+ 5
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb View File

@@ -1 +1,5 @@
<input type="text" name="<%= name -%>" value="<%= h value if value -%>" size="<%= (defined? size) ? size : 50 -%>" id="<%= id -%>"/>
<%
options = {:id => id}
options[:size] = (defined? size) ? size : nil
%>
<%= property_input_field(name, PropertyType::TYPE_FLOAT, value, PropertiesHelper::SCREEN_SETTINGS, options) %>

+ 5
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb View File

@@ -1 +1,5 @@
<input type="text" name="<%= name -%>" value="<%= h value if value -%>" size="<%= (defined? size) ? size : 50 -%>" id="<%= id-%>"/>
<%
options = {:id => id}
options[:size] = (defined? size) ? size : nil
%>
<%= property_input_field(name, PropertyType::TYPE_INTEGER, value, PropertiesHelper::SCREEN_SETTINGS, options) %>

+ 4
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb View File

@@ -1,5 +1,8 @@
<%
defaultValue = (defined? property.defaultValue) ? property.defaultValue : nil
%>
<select name="<%= name -%>" id="<%= id -%>">
<option value=""><%= message('default') -%></option>
<option value=""><%= !defaultValue.blank? ? message('default') : nil -%></option>
<%
metrics_per_domain={}
metrics_filtered_by(property.options).each do |metric|

+ 5
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb View File

@@ -1 +1,5 @@
<input type="password" name="<%= name -%>" value="<%= h value if value -%>" size="<%= defined? size ? size : 50-%>" id="<%= id -%>"/>
<%
options = {:id => id}
options[:size] = (defined? size) ? size : nil
%>
<%= property_input_field(name, PropertyType::TYPE_PASSWORD, value, PropertiesHelper::SCREEN_SETTINGS, options) %>

+ 5
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb View File

@@ -1 +1,5 @@
<input type="text" name="<%= name -%>" value="<%= h value if value -%>" size="<%= (defined? size) ? size : 50 -%>" id="<%= id -%>"/>
<%
options = {:id => id}
options[:size] = (defined? size) ? size : nil
%>
<%= property_input_field(name, PropertyType::TYPE_REGULAR_EXPRESSION, value, PropertiesHelper::SCREEN_SETTINGS, options) %>

+ 2
- 6
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb View File

@@ -1,6 +1,2 @@
<select name="<%= name -%>" id="<%= id -%>">
<option value=""><%= message('default') -%></option>
<% property.options.each do |option| %>
<option value="<%= h option -%>" <%= 'selected' if value && value==option -%>><%= option_name(property, field, option) -%></option>
<% end %>
</select>
<%= property_input_field(name, PropertyType::TYPE_SINGLE_SELECT_LIST, value, PropertiesHelper::SCREEN_SETTINGS,
{:id => id, :default => (defined? property.defaultValue) ? property.defaultValue : nil, :values => property.options, :extra_values => {:property => property, :field => field}}) %>

+ 5
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb View File

@@ -1 +1,5 @@
<input type="text" name="<%= name -%>" value="<%= h value if value -%>" size="<%= (defined? size) ? size : 50 -%>" id="<%= id -%>"/>
<%
options = {:id => id}
options[:size] = (defined? size) ? size : nil
%>
<%= property_input_field(name, PropertyType::TYPE_STRING, value, PropertiesHelper::SCREEN_SETTINGS, options) %>

+ 5
- 1
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb View File

@@ -1 +1,5 @@
<textarea rows="5" cols="<%= (defined? size) ? size : 80 -%>" class="<%= (defined? size) ? '' : 'width100' -%>" name="<%= name -%>" id="<%= id -%>"><%= h value if value -%></textarea>
<%
options = {:id => id}
options[:size] = (defined? size) ? size : nil
%>
<%= property_input_field(name, PropertyType::TYPE_TEXT, value, PropertiesHelper::SCREEN_SETTINGS, options) %>

+ 1
- 1
sonar-server/src/main/webapp/WEB-INF/config/routes.rb View File

@@ -30,7 +30,7 @@ ActionController::Routing::Routes.draw do |map|
map.resources 'properties', :path_prefix => 'api', :controller => 'api/properties', :requirements => { :id => /.*/ }

# home page
map.home '', :controller => :dashboard, :action => :index, :only_path => true
map.home '', :controller => :dashboard, :action => :index
map.root :controller => :dashboard, :action => :index

# page plugins

Loading…
Cancel
Save