From e5f5671d66295100d719d4712a03015268d65e02 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 1 Oct 2007 08:44:17 +0000 Subject: [PATCH] Added the ability to customize columns of a saved query. git-svn-id: http://redmine.rubyforge.org/svn/trunk@782 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/queries_helper.rb | 41 ++++++++++++++++- app/models/enumeration.rb | 2 + app/models/issue_category.rb | 2 + app/models/issue_status.rb | 4 +- app/models/query.rb | 52 ++++++++++++++++++++++ app/models/tracker.rb | 2 + app/views/issues/_list.rhtml | 20 +++++++++ app/views/issues/index.rhtml | 28 +----------- app/views/projects/list_issues.rhtml | 27 +---------- app/views/queries/_form.rhtml | 7 +++ db/migrate/071_add_queries_column_names.rb | 9 ++++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fr.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/sv.yml | 1 + lang/zh.yml | 1 + public/stylesheets/application.css | 6 +++ test/unit/query_test.rb | 13 ++++++ 28 files changed, 174 insertions(+), 54 deletions(-) create mode 100644 app/views/issues/_list.rhtml create mode 100644 db/migrate/071_add_queries_column_names.rb diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 1c0b59570..786932855 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -1,6 +1,45 @@ -module QueriesHelper +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +module QueriesHelper + def operators_for_select(filter_type) Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]} end + + def column_header(column) + if column.sortable + sort_header_tag(column.sortable, :caption => l("field_#{column.name}")) + else + content_tag('th', l("field_#{column.name}")) + end + end + + def column_content(column, issue) + value = issue.send(column.name) + if value.is_a?(Date) + format_date(value) + elsif value.is_a?(Time) + format_time(value) + elsif column.name == :subject + ((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + + link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) + else + h(value) + end + end end diff --git a/app/models/enumeration.rb b/app/models/enumeration.rb index c4f0f98cb..46d350a21 100644 --- a/app/models/enumeration.rb +++ b/app/models/enumeration.rb @@ -37,6 +37,8 @@ class Enumeration < ActiveRecord::Base OPTIONS[self.opt] end + def to_s; name end + private def check_integrity case self.opt diff --git a/app/models/issue_category.rb b/app/models/issue_category.rb index b184c132f..9478504f1 100644 --- a/app/models/issue_category.rb +++ b/app/models/issue_category.rb @@ -34,4 +34,6 @@ class IssueCategory < ActiveRecord::Base end destroy_without_reassign end + + def to_s; name end end diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index 13ed27f6c..3929c6774 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -51,7 +51,9 @@ class IssueStatus < ActiveRecord::Base :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : [] end - + + def to_s; name end + private def check_integrity raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) diff --git a/app/models/query.rb b/app/models/query.rb index 11460c1cb..c3d9d56e3 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -15,10 +15,23 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +class QueryColumn + attr_accessor :name, :sortable, :default + + def initialize(name, options={}) + self.name = name + self.sortable = options[:sortable] + self.default = options[:default] + end + + def default?; default end +end + class Query < ActiveRecord::Base belongs_to :project belongs_to :user serialize :filters + serialize :column_names attr_protected :project, :user attr_accessor :executed_by @@ -59,6 +72,22 @@ class Query < ActiveRecord::Base cattr_reader :operators_by_filter_type + @@available_columns = [ + QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :default => true), + QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :default => true), + QueryColumn.new(:priority, :sortable => "#{Issue.table_name}.priority_id", :default => true), + QueryColumn.new(:subject, :default => true), + QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname", :default => true), + QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default => true), + QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), + QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), + QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), + QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), + QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"), + QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on"), + ] + cattr_reader :available_columns + def initialize(attributes = nil) super attributes self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } @@ -173,7 +202,30 @@ class Query < ActiveRecord::Base label = @available_filters[field][:name] if @available_filters.has_key?(field) label ||= field.gsub(/\_id$/, "") end + + def available_columns + cols = Query.available_columns + end + def columns + if column_names && !column_names.empty? + available_columns.select {|c| column_names.include?(c.name) } + else + # default columns + available_columns.select {|c| c.default? } + end + end + + def column_names=(names) + names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names + names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names + write_attribute(:column_names, names) + end + + def has_column?(column) + column_names && column_names.include?(column.name) + end + def statement # project/subprojects clause clause = '' diff --git a/app/models/tracker.rb b/app/models/tracker.rb index c024c0911..90ef31912 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -27,6 +27,8 @@ class Tracker < ActiveRecord::Base validates_length_of :name, :maximum => 30 validates_format_of :name, :with => /^[\w\s\'\-]*$/i + def to_s; name end + private def check_integrity raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml new file mode 100644 index 000000000..0af415946 --- /dev/null +++ b/app/views/issues/_list.rhtml @@ -0,0 +1,20 @@ + + + + <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %> + <% query.columns.each do |column| %> + <%= column_header(column) %> + <% end %> + + + <% issues.each do |issue| %> + + + + <% query.columns.each do |column| %> + <%= content_tag 'td', column_content(column, issue), :class => column.name %> + <% end %> + + <% end %> + +
<%= check_box_tag "issue_ids[]", issue.id, false, :id => "issue_#{issue.id}" %><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index 270f9b215..eb0835357 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -23,32 +23,8 @@

