]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3825 manage my filters
authorSimon Brandhof <simon.brandhof@gmail.com>
Sat, 24 Nov 2012 16:17:54 +0000 (17:17 +0100)
committerSimon Brandhof <simon.brandhof@gmail.com>
Mon, 26 Nov 2012 12:54:16 +0000 (13:54 +0100)
18 files changed:
sonar-core/src/main/java/org/sonar/core/measure/MeasureFilter.java
sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterEngine.java
sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterFactory.java
sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSort.java
sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl
sonar-server/src/main/webapp/WEB-INF/app/controllers/measures_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/measures_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/models/measure_filter.rb
sonar-server/src/main/webapp/WEB-INF/app/models/user.rb
sonar-server/src/main/webapp/WEB-INF/app/models/widget_property.rb
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_display_list.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_edit_form.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/measures/_save_form.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/measures/manage.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/measures/search.html.erb
sonar-server/src/main/webapp/WEB-INF/db/migrate/356_create_measure_filters.rb
sonar-server/src/main/webapp/javascripts/application.js

index be5b8966ad5aed0a850c1025363535079c7c7b91..5c4d247a6d63db23a9ce8fd97ca9c4aacd59313e 100644 (file)
@@ -190,6 +190,6 @@ public class MeasureFilter {
 
   @Override
   public String toString() {
-    return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE);
+    return ReflectionToStringBuilder.toString(this);
   }
 }
