From c7c8dc71f2fc8f224deb0a5340f7bbb8906e584d Mon Sep 17 00:00:00 2001
From: Jean-Philippe Lang <jp_lang@yahoo.fr>
Date: Thu, 12 Mar 2009 18:06:54 +0000
Subject: [PATCH] Ability to save "sort order" in custom queries (#2899).

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2572 e93f8b46-1217-0410-a6f0-8f06a7374b81
---
 app/controllers/issues_controller.rb          |  3 +-
 app/helpers/sort_helper.rb                    | 57 +++++++++++++------
 app/models/query.rb                           | 27 +++++++++
 app/views/queries/_form.rhtml                 |  9 +++
 config/locales/bg.yml                         |  3 +
 config/locales/ca.yml                         |  3 +
 config/locales/cs.yml                         |  3 +
 config/locales/da.yml                         |  3 +
 config/locales/de.yml                         |  3 +
 config/locales/en.yml                         |  3 +
 config/locales/es.yml                         |  3 +
 config/locales/fi.yml                         |  3 +
 config/locales/fr.yml                         |  3 +
 config/locales/gl.yml                         |  3 +
 config/locales/he.yml                         |  3 +
 config/locales/hu.yml                         |  3 +
 config/locales/it.yml                         |  3 +
 config/locales/ja.yml                         |  3 +
 config/locales/ko.yml                         |  3 +
 config/locales/lt.yml                         |  3 +
 config/locales/nl.yml                         |  3 +
 config/locales/no.yml                         |  3 +
 config/locales/pl.yml                         |  3 +
 config/locales/pt-BR.yml                      |  3 +
 config/locales/pt.yml                         |  3 +
 config/locales/ro.yml                         |  3 +
 config/locales/ru.yml                         |  3 +
 config/locales/sk.yml                         |  3 +
 config/locales/sl.yml                         |  3 +
 config/locales/sr.yml                         |  3 +
 config/locales/sv.yml                         |  3 +
 config/locales/th.yml                         |  3 +
 config/locales/tr.yml                         |  3 +
 config/locales/uk.yml                         |  3 +
 config/locales/vi.yml                         |  3 +
 config/locales/zh-TW.yml                      |  3 +
 config/locales/zh.yml                         |  3 +
 ...0090312172426_add_queries_sort_criteria.rb |  9 +++
 test/fixtures/queries.yml                     | 20 +++++++
 test/functional/queries_controller_test.rb    | 29 ++++++++++
 test/unit/query_test.rb                       | 25 ++++++++
 41 files changed, 259 insertions(+), 19 deletions(-)
 create mode 100644 db/migrate/20090312172426_add_queries_sort_criteria.rb

diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index 0af5f192a..929b928ac 100644
--- a/app/controllers/issues_controller.rb
+++ b/app/controllers/issues_controller.rb
@@ -45,7 +45,7 @@ class IssuesController < ApplicationController
 
   def index
     retrieve_query
-    sort_init 'id', 'desc'
+    sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
     sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
     
     if @query.valid?
@@ -471,6 +471,7 @@ private
       @query = Query.find(params[:query_id], :conditions => cond)
       @query.project = @project
       session[:query] = {:id => @query.id, :project_id => @query.project_id}
+      sort_clear
     else
       if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
         # Give it a name, required to be valid
diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb
index d4d8f721a..7b7fbc8a3 100644
--- a/app/helpers/sort_helper.rb
+++ b/app/helpers/sort_helper.rb
@@ -69,6 +69,11 @@ module SortHelper
       normalize!
     end
     
+    def criteria=(arg)
+      @criteria = arg
+      normalize!
+    end
+    
     def to_param
       @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
     end
@@ -102,24 +107,42 @@ module SortHelper
       @criteria.first && @criteria.first.last
     end
     
+    def empty?
+      @criteria.empty?
+    end
+    
     private
     
     def normalize!
-      @criteria = @criteria.collect {|s| [s.first, (s.last == false || s.last == 'desc') ? false : true]}
+      @criteria ||= []
+      @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
       @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
       @criteria.slice!(3)
       self
     end
   end
+  
+  def sort_name
+    controller_name + '_' + action_name + '_sort'
+  end
 
-  # Initializes the default sort column (default_key) and sort order
-  # (default_order).
-  #
-  # - default_key is a column attribute name.
-  # - default_order is 'asc' or 'desc'.
+  # Initializes the default sort.
+  # Examples:
+  #   
+  #   sort_init 'name'
+  #   sort_init 'id', 'desc'
+  #   sort_init ['name', ['id', 'desc']]
+  #   sort_init [['name', 'desc'], ['id', 'desc']]
   #
-  def sort_init(default_key, default_order='asc')
-    @sort_default = "#{default_key}:#{default_order}"
+  def sort_init(*args)
+    case args.size
+    when 1
+      @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
+    when 2
+      @sort_default = [[args.first, args.last]]
+    else
+      raise ArgumentError
+    end
   end
 
   # Updates the sort state. Call this in the controller prior to calling
@@ -127,13 +150,18 @@ module SortHelper
   # - criteria can be either an array or a hash of allowed keys
   #
   def sort_update(criteria)
-    sort_name = controller_name + '_' + action_name + '_sort'
-    
     @sort_criteria = SortCriteria.new
     @sort_criteria.available_criteria = criteria
-    @sort_criteria.from_param(params[:sort] || session[sort_name] || @sort_default)
+    @sort_criteria.from_param(params[:sort] || session[sort_name])
+    @sort_criteria.criteria = @sort_default if @sort_criteria.empty?
     session[sort_name] = @sort_criteria.to_param
   end
+  
+  # Clears the sort criteria session data
+  #
+  def sort_clear
+    session[sort_name] = nil
+  end
 
   # Returns an SQL sort clause corresponding to the current sort state.
   # Use this to sort the controller's table items collection.
@@ -188,13 +216,6 @@ module SortHelper
   #
   #   <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
   #
-  # Renders:
-  #
-  #   <th title="Sort by contact ID" width="40">
-  #     <a href="/contact/list?sort_order=desc&amp;sort_key=id">Id</a>
-  #     &nbsp;&nbsp;<img alt="Sort_asc" src="/images/sort_asc.png" />
-  #   </th>
-  #
   def sort_header_tag(column, options = {})
     caption = options.delete(:caption) || column.to_s.humanize
     default_order = options.delete(:default_order) || 'asc'
diff --git a/app/models/query.rb b/app/models/query.rb
index 5c9fad5e5..99ebb757e 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -28,6 +28,11 @@ class QueryColumn
   def caption
     l("field_#{name}")
   end
+  
+  # Returns true if the column is sortable, otherwise false
+  def sortable?
+    !sortable.nil?
+  end
 end
 
 class QueryCustomFieldColumn < QueryColumn
@@ -52,6 +57,7 @@ class Query < ActiveRecord::Base
   belongs_to :user
   serialize :filters
   serialize :column_names
+  serialize :sort_criteria, Array
   
   attr_protected :project_id, :user_id
   
@@ -261,6 +267,27 @@ class Query < ActiveRecord::Base
     column_names.nil? || column_names.empty?
   end
   
+  def sort_criteria=(arg)
+    c = []
+    if arg.is_a?(Hash)
+      arg = arg.keys.sort.collect {|k| arg[k]}
+    end
+    c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
+    write_attribute(:sort_criteria, c)
+  end
+  
+  def sort_criteria
+    read_attribute(:sort_criteria) || []
+  end
+  
+  def sort_criteria_key(arg)
+    sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
+  end
+  
+  def sort_criteria_order(arg)
+    sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
+  end
+  
   def project_statement
     project_clauses = []
     if project && !@project.descendants.active.empty?
diff --git a/app/views/queries/_form.rhtml b/app/views/queries/_form.rhtml
index 8da264032..7c227a9f6 100644
--- a/app/views/queries/_form.rhtml
+++ b/app/views/queries/_form.rhtml
@@ -25,5 +25,14 @@
 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
 </fieldset>
 
+<fieldset><legend><%= l(:label_sort) %></legend>
+<% 3.times do |i| %>
+<%= i+1 %>: <%= select_tag("query[sort_criteria][#{i}][]",
+									options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i))) %>
+				    <%= select_tag("query[sort_criteria][#{i}][]",
+							    		  		options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i))) %><br />
+<% end %>
+</fieldset>
+
 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
 </div>
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 0a6bca015..0179cb261 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -778,3 +778,6 @@ bg:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 15cca4e0c..e62a936d6 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -779,3 +779,6 @@ ca:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 0795b0784..577758532 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -783,3 +783,6 @@ cs:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/da.yml b/config/locales/da.yml
index e27c4a055..76323520f 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -811,3 +811,6 @@ da:
   setting_per_page_options: Objects per page options
   mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 44329474b..95a6f6c38 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -810,3 +810,6 @@ de:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 47dcc0e4e..e0cce3cbe 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -662,6 +662,9 @@ en:
   label_issue_watchers: Watchers
   label_example: Example
   label_display: Display