<%= l(:label_no_data) %>

<% else %>   - - - <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %> - <%= sort_header_tag("#{Project.table_name}.name", :caption => l(:field_project)) %> - <%= sort_header_tag("#{Issue.table_name}.tracker_id", :caption => l(:field_tracker)) %> - <%= sort_header_tag("#{IssueStatus.table_name}.name", :caption => l(:field_status)) %> - <%= sort_header_tag("#{Issue.table_name}.priority_id", :caption => l(:field_priority)) %> - - <%= sort_header_tag("#{User.table_name}.lastname", :caption => l(:field_assigned_to)) %> - <%= sort_header_tag("#{Issue.table_name}.updated_on", :caption => l(:field_updated_on)) %> - - - <% for issue in @issues %> - "> - - - - - - - - - - <% end %> - -
<%=l(:field_subject)%>
<%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %><%=h issue.project.name %><%= issue.tracker.name %><%= issue.status.name %><%= issue.priority.name %><%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %><%= issue.assigned_to.name if issue.assigned_to %><%= format_time(issue.updated_on) %>
+<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %> +

<%= pagination_links_full @issue_pages %> [ <%= @issue_pages.current.first_item %> - <%= @issue_pages.current.last_item %> / <%= @issue_count %> ]

<% end %> diff --git a/app/views/projects/list_issues.rhtml b/app/views/projects/list_issues.rhtml index e17e8bc37..3c90c30f9 100644 --- a/app/views/projects/list_issues.rhtml +++ b/app/views/projects/list_issues.rhtml @@ -45,32 +45,7 @@ <% else %>   <% form_tag({:controller => 'projects', :action => 'move_issues', :id => @project}, :id => 'issues_form' ) do %> - - - - <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %> - <%= sort_header_tag("#{Issue.table_name}.tracker_id", :caption => l(:field_tracker)) %> - <%= sort_header_tag("#{IssueStatus.table_name}.name", :caption => l(:field_status)) %> - <%= sort_header_tag("#{Issue.table_name}.priority_id", :caption => l(:field_priority)) %> - - <%= sort_header_tag("#{User.table_name}.lastname", :caption => l(:field_assigned_to)) %> - <%= sort_header_tag("#{Issue.table_name}.updated_on", :caption => l(:field_updated_on)) %> - - - <% for issue in @issues %> - "> - - - - - - - - - - <% end %> - -
<%=l(:field_subject)%>
<%= check_box_tag "issue_ids[]", issue.id, false, :id => "issue_#{issue.id}" %><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %><%= issue.tracker.name %><%= issue.status.name %><%= issue.priority.name %><%= "#{issue.project.name} - " unless @project && @project == issue.project %><%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %><%= issue.assigned_to.name if issue.assigned_to %><%= format_time(issue.updated_on) %>
+<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
<%= l(:label_export_to) %> <%= link_to 'CSV', {:action => 'export_issues_csv', :id => @project}, :class => 'icon icon-csv' %>, diff --git a/app/views/queries/_form.rhtml b/app/views/queries/_form.rhtml index 28b6479e5..b4a4987cd 100644 --- a/app/views/queries/_form.rhtml +++ b/app/views/queries/_form.rhtml @@ -9,6 +9,13 @@

<%= check_box 'query', 'is_public' %>

<% end %> + +

+<% @query.available_columns.each do |column| %> +<%= check_box_tag 'query[column_names][]', column.name, @query.has_column?(column) %> <%= l("field_#{column.name}") %>
+<% end %> +<%= hidden_field_tag 'query[column_names][]', '' %> +