index 80c3627e881a3021b6309f1acabc5877e4c46af0..a161e7e9d45e29429e6deab25ae8ce5cdf84b07e 100644 (file)
@@ -54,11 +54,11 @@ public class MeasureFilterEngine implements ServerComponent {
   public List<MeasureFilterRow> execute2(Map<String, Object> filterMap, @Nullable Long userId) throws ParseException {
     Logger logger = FILTER_LOG;
     MeasureFilterContext context = new MeasureFilterContext();
-    context.setJson(Joiner.on("|").withKeyValueSeparator("=").join(filterMap));
     context.setUserId(userId);
     try {
       long start = System.currentTimeMillis();
       MeasureFilter filter = factory.create(filterMap);
+      context.setJson(filter.toString());
       List<MeasureFilterRow> rows = executor.execute(filter, context);
       log(context, rows, (System.currentTimeMillis() - start), logger);
       return rows;
index 8908226fc3065756fc0742cab6e8b5a8e11be2a0..761f8d5c12178b42cda1f7af78ebe4844faa2bc7 100644 (file)
@@ -26,6 +26,7 @@ import org.sonar.api.utils.DateUtils;
 
 import javax.annotation.Nullable;
 
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
@@ -41,33 +42,33 @@ public class MeasureFilterFactory implements ServerComponent {
 
   public MeasureFilter create(Map<String, Object> properties) {
     MeasureFilter filter = new MeasureFilter();
-    filter.setBaseResourceKey((String)properties.get("base"));
-    filter.setResourceScopes((List<String>)properties.get("scopes"));
-    filter.setResourceQualifiers((List<String>)(properties.get("qualifiers")));
-    filter.setResourceLanguages((List<String>)(properties.get("languages")));
-    if (properties.containsKey("onBaseChildren")) {
-      filter.setOnBaseResourceChildren(Boolean.valueOf((String)properties.get("onBaseChildren")));
+    filter.setBaseResourceKey((String) properties.get("base"));
+    filter.setResourceScopes(toList(properties.get("scopes")));
+    filter.setResourceQualifiers(toList(properties.get("qualifiers")));
+    filter.setResourceLanguages(toList(properties.get("languages")));
+    if (properties.containsKey("onBaseComponents")) {
+      filter.setOnBaseResourceChildren(Boolean.valueOf((String) properties.get("onBaseComponents")));
     }
-    filter.setResourceName((String)properties.get("nameRegexp"));
-    filter.setResourceKeyRegexp((String)properties.get("keyRegexp"));
+    filter.setResourceName((String) properties.get("nameRegexp"));
+    filter.setResourceKeyRegexp((String) properties.get("keyRegexp"));
     if (properties.containsKey("fromDate")) {
-      filter.setFromDate(toDate((String)properties.get("fromDate")));
+      filter.setFromDate(toDate((String) properties.get("fromDate")));
     } else if (properties.containsKey("afterDays")) {
-      filter.setFromDate(toDays((String)properties.get("afterDays")));
+      filter.setFromDate(toDays((String) properties.get("afterDays")));
     }
     if (properties.containsKey("toDate")) {
-      filter.setToDate(toDate((String)properties.get("toDate")));
+      filter.setToDate(toDate((String) properties.get("toDate")));
     } else if (properties.containsKey("beforeDays")) {
-      filter.setToDate(toDays((String)properties.get("beforeDays")));
+      filter.setToDate(toDays((String) properties.get("beforeDays")));
     }
 
-    if (properties.containsKey("favourites")) {
-      filter.setUserFavourites(Boolean.valueOf((String)properties.get("favourites")));
+    if (properties.containsKey("onFavourites")) {
+      filter.setUserFavourites(Boolean.valueOf((String) properties.get("onFavourites")));
     }
     if (properties.containsKey("asc")) {
-      filter.setSortAsc(Boolean.valueOf((String)properties.get("asc")));
+      filter.setSortAsc(Boolean.valueOf((String) properties.get("asc")));
     }
-    String s = (String)properties.get("sort");
+    String s = (String) properties.get("sort");
     if (s != null) {
       if (StringUtils.startsWith(s, "metric:")) {
         filter.setSortOnMetric(metricFinder.findByKey(StringUtils.substringAfter(s, "metric:")));
@@ -81,6 +82,18 @@ public class MeasureFilterFactory implements ServerComponent {
     return filter;
   }
 
+  private List<String> toList(@Nullable Object obj) {
+    List<String> result = null;
+    if (obj != null) {
+      if (obj instanceof String) {
+        result = Arrays.asList((String)obj);
+      } else {
+        result = (List<String>)obj;
+      }
+    }
+    return result;
+  }
+
   private static Date toDate(@Nullable String date) {
     if (date != null) {
       return DateUtils.parseDate(date);
index 3eae1162992f830097d2ed9a6e289d11ac7e73c8..b29e11b549fa3634984ce79eed05b9770fee742f 100644 (file)
@@ -23,7 +23,7 @@ import org.sonar.api.measures.Metric;
 
 class MeasureFilterSort {
   public static enum Field {
-    KEY, NAME, VERSION, LANGUAGE, DATE, METRIC
+    KEY, NAME, VERSION, LANGUAGE, DATE, METRIC, SHORT_NAME, DESCRIPTION
   }
 
   private Field field = Field.NAME;
@@ -85,6 +85,12 @@ class MeasureFilterSort {
       case NAME:
         column = "p.long_name";
         break;
+      case SHORT_NAME:
+        column = "p.name";
+        break;
+      case DESCRIPTION:
+        column = "p.description";
+        break;
       case VERSION:
         column = "s.version";
         break;
index e67a2a9ef637a2c44197734e1ae7db0c301800dd..98e7e586b04d825deadcb55a6dbb7d69652508e3 100644 (file)
@@ -517,7 +517,7 @@ CREATE TABLE "SEMAPHORES" (
 CREATE TABLE "MEASURE_FILTERS" (
   "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
   "NAME" VARCHAR(100) NOT NULL,
-  "SHARED" BOOLEAN NOT NULL,
+  "SHARED" BOOLEAN NOT NULL DEFAULT FALSE,
   "USER_ID" INTEGER,
   "DESCRIPTION" VARCHAR(4000),
   "DATA" CLOB(2147483647),
index 25a0248df1ca39ff4f51d3c74a8351ef2c26ebb6..ab33a006d21509e7cde6255bfc17413a9685bed5 100644 (file)
@@ -22,16 +22,108 @@ class MeasuresController < ApplicationController
   SECTION=Navigation::SECTION_HOME
 
   def index
+    @filter = MeasureFilter.new
     render :action => 'search'
   end
 
   def search
-    options = {
-      :user => current_user,
-      :page => (params[:page] || 1)
-    }
-    @filter = MeasureFilter.new(params)
-    @filter.execute(self, options)
+    if params[:id]
+      @filter = MeasureFilter.find(params[:id])
+    else
+      @filter = MeasureFilter.new
+    end
+    @filter.set_criteria_from_url_params(params)
+    @filter.execute(self, :user => current_user)
   end
 
+  # Load existing filter
+  def filter
+    require_parameters :id
+
+    @filter = find_filter(params[:id])
+    @filter.load_criteria_from_data
+    @filter.execute(self, :user => current_user)
+    render :action => 'search'
+  end
+
+  def save_form
+    if params[:id].present?
+      @filter = find_filter(params[:id])
+    else
+      @filter = MeasureFilter.new
+    end
+    @filter.set_criteria_from_url_params(params)
+    @filter.convert_criteria_to_data
+    render :partial => 'measures/save_form'
+  end
+
+  def save
+    verify_post_request
+    access_denied unless logged_in?
+
+    if params[:id].present?
+      @filter = find_filter(params[:id])
+    else
+      @filter = MeasureFilter.new
+      @filter.user_id=current_user.id
+    end
+    @filter.name=params[:name]
+    @filter.description=params[:description]
+    @filter.shared=(params[:shared]=='true')
+    @filter.data=URI.unescape(params[:data])
+    if @filter.save
+      render :text => @filter.id.to_s, :status => 200
+    else
+      render :partial => 'measures/save_form', :status => 400
+    end
+  end
+
+  # GET /measures/manage
+  def manage
+    access_denied unless logged_in?
+  end
+
+  def edit_form
+    require_parameters :id
+    @filter = find_filter(params[:id])
+    render :partial => 'measures/edit_form'
+  end
+
+  def edit
+    verify_post_request
+    access_denied unless logged_in?
+    require_parameters :id
+
+    @filter = MeasureFilter.find(params[:id])
+    access_denied unless owner?(@filter)
+    @filter.name=params[:name]
+    @filter.description=params[:description]
+    @filter.shared=(params[:shared]=='true')
+    if @filter.save
+      render :text => @filter.id.to_s, :status => 200
+    else
+      render :partial => 'measures/edit_form', :status => 400
+    end
+  end
+
+  def delete
+    verify_post_request
+    access_denied unless logged_in?
+    require_parameters :id
+
+    @filter = find_filter(params[:id])
+    @filter.destroy
+    redirect_to :action => 'manage'
+  end
+
+  private
+  def find_filter(id)
+    filter = MeasureFilter.find(id)
+    access_denied unless filter.shared || owner?(filter)
+    filter
+  end
+
+  def owner?(filter)
+    current_user && filter.user_id==current_user.id
+  end
 end
index eb887f649f5b72c4ce381e19ce2ecd56fafd146a..a4c30ef5227ab7f6fde46ef34af7755886dd33b6 100644 (file)
@@ -285,14 +285,14 @@ module ApplicationHelper
     period_index=nil if period_index && period_index<=0
     if resource.display_dashboard?
       if options[:dashboard]
-        link_to(name || resource.name, {:overwrite_params => {:controller => 'dashboard', :action => 'index', :id => resource.id, :period => period_index,
-                                                              :tab => options[:tab], :rule => options[:rule]}}, :title => options[:title])
+        link_to(name || resource.name, params.merge({:controller => 'dashboard', :action => 'index', :id => resource.id, :period => period_index,
+                                                              :tab => options[:tab], :rule => options[:rule]}), :title => options[:title])
       elsif options[:filter]
-        link_to(name || resource.name, {:overwrite_params => {:controller => 'dashboard', :action => 'index', :did => nil, :id => resource.id, :period => period_index,
-                                                                :tab => options[:tab], :rule => options[:rule]}}, :title => options[:title])
+        link_to(name || resource.name, params.merge({:controller => 'dashboard', :action => 'index', :did => nil, :id => resource.id, :period => period_index,
+                                                                :tab => options[:tab], :rule => options[:rule]}), :title => options[:title])
       else
         # stay on the same page (for example components)
-        link_to(name || resource.name, {:overwrite_params => {:id => resource.id, :period => period_index, :tab => options[:tab], :rule => options[:rule]}}, :title => options[:title])
+        link_to(name || resource.name, params.merge({:id => resource.id, :period => period_index, :tab => options[:tab], :rule => options[:rule]}), :title => options[:title])
       end
     else
       if options[:line]
index 6d4beff64a958f05d7690acc3167c6ab2d50ea0b..77f3593f123fce46e386c6dd4827e4bcefbafa3b 100644 (file)
 #
 module MeasuresHelper
 
-  def list_column_title(filter, column, url_params)
+  def list_column_html(filter, column)
+
     if column.sort?
-      html = link_to(h(column.display_name), url_params.merge({:controller => 'measures', :action => 'search', :asc => (!filter.sort_asc?).to_s, :sort => column.key}))
+      html = link_to(h(column.name), filter.url_params.merge({:controller => 'measures', :action => 'search', :asc => (!filter.sort_asc?).to_s, :sort => column.key}))
     else
-      html=h(column.display_name)
+      html=h(column.name)
     end
     #if column.variation
     #  html="<img src='#{ApplicationController.root_context}/images/trend-up.png'></img> #{html}"
@@ -35,4 +36,29 @@ module MeasuresHelper
     "<th class='#{column.align}'>#{html}</th>"
   end
 
+  def list_cell_html(column, result)
+    if column.metric
+      format_measure(result.measure(column.metric))
+    elsif column.key=='name'
+      "#{qualifier_icon(result.snapshot)} #{link_to(result.snapshot.resource.name(true), {:controller => 'dashboard', :id => result.snapshot.resource_id}, :title => result.snapshot.resource.key)}"
+    elsif column.key=='short_name'
+      "#{qualifier_icon(result.snapshot)} #{link_to(result.snapshot.resource.name(false), {:controller => 'dashboard', :id => result.snapshot.resource_id}, :title => result.snapshot.resource.key)}"
+    elsif column.key=='date'
+      human_short_date(result.snapshot.created_at)
+    elsif column.key=='key'
+      "<span class='small'>#{result.snapshot.resource.kee}</span>"
+    elsif column.key=='description'
+      h result.snapshot.resource.description
+    elsif column.key=='version'
+      h result.snapshot.version
+    elsif column.key=='language'
+      h result.snapshot.resource.language
+    elsif column.key=='links' && result.links
+      html = ''
+      result.links.select { |link| link.href.start_with?('http') }.each do |link|
+        html += link_to(image_tag(link.icon, :alt => link.name), link.href, :class => 'nolink', :popup => true) unless link.custom?
+      end
+      html
+    end
+  end
 end
index fd71d2aa75291bbe263d0ec3ab8ff116bc81c321..3e187330e353b9b6b45651e326f2d2f995fc099a 100644 (file)
 # License along with Sonar; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
 #
-class MeasureFilter
+require 'set'
+class MeasureFilter < ActiveRecord::Base
+
   # Row in the table of results
-  class Data
+  class Result
     attr_reader :snapshot, :measures_by_metric, :links
 
     def initialize(snapshot)
@@ -52,7 +54,7 @@ class MeasureFilter
       @metric = Metric.by_key(metric_key) if metric_key
     end
 
-    def display_name
+    def name
       if @metric
         Api::Utils.message("metric.#{@metric.key}.name", :default => @metric.short_name)
       else
@@ -61,7 +63,11 @@ class MeasureFilter
     end
 
     def align
-      (@key=='name') ? 'left' : 'right'
+      @align ||=
+          begin
+            # by default is table cells are left-aligned
+            (@key=='name' || @key=='short_name' || @key=='description') ? '' : 'right'
+          end
     end
 
     def sort?
@@ -74,108 +80,168 @@ class MeasureFilter
   end
 
   class Display
-    def prepare_filter(filter, options)
+    attr_reader :metric_ids
+
+    def initialize(filter)
+    end
+
+    def load_links?
+      false
     end
   end
 
   class ListDisplay < Display
-    def key
-      'list'
+    attr_reader :columns
+
+    KEY = :list
+
+    def initialize(filter)
+      filter.set_criteria_default_value('columns', ['name', 'short_name', 'description', 'links', 'date', 'language', 'version', 'alert', 'metric:ncloc', 'metric:violations'])
+      filter.set_criteria_default_value('sort', 'name')
+      filter.set_criteria_default_value('asc', 'true')
+      filter.set_criteria_default_value('pageSize', '30')
+      filter.pagination.per_page = [filter.criteria['pageSize'].to_i, 200].min
+      filter.pagination.page = (filter.criteria['page'] || 1).to_i
+
+      @columns = filter.criteria['columns'].map { |column_key| Column.new(column_key) }
+      @metric_ids = @columns.map { |column| column.metric.id if column.metric }.compact.uniq
     end
 
-    def prepare_filter(filter, options)
-      filter.set_criteria_default_value(:columns, 'name,date,metric:ncloc,metric:violations')
-      filter.set_criteria_default_value(:sort, 'name')
-      filter.set_criteria_default_value(:asc, true)
-      filter.set_criteria_default_value(:listPageSize, 30)
-      filter.pagination.per_page = [filter.criteria[:listPageSize].to_i, 200].min
-      filter.pagination.page = (options[:page] || 1).to_i
+    def load_links?
+      @columns.index { |column| column.links? }
     end
   end
 
   class TreemapDisplay < Display
-    def key
-      'treemap'
-    end
+    KEY = :treemap
   end
 
-  DISPLAYS = [ListDisplay.new, TreemapDisplay.new]
+  DISPLAYS = [ListDisplay, TreemapDisplay]
 
-  def self.register_display(display)
-    DISPLAYS << display
-  end
+  SUPPORTED_CRITERIA_KEYS=Set.new([:qualifiers, :scopes, :onFavourites, :base, :onBaseComponents, :languages, :fromDate, :toDate, :beforeDays, :afterDays,
+                                   :keyRegexp, :nameRegexp,
+                                   :sort, :asc, :columns, :display, :pageSize, :page])
+  CRITERIA_SEPARATOR = '|'
+  CRITERIA_KEY_VALUE_SEPARATOR = ','
 
-  # Simple hash {string key => fixnum or boolean or string}
-  attr_accessor :criteria
+# Configuration available after call to execute()
+  attr_reader :pagination, :security_exclusions, :columns
 
-  # Configuration available after call to execute()
-  attr_reader :pagination, :security_exclusions, :columns, :display
+# Results : sorted array of Result
+  attr_reader :results
 
-  # Results : sorted array of Data
-  attr_reader :data
+  belongs_to :user
+  validates_presence_of :name, :message => Api::Utils.message('measure_filter.missing_name')
+  validates_length_of :name, :maximum => 100, :message => Api::Utils.message('measure_filter.name_too_long')
+  validates_length_of :description, :allow_nil => true, :maximum => 4000
 
-  def initialize(criteria={})
-    @criteria = criteria
-    @pagination = Api::Pagination.new
+  def criteria
+    @criteria ||= {}
   end
 
   def sort_key
-    @criteria[:sort]
+    criteria['sort']
   end
 
   def sort_asc?
-    @criteria[:asc]=='true'
+    criteria['asc']=='true'
+  end
+
+# API for plugins
+  def self.register_display(display_class)
+    DISPLAYS<<display_class
+  end
+
+  def self.supported_criteria?(key)
+    SUPPORTED_CRITERIA_KEYS.include?(key.to_sym)
+  end
+
+  def set_criteria_from_url_params(params)
+    @criteria = {}
+    params.each_pair do |k, v|
+      if MeasureFilter.supported_criteria?(k) && !v.empty? && v!=['']
+        @criteria[k.to_s]=v
+      end
+    end
+  end
+
+  def load_criteria_from_data
+    if self.data
+      @criteria = self.data.split(CRITERIA_SEPARATOR).inject({}) do |h, s|
+        k, v=s.split('=')
+        if k && v
+          v=v.split(CRITERIA_KEY_VALUE_SEPARATOR) if v.include?(CRITERIA_KEY_VALUE_SEPARATOR)
+          h[k]=v
+        end
+        h
+      end
+    else
+      @criteria = {}
+    end
   end
 
-  # ==== Options
-  # 'page' : page id starting with 1. Used in display 'list'.
-  # 'user' : the authenticated user
-  # 'period' : index of the period between 1 and 5
-  #
+  def convert_criteria_to_data
+    string_data = []
+    if @criteria
+      @criteria.each_pair do |k, v|
+        string_value = (v.is_a?(String) ? v : v.join(CRITERIA_KEY_VALUE_SEPARATOR))
+        string_data << "#{k}=#{string_value}"
+      end
+    end
+    self.data = string_data.join(CRITERIA_SEPARATOR)
+  end
+
+  def display
+    @display ||=
+        begin
+          display_class = nil
+          key = criteria['display']
+          if key.present?
+            display_class = DISPLAYS.find { |d| d::KEY==key.to_sym }
+          end
+          display_class ||= DISPLAYS.first
+          display_class.new(self)
+        end
+  end
+
+
+# ==== Options
+# :user : the authenticated user
   def execute(controller, options={})
-    return reset_results if @criteria.empty?
-    init_display
-    init_filter(options)
+    init_results
 
     user = options[:user]
-    rows=Api::Utils.java_facade.executeMeasureFilter2(@criteria, (user ? user.id : nil))
+    rows=Api::Utils.java_facade.executeMeasureFilter2(criteria, (user ? user.id : nil))
     snapshot_ids = filter_authorized_snapshot_ids(rows, controller)
-    init_data(snapshot_ids)
+    load_results(snapshot_ids)
 
     self
   end
 
+# API used by Displays
   def set_criteria_value(key, value)
     if value
-      @criteria[key.to_sym]=value.to_s
+      @criteria[key.to_s]=value
     else
-      @criteria.delete(key.to_sym)
+      @criteria.delete(key)
     end
   end
 
+# API used by Displays
   def set_criteria_default_value(key, value)
-    set_criteria_value(key, value) unless @criteria.has_key?(key.to_sym)
+    set_criteria_value(key, value) unless criteria.has_key?(key)
   end
 
-  private
-
-  def init_display
-    key = @criteria[:display]
-    if key.present?
-      @display = DISPLAYS.find { |display| display.key==key }
-    end
-    @display ||= DISPLAYS.first
+  def url_params
+    criteria.merge({'id' => self.id})
   end
 
-  def init_filter(options)
-    @display.prepare_filter(self, options)
-    @columns = @criteria[:columns].split(',').map { |col_key| Column.new(col_key) }
-  end
+  private
 
-  def reset_results
+  def init_results
     @pagination = Api::Pagination.new
     @security_exclusions = nil
-    @data = nil
+    @results = nil
     self
   end
 
@@ -185,48 +251,63 @@ class MeasureFilter
     snapshot_ids = rows.map { |row| row.getSnapshotId() if authorized_project_ids.include?(row.getResourceRootId()) }.compact
     @security_exclusions = (snapshot_ids.size<rows.size)
     @pagination.count = snapshot_ids.size
-        snapshot_ids[@pagination.offset .. (@pagination.offset+@pagination.limit)]
+    snapshot_ids[@pagination.offset .. (@pagination.offset+@pagination.limit)]
   end
 
-  def init_data(snapshot_ids)
-    @data = []
+  def load_results(snapshot_ids)
+    @results = []
     if !snapshot_ids.empty?
-      data_by_snapshot_id = {}
+      results_by_snapshot_id = {}
       snapshots = Snapshot.find(:all, :include => ['project'], :conditions => ['id in (?)', snapshot_ids])
       snapshots.each do |snapshot|
-        data = Data.new(snapshot)
-        data_by_snapshot_id[snapshot.id] = data
+        result = Result.new(snapshot)
+        results_by_snapshot_id[snapshot.id] = result
       end
 
-      # @data must be in the same order than the snapshot ids
+      # @results must be in the same order than the snapshot ids
       snapshot_ids.each do |sid|
-        @data << data_by_snapshot_id[sid]
+        @results << results_by_snapshot_id[sid]
       end
 
-      metric_ids = @columns.map { |column| column.metric }.compact.uniq.map { |metric| metric.id }
-      unless metric_ids.empty?
+      if display.metric_ids && !display.metric_ids.empty?
         measures = ProjectMeasure.find(:all, :conditions =>
-          ['rule_priority is null and rule_id is null and characteristic_id is null and person_id is null and snapshot_id in (?) and metric_id in (?)', snapshot_ids, metric_ids]
+            ['rule_priority is null and rule_id is null and characteristic_id is null and person_id is null and snapshot_id in (?) and metric_id in (?)', snapshot_ids, display.metric_ids]
         )
         measures.each do |measure|
-          data = data_by_snapshot_id[measure.snapshot_id]
-          data.add_measure measure
+          result = results_by_snapshot_id[measure.snapshot_id]
+          result.add_measure measure
         end
       end
 
-      if @columns.index { |column| column.links? }
+      if display.load_links?
         project_ids = []
-        data_by_project_id = {}
+        results_by_project_id = {}
         snapshots.each do |snapshot|
           project_ids << snapshot.project_id
-          data_by_project_id[snapshot.project_id] = data_by_snapshot_id[snapshot.id]
+          results_by_project_id[snapshot.project_id] = results_by_snapshot_id[snapshot.id]
         end
         links = ProjectLink.find(:all, :conditions => {:project_id => project_ids}, :order => 'link_type')
         links.each do |link|
-          data_by_project_id[link.project_id].add_link(link)
+          results_by_project_id[link.project_id].add_link(link)
         end
       end
     end
   end
 
+  def validate
+    # validate uniqueness of name
+    if id
+      # update existing filter
+      count = MeasureFilter.count('id', :conditions => ['name=? and user_id=? and id<>?', name, user_id, id])
+    else
+      # new filter
+      count = MeasureFilter.count('id', :conditions => ['name=? and user_id=?', name, user_id])
+    end
+    errors.add_to_base('Name already exists') if count>0
+
+    if shared
+      count = MeasureFilter.count('id', :conditions => ['name=? and shared=? and user_id!=?', name, true, user_id])
+      errors.add_to_base('Other users already shared filters with the same name') if count>0
+    end
+  end
 end
\ No newline at end of file
index 08618b572d37007002d3635404e0b4f8aecda305..ea4fdeb8030d770734dfd6294c72707890eda3fa 100644 (file)
@@ -30,6 +30,7 @@ class User < ActiveRecord::Base
   has_many :filters, :dependent => :destroy
   has_many :active_dashboards, :dependent => :destroy, :order => 'order_index'
   has_many :dashboards, :dependent => :destroy
+  has_many :measure_filters, :class_name => 'MeasureFilter', :dependent => :destroy
 
   include Authentication
   include Authentication::ByPassword
index a6b58412fc64b36ae30e0c145e9a61cea9ce8a29..2d734b4b0d71265365029252bbbba9c764af6e01 100644 (file)
@@ -72,7 +72,7 @@ class WidgetProperty < ActiveRecord::Base
   protected
   def validate
     errors.add_to_base("Unknown property: #{key}") unless java_definition
-    PropertyType::validate(key, type, java_definition.optional(), text_value, errors);
+    PropertyType::validate(key, type, java_definition.optional(), text_value, errors)
   end
 
 end
index 15648dbf9e8c6599cfd21fc57aefc2e30844ebd2..ee642f3c0c90d9f04299bbc4e7f8614cb6693753 100644 (file)
@@ -1,43 +1,33 @@
 <%
    display_favourites = logged_in?
-   colspan = @filter.columns.size
+   colspan = @filter.display.columns.size
    colspan += 1 if display_favourites
 %>
 <table class="data">
   <thead>
   <tr>
-    <% if display_favourites %><th class="thin"></th><% end %>
-    <% @filter.columns.each do |column| %>
-      <%= list_column_title(@filter, column, url_params) -%>
+    <% if display_favourites %>
+      <th class="thin"></th>
+    <% end %>
+    <% @filter.display.columns.each do |column| %>
+      <%= list_column_html(@filter, column) -%>
     <% end %>
   </tr>
   </thead>
   <tbody>
-  <% @filter.data.each do |data| %>
+  <% @filter.results.each do |result| %>
     <tr class="<%= cycle 'even', 'odd' -%>">
-      <% if display_favourites %><td class="thin"><%= link_to_favourite(data.snapshot.resource) -%></td><% end %>
-      <% @filter.columns.each do |column| %>
-        <% if column.metric %>
-          <td class="right">
-            <%= format_measure(data.measure(column.metric)) -%>
-          </td>
-        <% elsif column.key=='name' %>
-          <td class="left">
-            <%= qualifier_icon(data.snapshot) %> <%= link_to_resource(data.snapshot.resource, h(data.snapshot.resource.name(true)), {:title => data.snapshot.resource.key}) -%>
-          </td>
-        <% elsif column.key=='date' %>
-          <td class="right">
-            <%= human_short_date(data.snapshot.created_at) -%>
-          </td>
-        <% elsif column.key=='key' %>
-          <td class="left"><span class="small"><%= data.snapshot.resource.kee -%></span></td>
-        <% else %>
-          <td></td>
-        <% end %>
+      <% if display_favourites %>
+        <td class="thin"><%= link_to_favourite(result.snapshot.resource) -%></td>
+      <% end %>
+      <% @filter.display.columns.each do |column| %>
+        <td class="<%= column.align -%>" >
+          <%= list_cell_html(column, result) -%>
+        </td>
       <% end %>
     </tr>
   <% end %>
-  <% if @filter.data.empty? %>
+  <% if @filter.results.empty? %>
     <tr class="even">
       <td colspan="<%= colspan -%>"><%= message 'no_data' -%></td>
     </tr>
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_edit_form.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_edit_form.html.erb
new file mode 100644 (file)
index 0000000..1708e95
--- /dev/null
@@ -0,0 +1,33 @@
+<form id="edit-filter-form" method="post" action="<%= ApplicationController.root_context -%>/measures/edit">
+  <input type="hidden" name="id" value="<%= @filter.id -%>">
+  <fieldset>
+    <div class="form-head">
+      <h2>Edit Filter</h2>
+    </div>
+    <div class="form-body">
+      <% @filter.errors.each do |attr, msg| %>
+        <p class="error"><%= h msg -%></p>
+      <% end %>
+      <div class="form-field">
+        <label for="name">Name <em class="mandatory">*</em></label>
+        <input id="name" name="name" type="text" size="50" maxlength="100" value="<%= h @filter.name -%>"/>
+      </div>
+      <div class="form-field">
+        <label for="description">Description</label>
+        <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= h @filter.description -%>"/>
+      </div>
+      <div class="form-field">
+        <label for="shared">Shared with all users</label>
+        <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if @filter.shared -%>/>
+      </div>
+    </div>
+    <div class="form-foot">
+      <input type="submit" value="<%= h message('save') -%>" id="save-submit"/>
+      <a href="#" onclick="return closeModalWindow()" id="save-cancel"><%= h message('cancel') -%></a>
+    </div>
+  </fieldset>
+</form>
+<script>
+  $j("#edit-filter-form").modalForm();
+  $j('#name').focus();
+</script>
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_save_form.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/_save_form.html.erb
new file mode 100644 (file)
index 0000000..0390f57
--- /dev/null
@@ -0,0 +1,37 @@
+<form id="save-filter-form" method="post" action="<%= ApplicationController.root_context -%>/measures/save">
+  <input type="hidden" name="id" value="<%= @filter.id -%>">
+  <input type="hidden" name="data" value="<%= u(@filter.data) -%>">
+  <fieldset>
+    <div class="form-head">
+      <h2>Save Filter</h2>
+    </div>
+
+    <div class="form-body">
+      <% @filter.errors.each do |attr, msg| %>
+        <p class="error"><%= h msg -%></p>
+      <% end %>
+      <div class="form-field">
+        <label for="name">Name <em class="mandatory">*</em></label>
+        <input id="name" name="name" type="text" size="50" maxlength="100" value="<%= h @filter.name -%>"/>
+      </div>
+      <div class="form-field">
+        <label for="description">Description</label>
+        <input id="description" name="description" type="text" size="50" maxlength="4000" value="<%= h @filter.description -%>"/>
+      </div>
+      <div class="form-field">
+        <label for="shared">Shared with all users</label>
+        <input id="shared" name="shared" type="checkbox" value="true" <%= 'checked' if @filter.shared -%>/>
+      </div>
+    </div>
+    <div class="form-foot">
+      <input type="submit" value="<%= h message('save') -%>" id="save-submit"/>
+      <a href="#" onclick="return closeModalWindow()" id="save-cancel"><%= h message('cancel') -%></a>
+    </div>
+  </fieldset>
+</form>
+<script>
+  $j("#save-filter-form").modalForm({success:function (data) {
+    window.location = baseUrl + '/measures/filter/' + data;
+  }});
+  $j('#name').focus();
+</script>
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/measures/manage.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/measures/manage.html.erb
new file mode 100644 (file)
index 0000000..33f46b0
--- /dev/null
@@ -0,0 +1,47 @@
+<div class="admin_page">
+  <table class="data" id="actives">
+    <thead>
+    <tr>
+      <th><%= message('name') -%></th>
+      <th><%= message('sharing') -%></th>
+      <th class="right"><%= message('operations') -%></th>
+    </tr>
+    </thead>
+    <tbody>
+    <% if current_user.measure_filters.empty? %>
+      <tr class="even">
+        <td colspan="3"><%= message('filters.no_filters') -%></td>
+      </tr>
+    <% else %>
+      <% current_user.measure_filters.each do |filter| %>
+        <tr id="active-<%= u filter.name -%>" class="<%= cycle('even', 'odd', :name => 'actives') -%>">
+          <td>
+            <%= link_to h(filter.name), :action => 'filter', :id => filter.id -%>
+            <% if filter.description %>
+              <div><%= h filter.description -%></div>
+            <% end %>
+          </td>
+          <td>
+            <% if filter.shared %>
+              Shared with all users
+            <% else %>
+              Private
+            <% end %>
+          </td>
+          <td class="thin nowrap right">
+            <a id="edit_<%= filter.name.parameterize -%>" href="<%= ApplicationController.root_context -%>/measures/edit_form/<%= filter.id -%>" class="link-action open-modal"><%= message('edit') -%></a>
+            &nbsp;
+            <%= link_to_action message('delete'), "#{ApplicationController.root_context}/measures/delete/#{filter.id}",
+                                             :class => 'link-action link-red',
+                                             :id => "delete_#{filter.name.parameterize}",
+                                             :confirm_button => message('delete'),
+                                             :confirm_title => 'measure_filters.delete_confirm_title',
+                                             :confirm_msg => 'measure_filters.are_you_sure_want_delete_filter_x',
+                                             :confirm_msg_params => [filter.name] -%>
+          </td>
+        </tr>
+      <% end %>
+    <% end %>
+    </tbody>
+  </table>
+</div>
index 09750fc6be3ccde1c13d688e929ff3578f71a51c..4905680ff25ebbca0aa2523ac34684f17bae6b1c 100644 (file)
   </style>
 <% end %>
 
-<% url_params = params.reject { |k, v| v.nil? || v=='' || k.starts_with?('_') }.merge({:controller => 'measures', :action => 'search'}) %>
-
 <div id="measure-filter">
   <div id="filter-form">
-    <form method="POST" action="<%= ApplicationController.root_context -%>/measures/search">
+    <form method="GET" action="<%= ApplicationController.root_context -%>/measures/search">
+      <% if @filter.id %>
+        <input type="hidden" name="id" value="<%= @filter.id -%>">
+      <% end %>
       <table>
         <tbody>
         <tr>
           <td>
             Base:<br>
-            <input type="text" name="base">
+            <input type="text" name="base" value="<%= @filter.criteria['base'] -%>">
           </td>
         </tr>
         <tr>
           <td>
-            Direct children:<br>
-            <%= check_box_tag 'onBaseChildren', 'true', params['onBaseChildren']=='true' -%>
+            On components:<br>
+            <%= check_box_tag 'onBaseComponents', 'true', @filter.criteria['onBaseComponents'] -%>
           </td>
         </tr>
         <tr>
           <td>
             Qualifiers:<br>
-            <%= select_tag 'qualifiers[]', options_for_select([['Any', ''], ['Project', 'TRK'], ['Sub-project', 'BRC'], ['Directory/Package', 'DIR']], params['qualifiers']||''), :size => 5, :multiple => true -%>
+            <%= select_tag 'qualifiers[]', options_for_select([['Any', ''], ['Project', 'TRK'], ['Sub-project', 'BRC'], ['Directory/Package', 'PAC'], ['File', 'CLA']], @filter.criteria['qualifiers']||''), :size => 5, :multiple => true -%>
           </td>
         </tr>
         <tr>
           <td>
             Language:<br>
-            <select name="languages[]" multiple size="5">
-              <option value="" <%= 'selected' if params['languages[]'].blank? -%>>Any</option>
-              <% Api::Utils.languages.each do |language| %>
-                <option value="<%= language.key.parameterize -%>"><%= h language.name -%></option>
-              <% end %>
-            </select>
+            <% languages = [['Any', '']].concat(Api::Utils.languages.map { |lang| [lang.name, lang.key] }) %>
+            <%= select_tag 'languages[]', options_for_select(languages, @filter.criteria['languages']||''), :size => 5, :multiple => true -%>
           </td>
         </tr>
         <tr>
@@ -69,7 +66,7 @@
         <tr>
           <td>
             Favourites only:<br>
-            <%= check_box_tag 'favourites', 'true', params['favourites']=='true' %>
+            <%= check_box_tag 'onFavourites', 'true', params['onFavourites']=='true' %>
           </td>
         </tr>
         <tr>
         </tr>
         <tr>
           <td>
-            <input type="submit" name="_search" value="Search">
+            <input type="submit" name="search" value="Search">
             <a href="<%= ApplicationController.root_context -%>/measures">Reset</a>
           </td>
         </tr>
         </tbody>
       </table>
     </form>
+
+    <br/>
+
+    <% if logged_in? %>
+      <ul id="my-filters">
+        <% current_user.measure_filters.each do |my_filter| %>
+          <li>
+            <a href="<%= ApplicationController.root_context -%>/measures/filter/<%= my_filter.id -%>"><%= h my_filter.name -%></a>
+          </li>
+        <% end %>
+      </ul>
+    <% end %>
   </div>
-  <% if @filter %>
+
+  <% if @filter.results %>
     <div id="filter-result">
+      <% if @filter.name %>
+        <h2><%= h @filter.name -%></h2>
+        <% if @filter.description %>
+          <p><%= h @filter.description -%></p>
+        <% end %>
+      <% end %>
+
       Display as:
-      <% MeasureFilter::DISPLAYS.each do |display| %>
-        <%= link_to_if display.key!=@filter.display.key, display.key, url_params.merge(:display => display.key) -%>
+      <% MeasureFilter::DISPLAYS.each do |display_class| %>
+        <%= link_to_if display_class::KEY!=@filter.display.class::KEY, display_class::KEY, params.merge(:action => 'search', :display => display_class::KEY, :id => nil) -%>
       <% end %>
 
-      <%= render :partial => "measures/display_#{@filter.display.key}", :locals => {:url_params => url_params} -%>
-      <p>
-        <% permalink = url_for(url_params) %>
-        <a href="<%= permalink -%>">Permalink</a>: <input type="text" value="<%= permalink -%>" size="100">
-      </p>
+      <% if logged_in? && (@filter.user_id==nil || @filter.user_id==current_user.id) %>
+        <a id="save_as" href="<%= url_for params.merge({:action => 'save_form', :id => @filter.id}) -%>" class="link-action open-modal"><%= message('save') -%></a>
+      <% end %>
+
+      <%= render :partial => "measures/display_#{@filter.display.class::KEY}" -%>
+      <br>
+
       <% if @filter.security_exclusions %>
         <p class="notes"><%= message('results_not_display_due_to_security') -%></p>
       <% end %>
 
     </div>
   <% end %>
+  </p>
 
 </div>
\ No newline at end of file
index 4a50f1103aa5ff232e48e0d59873bcc1dbc40591..30f5210435a97b1027e1b60039e5eb781f6ce6fe 100644 (file)
@@ -26,7 +26,7 @@ class CreateMeasureFilters < ActiveRecord::Migration
     create_table 'measure_filters' do |t|
       t.column 'name', :string, :null => false, :limit => 100
       t.column 'user_id', :integer, :null => true
-      t.column 'shared', :boolean, :null => true
+      t.column 'shared', :boolean, :default => false, :null => false
       t.column 'description', :string, :null => true, :limit => 4000
       t.column 'data', :text, :null => true
       t.timestamps
index adb76a2821a7410052df9aca7b7eb7d6c7730338..579609ebdcdb74d24c2e24e884d42aac085f8e74 100644 (file)
@@ -287,10 +287,10 @@ Treemap.prototype.onLoaded = function (componentsSize) {
                 });
             $dialog.dialog("open");
           }).error(function () {
-            alert("Server error. Please contact your administrator.");
-          }).complete(function() {
-            $dialog.removeClass('ui-widget-overlay');
-          });
+                alert("Server error. Please contact your administrator.");
+              }).complete(function () {
+                $dialog.removeClass('ui-widget-overlay');
+              });
 
           $link.click(function () {
             $dialog.dialog('open');
@@ -301,22 +301,22 @@ Treemap.prototype.onLoaded = function (componentsSize) {
         });
       });
     },
-    modalForm:function () {
+    modalForm:function (ajax_options) {
       return this.each(function () {
         var obj = $j(this);
         obj.submit(function (event) {
           $j('input[type=submit]', this).attr('disabled', 'disabled');
-          $j.ajax({
+          $j.ajax($j.extend({
             type:'POST',
             url:obj.attr('action'),
             data:obj.serialize(),
             success:function (data) {
-              location.reload();
+              window.location.reload();
             },
             error:function (xhr, textStatus, errorThrown) {
               $j("#modal").html(xhr.responseText);
             }
-          });
+          }, ajax_options));
           return false;
         });
       });