+  label_sort: Sort
+  label_ascending: Ascending
+  label_descending: Descending
   
   button_login: Login
   button_submit: Submit
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 78f3bbd5f..259d29489 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -831,3 +831,6 @@ es:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index d9f3c03d0..1ae6bc75c 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -821,3 +821,6 @@ fi:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 9af234cc1..72a1023a0 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -694,6 +694,9 @@ fr:
   label_issue_watchers: Observateurs
   label_example: Exemple
   label_display: Affichage
+  label_sort: Tri
+  label_ascending: Croissant
+  label_descending: Décroissant
   
   button_login: Connexion
   button_submit: Soumettre
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index d15448885..65f37de85 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -810,3 +810,6 @@ gl:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 661fca904..e674cc160 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -793,3 +793,6 @@ he:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index a1456c3ae..d1fe5a045 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -816,3 +816,6 @@
   field_identity_url: OpenID URL
   label_login_with_open_id_option: bejelentkezés OpenID használatával
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/it.yml b/config/locales/it.yml
index f0c008f80..d10084ce2 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -796,3 +796,6 @@ it:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 3753bb3ec..c82074b0f 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -809,3 +809,6 @@ ja:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index c60744f94..30d04324f 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -840,3 +840,6 @@ ko:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 09bf3fe7a..a8bea412f 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -821,3 +821,6 @@ lt:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 644d6f931..89e9b8346 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -766,3 +766,6 @@ nl:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/no.yml b/config/locales/no.yml
index f0da35cea..1b670ad6e 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -783,3 +783,6 @@
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 595705d51..6f263aaf4 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -814,3 +814,6 @@ pl:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 861881dfc..daeeb9fd4 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -816,3 +816,6 @@ pt-BR:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: ou use o OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 4f248edd1..fb4447978 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -802,3 +802,6 @@ pt:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index 3e2214d67..30fbc3a94 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -823,3 +823,6 @@ ro:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index c9788c37c..fcb62b35e 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -909,3 +909,6 @@ ru:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index b0ff847a7..fb75161f1 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -782,3 +782,6 @@ sk:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index 98c8995e3..249e47fbb 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -780,3 +780,6 @@ sl:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 80a290678..a4b66a058 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -804,3 +804,6 @@
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 9b5adcd41..0ed8e8d22 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -838,3 +838,6 @@ sv:
   enumeration_doc_categories: Dokumentkategorier
   enumeration_activities: Aktiviteter (tidsuppföljning)
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 06d46c5a4..147c59bfc 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -781,3 +781,6 @@ th:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index d240cae3c..de4611940 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -817,3 +817,6 @@ tr:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 08ca568af..4eda51136 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -780,3 +780,6 @@ uk:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index 375f03b58..decba375f 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -850,3 +850,6 @@ vi:
   field_identity_url: OpenID URL
   label_login_with_open_id_option: or login with OpenID
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 1b9387b2b..f1cd072fe 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -888,3 +888,6 @@
   enumeration_doc_categories: 文件分類
   enumeration_activities: 活動 (時間追蹤)
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index dd7de569e..ee6671a27 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -813,3 +813,6 @@ zh:
   enumeration_doc_categories: 文档类别
   enumeration_activities: 活动(时间跟踪)
   field_content: Content
