summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2019-10-19 11:42:20 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2019-10-19 11:42:20 +0000
commit45f039187df03e8925cb0d08ccc1838c9ea2d5c2 (patch)
tree3bbb8958c71b24e65123c0e8f29d6f15726424d5
parentecd9d9ba2f43f2f5a88c1d34d34d07dae40dfe4e (diff)
downloadredmine-45f039187df03e8925cb0d08ccc1838c9ea2d5c2.tar.gz
redmine-45f039187df03e8925cb0d08ccc1838c9ea2d5c2.zip
Option to switch between table list and board list (#29482).
Patch by Marius BALTEANU. git-svn-id: http://svn.redmine.org/redmine/trunk@18765 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/helpers/projects_helper.rb9
-rw-r--r--app/helpers/projects_queries_helper.rb61
-rw-r--r--app/helpers/queries_helper.rb8
-rw-r--r--app/models/project_query.rb29
-rw-r--r--app/models/query.rb16
-rw-r--r--app/views/projects/_board.html.erb3
-rw-r--r--app/views/projects/_list.html.erb35
-rw-r--r--app/views/projects/index.html.erb7
-rw-r--r--app/views/queries/_form.html.erb21
-rw-r--r--app/views/queries/_query_form.html.erb25
-rw-r--r--config/locales/en.yml3
-rw-r--r--public/stylesheets/application.css35
-rw-r--r--test/functional/projects_controller_test.rb53
-rw-r--r--test/unit/project_query_test.rb11
15 files changed, 294 insertions, 33 deletions
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e7a79e495..b9bda6b43 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -34,6 +34,8 @@ class ProjectsController < ApplicationController
helper :issues
helper :queries
include QueriesHelper
+ helper :projects_queries
+ include ProjectsQueriesHelper
helper :repositories
helper :members
helper :trackers
@@ -50,7 +52,9 @@ class ProjectsController < ApplicationController
respond_to do |format|
format.html {
- @projects = scope.to_a
+ @entry_count = scope.count
+ @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
+ @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
}
format.api {
@offset, @limit = api_offset_and_limit
@@ -61,6 +65,11 @@ class ProjectsController < ApplicationController
projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
}
+ format.csv {
+ # Export all entries
+ @entries = scope.to_a
+ send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'projects.csv')
+ }
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index bdb1ac44c..335db8b0b 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -158,4 +158,13 @@ module ProjectsHelper
url = bookmark_project_url(project)
link_to text, url, remote: true, method: method, class: css
end
+
+ def grouped_project_list(projects, query, &block)
+ ancestors = []
+ grouped_query_results(projects, query) do |project, group_name, group_count, group_totals|
+ ancestors.pop while ancestors.any? && !project.is_descendant_of?(ancestors.last)
+ yield project, ancestors.size, group_name, group_count, group_totals
+ ancestors << project unless project.leaf?
+ end
+ end
end
diff --git a/app/helpers/projects_queries_helper.rb b/app/helpers/projects_queries_helper.rb
new file mode 100644
index 000000000..b5de291c0
--- /dev/null
+++ b/app/helpers/projects_queries_helper.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2017 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 ProjectsQueriesHelper
+ include ApplicationHelper
+
+ def column_value(column, item, value)
+ if item.is_a?(Project)
+ case column.name
+ when :name
+ link_to_project(item) + (content_tag('span', '', :class => 'icon icon-user my-project', :title => l(:label_my_projects)) if User.current.member_of?(item))
+ when :short_description
+ item.description? ? content_tag('div', textilizable(item, :short_description), :class => "wiki") : ''
+ when :homepage
+ item.homepage? ? content_tag('div', textilizable(item, :homepage), :class => "wiki") : ''
+ when :status
+ get_project_status_label[column.value_object(item)]
+ when :parent_id
+ link_to_project(item.parent) unless item.parent.nil?
+ else
+ super
+ end
+ end
+ end
+
+ def csv_content(column, item)
+ if item.is_a?(Project)
+ case column.name
+ when :status
+ get_project_status_label[column.value_object(item)]
+ when :parent_id
+ return item.parent.name unless item.parent.nil?
+ end
+ end
+ super
+ end
+
+ private
+
+ def get_project_status_label
+ {
+ Project::STATUS_ACTIVE => l(:project_status_active),
+ Project::STATUS_CLOSED => l(:project_status_closed)
+ }
+ end
+end
diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb
index fa03a76ef..9ed2dd21d 100644
--- a/app/helpers/queries_helper.rb
+++ b/app/helpers/queries_helper.rb
@@ -120,6 +120,14 @@ module QueriesHelper
render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
end
+ def available_display_types_tags(query)
+ available_display_types = []
+ query.available_display_types.each do |t|
+ available_display_types << [l(:"label_display_type_#{t}"), t]
+ end
+ select_tag('display_type', options_for_select(available_display_types, @query.display_type), :id => 'display_type')
+ end
+
def grouped_query_results(items, query, &block)
result_count_by_group = query.result_count_by_group
previous_group, first = false, true
diff --git a/app/models/project_query.rb b/app/models/project_query.rb
index 2bc6b40e1..ecf3e687b 100644
--- a/app/models/project_query.rb
+++ b/app/models/project_query.rb
@@ -22,7 +22,16 @@ class ProjectQuery < Query
self.queried_class = Project
self.view_permission = :search_project
- self.available_columns = []
+ self.available_columns = [
+ QueryColumn.new(:name, :sortable => "#{Project.table_name}.name"),
+ QueryColumn.new(:status, :sortable => "#{Project.table_name}.status"),
+ QueryColumn.new(:short_description, :sortable => "#{Project.table_name}.description", :caption => :field_description),
+ QueryColumn.new(:homepage, :sortable => "#{Project.table_name}.homepage"),
+ QueryColumn.new(:identifier, :sortable => "#{Project.table_name}.identifier"),
+ QueryColumn.new(:parent_id, :sortable => "#{Project.table_name}.lft ASC", :default_order => 'desc', :caption => :field_parent),
+ QueryColumn.new(:is_public, :sortable => "#{Project.table_name}.is_public", :groupable => true),
+ QueryColumn.new(:created_on, :sortable => "#{Project.table_name}.created_on", :default_order => 'desc')
+ ]
def initialize(attributes=nil, *args)
super attributes
@@ -48,7 +57,23 @@ class ProjectQuery < Query
end
def available_columns
- []
+ return @available_columns if @available_columns
+ @available_columns = self.class.available_columns.dup
+ @available_columns += ProjectCustomField.visible.
+ map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) }
+ @available_columns
+ end
+
+ def available_display_types
+ ['board', 'list']
+ end
+
+ def default_columns_names
+ @default_columns_names ||= [:name, :identifier, :short_description]
+ end
+
+ def default_sort_criteria
+ [[]]
end
def base_scope
diff --git a/app/models/query.rb b/app/models/query.rb
index 7372f712e..19db899d8 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -409,6 +409,7 @@ class Query < ActiveRecord::Base
self.column_names = params[:c] || query_params[:column_names] || self.column_names
self.totalable_names = params[:t] || query_params[:totalable_names] || self.totalable_names
self.sort_criteria = params[:sort] || query_params[:sort_criteria] || self.sort_criteria
+ self.display_type = params[:display_type] || query_params[:display_type] || self.display_type
self
end
@@ -983,6 +984,21 @@ class Query < ActiveRecord::Base
end
end
+ def display_type
+ options[:display_type] || self.available_display_types.first
+ end
+
+ def display_type=(type)
+ unless type || self.available_display_types.include?(type)
+ type = self.available_display_types.first
+ end
+ options[:display_type] = type
+ end
+
+ def available_display_types
+ ['list']
+ end
+
private
def grouped_query(&block)
diff --git a/app/views/projects/_board.html.erb b/app/views/projects/_board.html.erb
new file mode 100644
index 000000000..7d63a533d
--- /dev/null
+++ b/app/views/projects/_board.html.erb
@@ -0,0 +1,3 @@
+<div id="projects-index">
+ <%= render_project_hierarchy(@entries) %>
+</div>
diff --git a/app/views/projects/_list.html.erb b/app/views/projects/_list.html.erb
new file mode 100644
index 000000000..8a1efc204
--- /dev/null
+++ b/app/views/projects/_list.html.erb
@@ -0,0 +1,35 @@
+<div class="autoscroll">
+<table class="list projects odd-even <%= @query.css_classes %>">
+<thead>
+ <tr>
+ <% @query.inline_columns.each do |column| %>
+ <%= column_header(@query, column) %>
+ <% end %>
+ </tr>
+</thead>
+<tbody>
+<% grouped_project_list(entries, @query) do |entry, level, group_name, group_count, group_totals| -%>
+ <% if group_name %>
+ <% reset_cycle %>
+ <tr class="group open">
+ <td colspan="<%= @query.inline_columns.size %>">
+ <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
+ <span class="name"><%= group_name %></span>
+ <% if group_count %>
+ <span class="count"><%= group_count %></span>
+ <% end %>
+ <span class="totals"><%= group_totals %></span>
+ <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
+ "toggleAllRowGroups(this)", :class => 'toggle-all') %>
+ </td>
+ </tr>
+ <% end %>
+ <tr id="project-<%= entry.id %>" class="<%= cycle('odd', 'even') %> <%= entry.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
+ <% @query.inline_columns.each do |column| %>
+ <%= content_tag('td', column_content(column, entry), :class => column.css_classes) %>
+ <% end %>
+ </tr>
+<% end -%>
+</tbody>
+</table>
+</div>
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index 3c2543b43..067c9bdee 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -11,12 +11,11 @@
<% end %>
<% if @query.valid? %>
- <% if @projects.empty? %>
+ <% if @entries.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
- <div id="projects-index">
- <%= render_project_hierarchy(@projects) %>
- </div>
+ <%= render :partial => @query.display_type, :locals => { :entries => @entries }%>
+ <span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
<% end %>
<% end %>
diff --git a/app/views/queries/_form.html.erb b/app/views/queries/_form.html.erb
index a2dd9f589..12a1d0202 100644
--- a/app/views/queries/_form.html.erb
+++ b/app/views/queries/_form.html.erb
@@ -29,12 +29,18 @@
<% end %>
<fieldset id="options"><legend><%= l(:label_options) %></legend>
-<p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
+ <% if @query.available_display_types.size > 1 %>
+ <p><label for='display_type'><%= l(:label_display_type) %></label>
+ <%= available_display_types_tags(@query) %>
+ </p>
+<% end %>
+
+<p id ="default_columns"><label for="query_default_columns"><%=l(:label_default_columns)%></label>
<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
:data => {:disables => "#columns, .block_columns input"} %></p>
<% unless params[:gantt] %>
- <p><label for="query_group_by"><%= l(:field_group_by) %></label>
+ <p id="group_by"><label id="group_by" for="query_group_by"><%= l(:field_group_by) %></label>
<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
<% unless @query.available_block_columns.empty? %>
@@ -99,4 +105,15 @@ $(document).ready(function(){
$("input.disable-unless-private").attr('disabled', !private_checked);
}).trigger('change');
});
+
+$(function ($) {
+ $('#display_type').change(function (e) {
+ var option = $(e.target).val()
+ if (option == 'board') {
+ $('fieldset#columns, fieldset#sort, p#default_columns, p#group_by').hide();
+ } else {
+ $('fieldset#columns, fieldset#sort, p#default_columns, p#group_by').show();
+ }
+ }).change()
+});
<% end %>
diff --git a/app/views/queries/_query_form.html.erb b/app/views/queries/_query_form.html.erb
index 65bcc3eb5..62d156684 100644
--- a/app/views/queries/_query_form.html.erb
+++ b/app/views/queries/_query_form.html.erb
@@ -14,8 +14,14 @@
<% if @query.available_columns.any? %>
<fieldset id="options" class="collapsible collapsed">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
- <div style="display: none;">
- <table>
+ <div class="hidden">
+ <% if @query.available_display_types.size > 1 %>
+ <div>
+ <span class="field"><label for='display_type'><%= l(:label_display_type) %></label></span>
+ <%= available_display_types_tags(@query) %>
+ </div>
+ <% end %>
+ <table id="list" class="<%= 'hidden' if (@query.display_type == 'board') %>">
<% if @query.available_columns.any? %>
<tr>
<td class="field"><%= l(:field_column_names) %></td>
@@ -65,3 +71,18 @@
</div>
<%= error_messages_for @query %>
+
+<%= javascript_tag do %>
+$(function ($) {
+ $('#display_type').change(function (e) {
+ var option = $(e.target).val()
+ if (option == 'board') {
+ $('table#list').hide();
+ } else {
+ $('table#list').show();
+ }
+
+ })
+});
+
+<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4c90deb9d..e5ae3220f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1072,6 +1072,9 @@ en:
label_password_char_class_lowercase: lowercase letters
label_password_char_class_digits: digits
label_password_char_class_special_chars: special characters
+ label_display_type: Display results as
+ label_display_type_list: List
+ label_display_type_board: Board
button_login: Login
button_submit: Submit
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index 7aa1bca54..0abd4fdae 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -130,6 +130,7 @@ div.modal .box p {margin: 0.3em 0;}
.clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
.mobile-show {display: none;}
+.hidden {display: none;}
/***** Links *****/
a, a:link, a:visited{ color: #169; text-decoration: none; }
@@ -235,7 +236,8 @@ table.list, .table-list { border: 1px solid #e4e4e4; width: 100%; margin-bottom:
table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
table.list td {text-align:center; vertical-align:middle; padding-right:10px;}
table.list td.id { width: 2%; text-align: center;}
-table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles, table.list td.attachments, table.list td.text {text-align: left;}
+table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles, table.list td.attachments, table.list td.text, table.list td.short_description {text-align: left;}
+
table.list td.attachments a {display:block;}
table.list td.tick {width:15%}
table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
@@ -257,17 +259,6 @@ tr.project td.name a { white-space:nowrap; }
tr.project.closed, tr.project.archived { color: #aaa; }
tr.project.closed a, tr.project.archived a { color: #aaa; }
-tr.project.idnt td.name span {background: url(../images/arrow_right.png) no-repeat 2px 50%; padding-left: 16px;}
-tr.project.idnt-1 td.name {padding-left: 0.5em;}
-tr.project.idnt-2 td.name {padding-left: 2em;}
-tr.project.idnt-3 td.name {padding-left: 3.5em;}
-tr.project.idnt-4 td.name {padding-left: 5em;}
-tr.project.idnt-5 td.name {padding-left: 6.5em;}
-tr.project.idnt-6 td.name {padding-left: 8em;}
-tr.project.idnt-7 td.name {padding-left: 9.5em;}
-tr.project.idnt-8 td.name {padding-left: 11em;}
-tr.project.idnt-9 td.name {padding-left: 12.5em;}
-
tr.issue { text-align: center; white-space: nowrap; }
tr.issue td.subject, tr.issue td.category, td.assigned_to, td.last_updated_by, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
tr.issue td.relations { text-align: left; }
@@ -277,16 +268,16 @@ table.issues td.block_column {color:#777; font-size:90%; padding:4px 4px 4px 24p
table.issues td.block_column span {font-weight: bold; display: block; margin-bottom: 4px;}
table.issues td.block_column pre {white-space:normal;}
-tr.issue.idnt td.subject {background: url(../images/arrow_right.png) no-repeat 2px 50%;}
-tr.issue.idnt-1 td.subject {padding-left: 24px; background-position: 8px 50%;}
-tr.issue.idnt-2 td.subject {padding-left: 40px; background-position: 24px 50%;}
-tr.issue.idnt-3 td.subject {padding-left: 56px; background-position: 40px 50%;}
-tr.issue.idnt-4 td.subject {padding-left: 72px; background-position: 56px 50%;}
-tr.issue.idnt-5 td.subject {padding-left: 88px; background-position: 72px 50%;}
-tr.issue.idnt-6 td.subject {padding-left: 104px; background-position: 88px 50%;}
-tr.issue.idnt-7 td.subject {padding-left: 120px; background-position: 104px 50%;}
-tr.issue.idnt-8 td.subject {padding-left: 136px; background-position: 120px 50%;}
-tr.issue.idnt-9 td.subject {padding-left: 152px; background-position: 136px 50%;}
+tr.issue.idnt td.subject, tr.project.idnt td.name {background: url(../images/arrow_right.png) no-repeat 2px 50%;}
+tr.issue.idnt-1 td.subject, tr.project.idnt-1 td.name {padding-left: 24px; background-position: 8px 50%;}
+tr.issue.idnt-2 td.subject, tr.project.idnt-2 td.name {padding-left: 40px; background-position: 24px 50%;}
+tr.issue.idnt-3 td.subject, tr.project.idnt-3 td.name {padding-left: 56px; background-position: 40px 50%;}
+tr.issue.idnt-4 td.subject, tr.project.idnt-4 td.name {padding-left: 72px; background-position: 56px 50%;}
+tr.issue.idnt-5 td.subject, tr.project.idnt-5 td.name {padding-left: 88px; background-position: 72px 50%;}
+tr.issue.idnt-6 td.subject, tr.project.idnt-6 td.name {padding-left: 104px; background-position: 88px 50%;}
+tr.issue.idnt-7 td.subject, tr.project.idnt-7 td.name {padding-left: 120px; background-position: 104px 50%;}
+tr.issue.idnt-8 td.subject, tr.project.idnt-8 td.name {padding-left: 136px; background-position: 120px 50%;}
+tr.issue.idnt-9 td.subject, tr.project.idnt-9 td.name {padding-left: 152px; background-position: 136px 50%;}
table.issue-report {table-layout:fixed;}
.issue-report-graph {width: 75%; margin: 2em 0;}
diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb
index 3f4dd6967..5e1efd240 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -94,6 +94,59 @@ class ProjectsControllerTest < Redmine::ControllerTest
end
end
+ def test_index_as_list_should_format_column_value
+ get :index, :params => {
+ :c => ['name', 'status', 'short_description', 'homepage', 'parent_id', 'identifier', 'is_public', 'created_on', 'project.cf_3'],
+ :display_type => 'list'
+ }
+ assert_response :success
+
+ assert_select 'table.projects' do
+ assert_select 'tr[id=?]', 'project-1' do
+ assert_select 'td.name a[href=?]', '/projects/ecookbook', :text => 'eCookbook'
+ assert_select 'td.status', :text => 'active'
+ assert_select 'td.short_description', :text => 'Recipes management application'
+ assert_select 'td.homepage a.external', :text => 'http://ecookbook.somenet.foo/'
+ assert_select 'td.identifier', :text => 'ecookbook'
+ assert_select 'td.is_public', :text => 'Yes'
+ assert_select 'td.created_on', :text => '07/19/2006 05:13 PM'
+ assert_select 'td.project_cf_3.list', :text => 'Stable'
+ end
+ assert_select 'tr[id=?]', 'project-4' do
+ assert_select 'td.parent_id a[href=?]', '/projects/ecookbook', :text => 'eCookbook'
+ end
+ end
+ end
+
+ def test_index_as_list_should_show_my_favourite_projects
+ @request.session[:user_id] = 1
+ get :index, :params => {
+ :display_type => 'list'
+ }
+
+ assert_response :success
+ assert_select 'tr[id=?] td.name span[class=?]', 'project-5', 'icon icon-user my-project'
+ end
+
+ def test_index_as_list_should_indent_projects
+ @request.session[:user_id] = 1
+ get :index, :params => {
+ :c => ['name', 'short_description'],
+ :sort => 'parent_id:desc,lft:desc',
+ :display_type => 'list'
+ }
+ assert_response :success
+
+ child_level1 = css_select('tr#project-5').map {|e| e.attr('class')}.first.split(' ')
+ child_level2 = css_select('tr#project-6').map {|e| e.attr('class')}.first.split(' ')
+
+ assert_include 'idnt', child_level1
+ assert_include 'idnt-1', child_level1
+
+ assert_include 'idnt', child_level2
+ assert_include 'idnt-2', child_level2
+ end
+
def test_autocomplete_js
get :autocomplete, :params => {
:format => 'js',
diff --git a/test/unit/project_query_test.rb b/test/unit/project_query_test.rb
index f4182e560..43e95d308 100644
--- a/test/unit/project_query_test.rb
+++ b/test/unit/project_query_test.rb
@@ -44,6 +44,17 @@ class ProjectQueryTest < ActiveSupport::TestCase
values = query.available_filters['status'][:values]
assert_equal ['active', 'closed'], values.map(&:first)
assert_equal ['1', '5'], values.map(&:second)
+ end
+
+ def test_default_columns
+ q = ProjectQuery.new
+ assert q.columns.any?
+ assert q.inline_columns.any?
+ assert q.block_columns.empty?
+ end
+ def test_available_columns_should_include_project_custom_fields
+ query = ProjectQuery.new
+ assert_include :"project.cf_3", query.available_columns.map(&:name)
end
end