<%= render :partial => 'queries/filters', :locals => {:query => query}%> diff --git a/db/migrate/071_add_queries_column_names.rb b/db/migrate/071_add_queries_column_names.rb new file mode 100644 index 000000000..acaf4dab0 --- /dev/null +++ b/db/migrate/071_add_queries_column_names.rb @@ -0,0 +1,9 @@ +class AddQueriesColumnNames < ActiveRecord::Migration + def self.up + add_column :queries, :column_names, :text + end + + def self.down + remove_column :queries, :column_names + end +end diff --git a/lang/bg.yml b/lang/bg.yml index c15abfea4..d576142d1 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -511,3 +511,4 @@ enumeration_doc_categories: Категории документи enumeration_activities: Дейности (time tracking) label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/cs.yml b/lang/cs.yml index 8a68d95b8..569002636 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -511,3 +511,4 @@ text_issue_category_destroy_assignments: Remove category assignments label_added_time_by: Added by %s %s ago field_estimated_hours: Estimated time label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/de.yml b/lang/de.yml index 2fb1b1e92..f2b5b069f 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -511,3 +511,4 @@ enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/en.yml b/lang/en.yml index dc818e37c..6e23f377f 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -159,6 +159,7 @@ field_delay: Delay field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time +field_column_names: Columns setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/lang/es.yml b/lang/es.yml index 09017909d..82b0908a7 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -511,3 +511,4 @@ enumeration_doc_categories: Categorías del documento enumeration_activities: Activities (time tracking) label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/fr.yml b/lang/fr.yml index 3f5371daa..68d329774 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -159,6 +159,7 @@ field_delay: Retard field_assignable: Demandes assignables à ce rôle field_redirect_existing_links: Rediriger les liens existants field_estimated_hours: Temps estimé +field_column_names: Colonnes setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application diff --git a/lang/it.yml b/lang/it.yml index 73aa3437b..54b008580 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -511,3 +511,4 @@ enumeration_doc_categories: Categorie di documenti enumeration_activities: Attività (time tracking) label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/ja.yml b/lang/ja.yml index 1e4977908..0e2628c60 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -512,3 +512,4 @@ enumeration_doc_categories: 文書カテゴリ enumeration_activities: 作業分類 (時間トラッキング) label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/nl.yml b/lang/nl.yml index 0c862ab30..916040747 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -512,3 +512,4 @@ enumeration_activities: Activiteiten (tijd tracking) text_comma_separated: Multiple values allowed (comma separated). label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/pl.yml b/lang/pl.yml index e5d380d1e..a61974afa 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -511,3 +511,4 @@ label_added_time_by: Dodane przez %s %s temu field_estimated_hours: Szacowany czas label_file_plural: Pliki label_changeset_plural: Zestawienia zmian +field_column_names: Columns diff --git a/lang/pt-br.yml b/lang/pt-br.yml index afcd4cb27..e8f34b6d7 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -511,3 +511,4 @@ enumeration_doc_categories: Categorias de documento enumeration_activities: Atividades (time tracking) label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/pt.yml b/lang/pt.yml index 2ce44f49b..f7cb5f91a 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -511,3 +511,4 @@ enumeration_doc_categories: Categorias de documento enumeration_activities: Atividades (time tracking) label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/ro.yml b/lang/ro.yml index 19d35c11d..d4cd69b37 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -511,3 +511,4 @@ label_index_by_date: Index by date label_index_by_title: Index by title label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/sv.yml b/lang/sv.yml index adcd28879..ed5106a13 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -512,3 +512,4 @@ enumeration_activities: Aktiviteter (tidsspårning) field_comments: Comment label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/lang/zh.yml b/lang/zh.yml index c3edc0308..ec286878d 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -514,3 +514,4 @@ enumeration_activities: Activities (time tracking) label_wiki_page: Wiki page label_file_plural: Files label_changeset_plural: Changesets +field_column_names: Columns diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 307eaa024..8a17600ef 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -133,6 +133,12 @@ margin*/ div.attachments p { margin:4px 0 2px 0; } +/***** Issue list ****/ +tr.issue { text-align: center; white-space: nowrap; } +tr.issue th.checkbox { width: 15px; } +tr.issue td.subject, tr.issue td.category { white-space: normal; } +tr.issue td.subject { text-align: left; } + /***** Flash & error messages ****/ #flash div, #errorExplanation, .nodata { padding: 4px 4px 4px 30px; diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index ea004e39e..c00f47e5d 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -28,4 +28,17 @@ class QueryTest < Test::Unit::TestCase assert_equal 1, issues.length assert_equal Issue.find(3), issues.first end + + def test_default_columns + q = Query.new + assert !q.columns.empty? + end + + def test_set_column_names + q = Query.new + q.column_names = ['tracker', :subject, '', 'unknonw_column'] + assert_equal [:tracker, :subject], q.columns.collect {|c| c.name} + c = q.columns.first + assert q.has_column?(c) + end end