+  label_descending: Descending
+  label_sort: Sort
+  label_ascending: Ascending
diff --git a/db/migrate/20090312172426_add_queries_sort_criteria.rb b/db/migrate/20090312172426_add_queries_sort_criteria.rb
new file mode 100644
index 000000000..743ed42ff
--- /dev/null
+++ b/db/migrate/20090312172426_add_queries_sort_criteria.rb
@@ -0,0 +1,9 @@
+class AddQueriesSortCriteria < ActiveRecord::Migration
+  def self.up
+    add_column :queries, :sort_criteria, :text
+  end
+
+  def self.down
+    remove_column :queries, :sort_criteria
+  end
+end
diff --git a/test/fixtures/queries.yml b/test/fixtures/queries.yml
index f12022729..a274ce350 100644
--- a/test/fixtures/queries.yml
+++ b/test/fixtures/queries.yml
@@ -67,3 +67,23 @@ queries_004:
 
   user_id: 2
   column_names: 
+queries_005: 
+  id: 5
+  project_id: 
+  is_public: true
+  name: Open issues by priority and tracker
+  filters: |
+    status_id: 
+      :values: 
+      - "1"
+      :operator: o
+
+  user_id: 1
+  column_names: 
+  sort_criteria: |
+    --- 
+    - - priority
+      - desc
+    - - tracker
+      - asc
+  
\ No newline at end of file
diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb
index de08b4245..1951aff8f 100644
--- a/test/functional/queries_controller_test.rb
+++ b/test/functional/queries_controller_test.rb
@@ -111,6 +111,22 @@ class QueriesControllerTest < Test::Unit::TestCase
     assert q.valid?
   end
   
+  def test_new_with_sort
+    @request.session[:user_id] = 1
+    post :new,
+         :confirm => '1',
+         :default_columns => '1',
+         :operators => {"status_id" => "o"},
+         :values => {"status_id" => ["1"]},
+         :query => {:name => "test_new_with_sort",
+                    :is_public => "1", 
+                    :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
+    
+    query = Query.find_by_name("test_new_with_sort")
+    assert_not_nil query
+    assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
+  end
+  
   def test_get_edit_global_public_query
     @request.session[:user_id] = 1
     get :edit, :id => 4
@@ -202,6 +218,19 @@ class QueriesControllerTest < Test::Unit::TestCase
                                                  :disabled => 'disabled' }
   end
   
+  def test_get_edit_sort_criteria
+    @request.session[:user_id] = 1
+    get :edit, :id => 5
+    assert_response :success
+    assert_template 'edit'
+    assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
+                                 :child => { :tag => 'option', :attributes => { :value => 'priority',
+                                                                                :selected => 'selected' } }
+    assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
+                                 :child => { :tag => 'option', :attributes => { :value => 'desc',
+                                                                                :selected => 'selected' } }
+  end
+  
   def test_destroy
     @request.session[:user_id] = 2
     post :destroy, :id => 1
diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb
index d568604c3..80112b779 100644
--- a/test/unit/query_test.rb
+++ b/test/unit/query_test.rb
@@ -195,6 +195,31 @@ class QueryTest < Test::Unit::TestCase
     assert q.has_column?(c)
   end
   
+  def test_default_sort
+    q = Query.new
+    assert_equal [], q.sort_criteria
+  end
+  
+  def test_set_sort_criteria_with_hash
+    q = Query.new
+    q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
+    assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
+  end
+  
+  def test_set_sort_criteria_with_array
+    q = Query.new
+    q.sort_criteria = [['priority', 'desc'], 'tracker']
+    assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
+  end
+  
+  def test_create_query_with_sort
+    q = Query.new(:name => 'Sorted')
+    q.sort_criteria = [['priority', 'desc'], 'tracker']
+    assert q.save
+    q.reload
+    assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
+  end
+  
   def test_sort_by_string_custom_field_asc
     q = Query.new
     c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
-- 
GitLab