##// END OF EJS Templates
Role based custom queries (#1019)....
Jean-Philippe Lang -
r11764:888c3581eb0f
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,13
1 class CreateQueriesRoles < ActiveRecord::Migration
2 def self.up
3 create_table :queries_roles, :id => false do |t|
4 t.column :query_id, :integer, :null => false
5 t.column :role_id, :integer, :null => false
6 end
7 add_index :queries_roles, [:query_id, :role_id], :unique => true, :name => :queries_roles_ids
8 end
9
10 def self.down
11 drop_table :queries_roles
12 end
13 end
@@ -0,0 +1,13
1 class AddQueriesVisibility < ActiveRecord::Migration
2 def up
3 add_column :queries, :visibility, :integer, :default => 0
4 Query.where(:is_public => true).update_all(:visibility => 2)
5 remove_column :queries, :is_public
6 end
7
8 def down
9 add_column :queries, :is_public, :boolean, :default => true, :null => false
10 Query.where('visibility <> ?', 2).update_all(:is_public => false)
11 remove_column :queries, :visibility
12 end
13 end
@@ -1,106 +1,106
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueriesController < ApplicationController
18 class QueriesController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_query, :except => [:new, :create, :index]
20 before_filter :find_query, :except => [:new, :create, :index]
21 before_filter :find_optional_project, :only => [:new, :create]
21 before_filter :find_optional_project, :only => [:new, :create]
22
22
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 include QueriesHelper
25 include QueriesHelper
26
26
27 def index
27 def index
28 case params[:format]
28 case params[:format]
29 when 'xml', 'json'
29 when 'xml', 'json'
30 @offset, @limit = api_offset_and_limit
30 @offset, @limit = api_offset_and_limit
31 else
31 else
32 @limit = per_page_option
32 @limit = per_page_option
33 end
33 end
34
34
35 @query_count = IssueQuery.visible.count
35 @query_count = IssueQuery.visible.count
36 @query_pages = Paginator.new @query_count, @limit, params['page']
36 @query_pages = Paginator.new @query_count, @limit, params['page']
37 @queries = IssueQuery.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
37 @queries = IssueQuery.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
38
38
39 respond_to do |format|
39 respond_to do |format|
40 format.api
40 format.api
41 end
41 end
42 end
42 end
43
43
44 def new
44 def new
45 @query = IssueQuery.new
45 @query = IssueQuery.new
46 @query.user = User.current
46 @query.user = User.current
47 @query.project = @project
47 @query.project = @project
48 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
48 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
49 @query.build_from_params(params)
49 @query.build_from_params(params)
50 end
50 end
51
51
52 def create
52 def create
53 @query = IssueQuery.new(params[:query])
53 @query = IssueQuery.new(params[:query])
54 @query.user = User.current
54 @query.user = User.current
55 @query.project = params[:query_is_for_all] ? nil : @project
55 @query.project = params[:query_is_for_all] ? nil : @project
56 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
56 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
57 @query.build_from_params(params)
57 @query.build_from_params(params)
58 @query.column_names = nil if params[:default_columns]
58 @query.column_names = nil if params[:default_columns]
59
59
60 if @query.save
60 if @query.save
61 flash[:notice] = l(:notice_successful_create)
61 flash[:notice] = l(:notice_successful_create)
62 redirect_to _project_issues_path(@project, :query_id => @query)
62 redirect_to _project_issues_path(@project, :query_id => @query)
63 else
63 else
64 render :action => 'new', :layout => !request.xhr?
64 render :action => 'new', :layout => !request.xhr?
65 end
65 end
66 end
66 end
67
67
68 def edit
68 def edit
69 end
69 end
70
70
71 def update
71 def update
72 @query.attributes = params[:query]
72 @query.attributes = params[:query]
73 @query.project = nil if params[:query_is_for_all]
73 @query.project = nil if params[:query_is_for_all]
74 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
74 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
75 @query.build_from_params(params)
75 @query.build_from_params(params)
76 @query.column_names = nil if params[:default_columns]
76 @query.column_names = nil if params[:default_columns]
77
77
78 if @query.save
78 if @query.save
79 flash[:notice] = l(:notice_successful_update)
79 flash[:notice] = l(:notice_successful_update)
80 redirect_to _project_issues_path(@project, :query_id => @query)
80 redirect_to _project_issues_path(@project, :query_id => @query)
81 else
81 else
82 render :action => 'edit'
82 render :action => 'edit'
83 end
83 end
84 end
84 end
85
85
86 def destroy
86 def destroy
87 @query.destroy
87 @query.destroy
88 redirect_to _project_issues_path(@project, :set_filter => 1)
88 redirect_to _project_issues_path(@project, :set_filter => 1)
89 end
89 end
90
90
91 private
91 private
92 def find_query
92 def find_query
93 @query = IssueQuery.find(params[:id])
93 @query = IssueQuery.find(params[:id])
94 @project = @query.project
94 @project = @query.project
95 render_403 unless @query.editable_by?(User.current)
95 render_403 unless @query.editable_by?(User.current)
96 rescue ActiveRecord::RecordNotFound
96 rescue ActiveRecord::RecordNotFound
97 render_404
97 render_404
98 end
98 end
99
99
100 def find_optional_project
100 def find_optional_project
101 @project = Project.find(params[:project_id]) if params[:project_id]
101 @project = Project.find(params[:project_id]) if params[:project_id]
102 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
102 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
103 rescue ActiveRecord::RecordNotFound
103 rescue ActiveRecord::RecordNotFound
104 render_404
104 render_404
105 end
105 end
106 end
106 end
@@ -1,405 +1,405
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def issue_list(issues, &block)
23 def issue_list(issues, &block)
24 ancestors = []
24 ancestors = []
25 issues.each do |issue|
25 issues.each do |issue|
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 ancestors.pop
27 ancestors.pop
28 end
28 end
29 yield issue, ancestors.size
29 yield issue, ancestors.size
30 ancestors << issue unless issue.leaf?
30 ancestors << issue unless issue.leaf?
31 end
31 end
32 end
32 end
33
33
34 # Renders a HTML/CSS tooltip
34 # Renders a HTML/CSS tooltip
35 #
35 #
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 # that contains this method wrapped in a span with the class of "tip"
37 # that contains this method wrapped in a span with the class of "tip"
38 #
38 #
39 # <div class="tooltip"><%= link_to_issue(issue) %>
39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 # </div>
41 # </div>
42 #
42 #
43 def render_issue_tooltip(issue)
43 def render_issue_tooltip(issue)
44 @cached_label_status ||= l(:field_status)
44 @cached_label_status ||= l(:field_status)
45 @cached_label_start_date ||= l(:field_start_date)
45 @cached_label_start_date ||= l(:field_start_date)
46 @cached_label_due_date ||= l(:field_due_date)
46 @cached_label_due_date ||= l(:field_due_date)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 @cached_label_priority ||= l(:field_priority)
48 @cached_label_priority ||= l(:field_priority)
49 @cached_label_project ||= l(:field_project)
49 @cached_label_project ||= l(:field_project)
50
50
51 link_to_issue(issue) + "<br /><br />".html_safe +
51 link_to_issue(issue) + "<br /><br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 end
58 end
59
59
60 def issue_heading(issue)
60 def issue_heading(issue)
61 h("#{issue.tracker} ##{issue.id}")
61 h("#{issue.tracker} ##{issue.id}")
62 end
62 end
63
63
64 def render_issue_subject_with_tree(issue)
64 def render_issue_subject_with_tree(issue)
65 s = ''
65 s = ''
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 ancestors.each do |ancestor|
67 ancestors.each do |ancestor|
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 end
69 end
70 s << '<div>'
70 s << '<div>'
71 subject = h(issue.subject)
71 subject = h(issue.subject)
72 if issue.is_private?
72 if issue.is_private?
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 end
74 end
75 s << content_tag('h3', subject)
75 s << content_tag('h3', subject)
76 s << '</div>' * (ancestors.size + 1)
76 s << '</div>' * (ancestors.size + 1)
77 s.html_safe
77 s.html_safe
78 end
78 end
79
79
80 def render_descendants_tree(issue)
80 def render_descendants_tree(issue)
81 s = '<form><table class="list issues">'
81 s = '<form><table class="list issues">'
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 css = "issue issue-#{child.id} hascontextmenu"
83 css = "issue issue-#{child.id} hascontextmenu"
84 css << " idnt idnt-#{level}" if level > 0
84 css << " idnt idnt-#{level}" if level > 0
85 s << content_tag('tr',
85 s << content_tag('tr',
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 content_tag('td', h(child.status)) +
88 content_tag('td', h(child.status)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 :class => css)
91 :class => css)
92 end
92 end
93 s << '</table></form>'
93 s << '</table></form>'
94 s.html_safe
94 s.html_safe
95 end
95 end
96
96
97 # Returns an array of error messages for bulk edited issues
97 # Returns an array of error messages for bulk edited issues
98 def bulk_edit_error_messages(issues)
98 def bulk_edit_error_messages(issues)
99 messages = {}
99 messages = {}
100 issues.each do |issue|
100 issues.each do |issue|
101 issue.errors.full_messages.each do |message|
101 issue.errors.full_messages.each do |message|
102 messages[message] ||= []
102 messages[message] ||= []
103 messages[message] << issue
103 messages[message] << issue
104 end
104 end
105 end
105 end
106 messages.map { |message, issues|
106 messages.map { |message, issues|
107 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
107 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
108 }
108 }
109 end
109 end
110
110
111 # Returns a link for adding a new subtask to the given issue
111 # Returns a link for adding a new subtask to the given issue
112 def link_to_new_subtask(issue)
112 def link_to_new_subtask(issue)
113 attrs = {
113 attrs = {
114 :tracker_id => issue.tracker,
114 :tracker_id => issue.tracker,
115 :parent_issue_id => issue
115 :parent_issue_id => issue
116 }
116 }
117 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
117 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
118 end
118 end
119
119
120 class IssueFieldsRows
120 class IssueFieldsRows
121 include ActionView::Helpers::TagHelper
121 include ActionView::Helpers::TagHelper
122
122
123 def initialize
123 def initialize
124 @left = []
124 @left = []
125 @right = []
125 @right = []
126 end
126 end
127
127
128 def left(*args)
128 def left(*args)
129 args.any? ? @left << cells(*args) : @left
129 args.any? ? @left << cells(*args) : @left
130 end
130 end
131
131
132 def right(*args)
132 def right(*args)
133 args.any? ? @right << cells(*args) : @right
133 args.any? ? @right << cells(*args) : @right
134 end
134 end
135
135
136 def size
136 def size
137 @left.size > @right.size ? @left.size : @right.size
137 @left.size > @right.size ? @left.size : @right.size
138 end
138 end
139
139
140 def to_html
140 def to_html
141 html = ''.html_safe
141 html = ''.html_safe
142 blank = content_tag('th', '') + content_tag('td', '')
142 blank = content_tag('th', '') + content_tag('td', '')
143 size.times do |i|
143 size.times do |i|
144 left = @left[i] || blank
144 left = @left[i] || blank
145 right = @right[i] || blank
145 right = @right[i] || blank
146 html << content_tag('tr', left + right)
146 html << content_tag('tr', left + right)
147 end
147 end
148 html
148 html
149 end
149 end
150
150
151 def cells(label, text, options={})
151 def cells(label, text, options={})
152 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
152 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
153 end
153 end
154 end
154 end
155
155
156 def issue_fields_rows
156 def issue_fields_rows
157 r = IssueFieldsRows.new
157 r = IssueFieldsRows.new
158 yield r
158 yield r
159 r.to_html
159 r.to_html
160 end
160 end
161
161
162 def render_custom_fields_rows(issue)
162 def render_custom_fields_rows(issue)
163 return if issue.custom_field_values.empty?
163 return if issue.custom_field_values.empty?
164 ordered_values = []
164 ordered_values = []
165 half = (issue.custom_field_values.size / 2.0).ceil
165 half = (issue.custom_field_values.size / 2.0).ceil
166 half.times do |i|
166 half.times do |i|
167 ordered_values << issue.custom_field_values[i]
167 ordered_values << issue.custom_field_values[i]
168 ordered_values << issue.custom_field_values[i + half]
168 ordered_values << issue.custom_field_values[i + half]
169 end
169 end
170 s = "<tr>\n"
170 s = "<tr>\n"
171 n = 0
171 n = 0
172 ordered_values.compact.each do |value|
172 ordered_values.compact.each do |value|
173 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
173 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
174 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
174 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
175 n += 1
175 n += 1
176 end
176 end
177 s << "</tr>\n"
177 s << "</tr>\n"
178 s.html_safe
178 s.html_safe
179 end
179 end
180
180
181 def issues_destroy_confirmation_message(issues)
181 def issues_destroy_confirmation_message(issues)
182 issues = [issues] unless issues.is_a?(Array)
182 issues = [issues] unless issues.is_a?(Array)
183 message = l(:text_issues_destroy_confirmation)
183 message = l(:text_issues_destroy_confirmation)
184 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
184 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
185 if descendant_count > 0
185 if descendant_count > 0
186 issues.each do |issue|
186 issues.each do |issue|
187 next if issue.root?
187 next if issue.root?
188 issues.each do |other_issue|
188 issues.each do |other_issue|
189 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
189 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
190 end
190 end
191 end
191 end
192 if descendant_count > 0
192 if descendant_count > 0
193 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
193 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
194 end
194 end
195 end
195 end
196 message
196 message
197 end
197 end
198
198
199 def sidebar_queries
199 def sidebar_queries
200 unless @sidebar_queries
200 unless @sidebar_queries
201 @sidebar_queries = IssueQuery.visible.
201 @sidebar_queries = IssueQuery.visible.
202 order("#{Query.table_name}.name ASC").
202 order("#{Query.table_name}.name ASC").
203 # Project specific queries and global queries
203 # Project specific queries and global queries
204 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
204 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
205 all
205 all
206 end
206 end
207 @sidebar_queries
207 @sidebar_queries
208 end
208 end
209
209
210 def query_links(title, queries)
210 def query_links(title, queries)
211 return '' if queries.empty?
211 return '' if queries.empty?
212 # links to #index on issues/show
212 # links to #index on issues/show
213 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
213 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
214
214
215 content_tag('h3', title) + "\n" +
215 content_tag('h3', title) + "\n" +
216 content_tag('ul',
216 content_tag('ul',
217 queries.collect {|query|
217 queries.collect {|query|
218 css = 'query'
218 css = 'query'
219 css << ' selected' if query == @query
219 css << ' selected' if query == @query
220 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
220 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
221 }.join("\n").html_safe,
221 }.join("\n").html_safe,
222 :class => 'queries'
222 :class => 'queries'
223 ) + "\n"
223 ) + "\n"
224 end
224 end
225
225
226 def render_sidebar_queries
226 def render_sidebar_queries
227 out = ''.html_safe
227 out = ''.html_safe
228 out << query_links(l(:label_my_queries), sidebar_queries.reject(&:is_public?))
228 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
229 out << query_links(l(:label_query_plural), sidebar_queries.select(&:is_public?))
229 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
230 out
230 out
231 end
231 end
232
232
233 # Returns the textual representation of a journal details
233 # Returns the textual representation of a journal details
234 # as an array of strings
234 # as an array of strings
235 def details_to_strings(details, no_html=false, options={})
235 def details_to_strings(details, no_html=false, options={})
236 options[:only_path] = (options[:only_path] == false ? false : true)
236 options[:only_path] = (options[:only_path] == false ? false : true)
237 strings = []
237 strings = []
238 values_by_field = {}
238 values_by_field = {}
239 details.each do |detail|
239 details.each do |detail|
240 if detail.property == 'cf'
240 if detail.property == 'cf'
241 field_id = detail.prop_key
241 field_id = detail.prop_key
242 field = CustomField.find_by_id(field_id)
242 field = CustomField.find_by_id(field_id)
243 if field && field.multiple?
243 if field && field.multiple?
244 values_by_field[field_id] ||= {:added => [], :deleted => []}
244 values_by_field[field_id] ||= {:added => [], :deleted => []}
245 if detail.old_value
245 if detail.old_value
246 values_by_field[field_id][:deleted] << detail.old_value
246 values_by_field[field_id][:deleted] << detail.old_value
247 end
247 end
248 if detail.value
248 if detail.value
249 values_by_field[field_id][:added] << detail.value
249 values_by_field[field_id][:added] << detail.value
250 end
250 end
251 next
251 next
252 end
252 end
253 end
253 end
254 strings << show_detail(detail, no_html, options)
254 strings << show_detail(detail, no_html, options)
255 end
255 end
256 values_by_field.each do |field_id, changes|
256 values_by_field.each do |field_id, changes|
257 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
257 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
258 if changes[:added].any?
258 if changes[:added].any?
259 detail.value = changes[:added]
259 detail.value = changes[:added]
260 strings << show_detail(detail, no_html, options)
260 strings << show_detail(detail, no_html, options)
261 elsif changes[:deleted].any?
261 elsif changes[:deleted].any?
262 detail.old_value = changes[:deleted]
262 detail.old_value = changes[:deleted]
263 strings << show_detail(detail, no_html, options)
263 strings << show_detail(detail, no_html, options)
264 end
264 end
265 end
265 end
266 strings
266 strings
267 end
267 end
268
268
269 # Returns the textual representation of a single journal detail
269 # Returns the textual representation of a single journal detail
270 def show_detail(detail, no_html=false, options={})
270 def show_detail(detail, no_html=false, options={})
271 multiple = false
271 multiple = false
272 case detail.property
272 case detail.property
273 when 'attr'
273 when 'attr'
274 field = detail.prop_key.to_s.gsub(/\_id$/, "")
274 field = detail.prop_key.to_s.gsub(/\_id$/, "")
275 label = l(("field_" + field).to_sym)
275 label = l(("field_" + field).to_sym)
276 case detail.prop_key
276 case detail.prop_key
277 when 'due_date', 'start_date'
277 when 'due_date', 'start_date'
278 value = format_date(detail.value.to_date) if detail.value
278 value = format_date(detail.value.to_date) if detail.value
279 old_value = format_date(detail.old_value.to_date) if detail.old_value
279 old_value = format_date(detail.old_value.to_date) if detail.old_value
280
280
281 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
281 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
282 'priority_id', 'category_id', 'fixed_version_id'
282 'priority_id', 'category_id', 'fixed_version_id'
283 value = find_name_by_reflection(field, detail.value)
283 value = find_name_by_reflection(field, detail.value)
284 old_value = find_name_by_reflection(field, detail.old_value)
284 old_value = find_name_by_reflection(field, detail.old_value)
285
285
286 when 'estimated_hours'
286 when 'estimated_hours'
287 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
287 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
288 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
288 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
289
289
290 when 'parent_id'
290 when 'parent_id'
291 label = l(:field_parent_issue)
291 label = l(:field_parent_issue)
292 value = "##{detail.value}" unless detail.value.blank?
292 value = "##{detail.value}" unless detail.value.blank?
293 old_value = "##{detail.old_value}" unless detail.old_value.blank?
293 old_value = "##{detail.old_value}" unless detail.old_value.blank?
294
294
295 when 'is_private'
295 when 'is_private'
296 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
296 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
297 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
297 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
298 end
298 end
299 when 'cf'
299 when 'cf'
300 custom_field = CustomField.find_by_id(detail.prop_key)
300 custom_field = CustomField.find_by_id(detail.prop_key)
301 if custom_field
301 if custom_field
302 multiple = custom_field.multiple?
302 multiple = custom_field.multiple?
303 label = custom_field.name
303 label = custom_field.name
304 value = format_value(detail.value, custom_field.field_format) if detail.value
304 value = format_value(detail.value, custom_field.field_format) if detail.value
305 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
305 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
306 end
306 end
307 when 'attachment'
307 when 'attachment'
308 label = l(:label_attachment)
308 label = l(:label_attachment)
309 when 'relation'
309 when 'relation'
310 if detail.value && !detail.old_value
310 if detail.value && !detail.old_value
311 rel_issue = Issue.visible.find_by_id(detail.value)
311 rel_issue = Issue.visible.find_by_id(detail.value)
312 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
312 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
313 (no_html ? rel_issue : link_to_issue(rel_issue))
313 (no_html ? rel_issue : link_to_issue(rel_issue))
314 elsif detail.old_value && !detail.value
314 elsif detail.old_value && !detail.value
315 rel_issue = Issue.visible.find_by_id(detail.old_value)
315 rel_issue = Issue.visible.find_by_id(detail.old_value)
316 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
316 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
317 (no_html ? rel_issue : link_to_issue(rel_issue))
317 (no_html ? rel_issue : link_to_issue(rel_issue))
318 end
318 end
319 label = l(detail.prop_key.to_sym)
319 label = l(detail.prop_key.to_sym)
320 end
320 end
321 call_hook(:helper_issues_show_detail_after_setting,
321 call_hook(:helper_issues_show_detail_after_setting,
322 {:detail => detail, :label => label, :value => value, :old_value => old_value })
322 {:detail => detail, :label => label, :value => value, :old_value => old_value })
323
323
324 label ||= detail.prop_key
324 label ||= detail.prop_key
325 value ||= detail.value
325 value ||= detail.value
326 old_value ||= detail.old_value
326 old_value ||= detail.old_value
327
327
328 unless no_html
328 unless no_html
329 label = content_tag('strong', label)
329 label = content_tag('strong', label)
330 old_value = content_tag("i", h(old_value)) if detail.old_value
330 old_value = content_tag("i", h(old_value)) if detail.old_value
331 if detail.old_value && detail.value.blank? && detail.property != 'relation'
331 if detail.old_value && detail.value.blank? && detail.property != 'relation'
332 old_value = content_tag("del", old_value)
332 old_value = content_tag("del", old_value)
333 end
333 end
334 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
334 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
335 # Link to the attachment if it has not been removed
335 # Link to the attachment if it has not been removed
336 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
336 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
337 if options[:only_path] != false && atta.is_text?
337 if options[:only_path] != false && atta.is_text?
338 value += link_to(
338 value += link_to(
339 image_tag('magnifier.png'),
339 image_tag('magnifier.png'),
340 :controller => 'attachments', :action => 'show',
340 :controller => 'attachments', :action => 'show',
341 :id => atta, :filename => atta.filename
341 :id => atta, :filename => atta.filename
342 )
342 )
343 end
343 end
344 else
344 else
345 value = content_tag("i", h(value)) if value
345 value = content_tag("i", h(value)) if value
346 end
346 end
347 end
347 end
348
348
349 if detail.property == 'attr' && detail.prop_key == 'description'
349 if detail.property == 'attr' && detail.prop_key == 'description'
350 s = l(:text_journal_changed_no_detail, :label => label)
350 s = l(:text_journal_changed_no_detail, :label => label)
351 unless no_html
351 unless no_html
352 diff_link = link_to 'diff',
352 diff_link = link_to 'diff',
353 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
353 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
354 :detail_id => detail.id, :only_path => options[:only_path]},
354 :detail_id => detail.id, :only_path => options[:only_path]},
355 :title => l(:label_view_diff)
355 :title => l(:label_view_diff)
356 s << " (#{ diff_link })"
356 s << " (#{ diff_link })"
357 end
357 end
358 s.html_safe
358 s.html_safe
359 elsif detail.value.present?
359 elsif detail.value.present?
360 case detail.property
360 case detail.property
361 when 'attr', 'cf'
361 when 'attr', 'cf'
362 if detail.old_value.present?
362 if detail.old_value.present?
363 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
363 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
364 elsif multiple
364 elsif multiple
365 l(:text_journal_added, :label => label, :value => value).html_safe
365 l(:text_journal_added, :label => label, :value => value).html_safe
366 else
366 else
367 l(:text_journal_set_to, :label => label, :value => value).html_safe
367 l(:text_journal_set_to, :label => label, :value => value).html_safe
368 end
368 end
369 when 'attachment', 'relation'
369 when 'attachment', 'relation'
370 l(:text_journal_added, :label => label, :value => value).html_safe
370 l(:text_journal_added, :label => label, :value => value).html_safe
371 end
371 end
372 else
372 else
373 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
373 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
374 end
374 end
375 end
375 end
376
376
377 # Find the name of an associated record stored in the field attribute
377 # Find the name of an associated record stored in the field attribute
378 def find_name_by_reflection(field, id)
378 def find_name_by_reflection(field, id)
379 unless id.present?
379 unless id.present?
380 return nil
380 return nil
381 end
381 end
382 association = Issue.reflect_on_association(field.to_sym)
382 association = Issue.reflect_on_association(field.to_sym)
383 if association
383 if association
384 record = association.class_name.constantize.find_by_id(id)
384 record = association.class_name.constantize.find_by_id(id)
385 if record
385 if record
386 record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding)
386 record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding)
387 return record.name
387 return record.name
388 end
388 end
389 end
389 end
390 end
390 end
391
391
392 # Renders issue children recursively
392 # Renders issue children recursively
393 def render_api_issue_children(issue, api)
393 def render_api_issue_children(issue, api)
394 return if issue.leaf?
394 return if issue.leaf?
395 api.array :children do
395 api.array :children do
396 issue.children.each do |child|
396 issue.children.each do |child|
397 api.issue(:id => child.id) do
397 api.issue(:id => child.id) do
398 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
398 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
399 api.subject child.subject
399 api.subject child.subject
400 render_api_issue_children(child, api)
400 render_api_issue_children(child, api)
401 end
401 end
402 end
402 end
403 end
403 end
404 end
404 end
405 end
405 end
@@ -1,417 +1,454
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21
21
22 self.available_columns = [
22 self.available_columns = [
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
26 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
26 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
27 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
27 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
31 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
31 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
32 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
32 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
33 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
33 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
34 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
34 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
38 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
38 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
39 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
39 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
40 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
40 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
41 QueryColumn.new(:relations, :caption => :label_related_issues),
41 QueryColumn.new(:relations, :caption => :label_related_issues),
42 QueryColumn.new(:description, :inline => false)
42 QueryColumn.new(:description, :inline => false)
43 ]
43 ]
44
44
45 scope :visible, lambda {|*args|
45 scope :visible, lambda {|*args|
46 user = args.shift || User.current
46 user = args.shift || User.current
47 base = Project.allowed_to_condition(user, :view_issues, *args)
47 base = Project.allowed_to_condition(user, :view_issues, *args)
48 user_id = user.logged? ? user.id : 0
48 scope = includes(:project).where("#{table_name}.project_id IS NULL OR (#{base})")
49
49
50 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
50 if user.admin?
51 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
52 elsif user.memberships.any?
53 scope.where("#{table_name}.visibility = ?" +
54 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
55 "SELECT DISTINCT q.id FROM #{table_name} q" +
56 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
57 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
58 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
59 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
60 " OR #{table_name}.user_id = ?",
61 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
62 elsif user.logged?
63 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
64 else
65 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
66 end
51 }
67 }
52
68
53 def initialize(attributes=nil, *args)
69 def initialize(attributes=nil, *args)
54 super attributes
70 super attributes
55 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
71 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
56 end
72 end
57
73
58 # Returns true if the query is visible to +user+ or the current user.
74 # Returns true if the query is visible to +user+ or the current user.
59 def visible?(user=User.current)
75 def visible?(user=User.current)
60 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
76 return true if user.admin?
77 return false unless project.nil? || user.allowed_to?(:view_issues, project)
78 case visibility
79 when VISIBILITY_PUBLIC
80 true
81 when VISIBILITY_ROLES
82 if project
83 (user.roles_for_project(project) & roles).any?
84 else
85 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
86 end
87 else
88 user == self.user
89 end
90 end
91
92 def is_private?
93 visibility == VISIBILITY_PRIVATE
94 end
95
96 def is_public?
97 !is_private?
61 end
98 end
62
99
63 def initialize_available_filters
100 def initialize_available_filters
64 principals = []
101 principals = []
65 subprojects = []
102 subprojects = []
66 versions = []
103 versions = []
67 categories = []
104 categories = []
68 issue_custom_fields = []
105 issue_custom_fields = []
69
106
70 if project
107 if project
71 principals += project.principals.sort
108 principals += project.principals.sort
72 unless project.leaf?
109 unless project.leaf?
73 subprojects = project.descendants.visible.all
110 subprojects = project.descendants.visible.all
74 principals += Principal.member_of(subprojects)
111 principals += Principal.member_of(subprojects)
75 end
112 end
76 versions = project.shared_versions.all
113 versions = project.shared_versions.all
77 categories = project.issue_categories.all
114 categories = project.issue_categories.all
78 issue_custom_fields = project.all_issue_custom_fields
115 issue_custom_fields = project.all_issue_custom_fields
79 else
116 else
80 if all_projects.any?
117 if all_projects.any?
81 principals += Principal.member_of(all_projects)
118 principals += Principal.member_of(all_projects)
82 end
119 end
83 versions = Version.visible.find_all_by_sharing('system')
120 versions = Version.visible.find_all_by_sharing('system')
84 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
121 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
85 end
122 end
86 principals.uniq!
123 principals.uniq!
87 principals.sort!
124 principals.sort!
88 users = principals.select {|p| p.is_a?(User)}
125 users = principals.select {|p| p.is_a?(User)}
89
126
90 add_available_filter "status_id",
127 add_available_filter "status_id",
91 :type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
128 :type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
92
129
93 if project.nil?
130 if project.nil?
94 project_values = []
131 project_values = []
95 if User.current.logged? && User.current.memberships.any?
132 if User.current.logged? && User.current.memberships.any?
96 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
133 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
97 end
134 end
98 project_values += all_projects_values
135 project_values += all_projects_values
99 add_available_filter("project_id",
136 add_available_filter("project_id",
100 :type => :list, :values => project_values
137 :type => :list, :values => project_values
101 ) unless project_values.empty?
138 ) unless project_values.empty?
102 end
139 end
103
140
104 add_available_filter "tracker_id",
141 add_available_filter "tracker_id",
105 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
142 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
106 add_available_filter "priority_id",
143 add_available_filter "priority_id",
107 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
144 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
108
145
109 author_values = []
146 author_values = []
110 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
147 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
111 author_values += users.collect{|s| [s.name, s.id.to_s] }
148 author_values += users.collect{|s| [s.name, s.id.to_s] }
112 add_available_filter("author_id",
149 add_available_filter("author_id",
113 :type => :list, :values => author_values
150 :type => :list, :values => author_values
114 ) unless author_values.empty?
151 ) unless author_values.empty?
115
152
116 assigned_to_values = []
153 assigned_to_values = []
117 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
154 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
118 assigned_to_values += (Setting.issue_group_assignment? ?
155 assigned_to_values += (Setting.issue_group_assignment? ?
119 principals : users).collect{|s| [s.name, s.id.to_s] }
156 principals : users).collect{|s| [s.name, s.id.to_s] }
120 add_available_filter("assigned_to_id",
157 add_available_filter("assigned_to_id",
121 :type => :list_optional, :values => assigned_to_values
158 :type => :list_optional, :values => assigned_to_values
122 ) unless assigned_to_values.empty?
159 ) unless assigned_to_values.empty?
123
160
124 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
161 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
125 add_available_filter("member_of_group",
162 add_available_filter("member_of_group",
126 :type => :list_optional, :values => group_values
163 :type => :list_optional, :values => group_values
127 ) unless group_values.empty?
164 ) unless group_values.empty?
128
165
129 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
166 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
130 add_available_filter("assigned_to_role",
167 add_available_filter("assigned_to_role",
131 :type => :list_optional, :values => role_values
168 :type => :list_optional, :values => role_values
132 ) unless role_values.empty?
169 ) unless role_values.empty?
133
170
134 if versions.any?
171 if versions.any?
135 add_available_filter "fixed_version_id",
172 add_available_filter "fixed_version_id",
136 :type => :list_optional,
173 :type => :list_optional,
137 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
174 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
138 end
175 end
139
176
140 if categories.any?
177 if categories.any?
141 add_available_filter "category_id",
178 add_available_filter "category_id",
142 :type => :list_optional,
179 :type => :list_optional,
143 :values => categories.collect{|s| [s.name, s.id.to_s] }
180 :values => categories.collect{|s| [s.name, s.id.to_s] }
144 end
181 end
145
182
146 add_available_filter "subject", :type => :text
183 add_available_filter "subject", :type => :text
147 add_available_filter "created_on", :type => :date_past
184 add_available_filter "created_on", :type => :date_past
148 add_available_filter "updated_on", :type => :date_past
185 add_available_filter "updated_on", :type => :date_past
149 add_available_filter "closed_on", :type => :date_past
186 add_available_filter "closed_on", :type => :date_past
150 add_available_filter "start_date", :type => :date
187 add_available_filter "start_date", :type => :date
151 add_available_filter "due_date", :type => :date
188 add_available_filter "due_date", :type => :date
152 add_available_filter "estimated_hours", :type => :float
189 add_available_filter "estimated_hours", :type => :float
153 add_available_filter "done_ratio", :type => :integer
190 add_available_filter "done_ratio", :type => :integer
154
191
155 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
192 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
156 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
193 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
157 add_available_filter "is_private",
194 add_available_filter "is_private",
158 :type => :list,
195 :type => :list,
159 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
196 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
160 end
197 end
161
198
162 if User.current.logged?
199 if User.current.logged?
163 add_available_filter "watcher_id",
200 add_available_filter "watcher_id",
164 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
201 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
165 end
202 end
166
203
167 if subprojects.any?
204 if subprojects.any?
168 add_available_filter "subproject_id",
205 add_available_filter "subproject_id",
169 :type => :list_subprojects,
206 :type => :list_subprojects,
170 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
207 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
171 end
208 end
172
209
173 add_custom_fields_filters(issue_custom_fields)
210 add_custom_fields_filters(issue_custom_fields)
174
211
175 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
212 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
176
213
177 IssueRelation::TYPES.each do |relation_type, options|
214 IssueRelation::TYPES.each do |relation_type, options|
178 add_available_filter relation_type, :type => :relation, :label => options[:name]
215 add_available_filter relation_type, :type => :relation, :label => options[:name]
179 end
216 end
180
217
181 Tracker.disabled_core_fields(trackers).each {|field|
218 Tracker.disabled_core_fields(trackers).each {|field|
182 delete_available_filter field
219 delete_available_filter field
183 }
220 }
184 end
221 end
185
222
186 def available_columns
223 def available_columns
187 return @available_columns if @available_columns
224 return @available_columns if @available_columns
188 @available_columns = self.class.available_columns.dup
225 @available_columns = self.class.available_columns.dup
189 @available_columns += (project ?
226 @available_columns += (project ?
190 project.all_issue_custom_fields :
227 project.all_issue_custom_fields :
191 IssueCustomField.all
228 IssueCustomField.all
192 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
229 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
193
230
194 if User.current.allowed_to?(:view_time_entries, project, :global => true)
231 if User.current.allowed_to?(:view_time_entries, project, :global => true)
195 index = nil
232 index = nil
196 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
233 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
197 index = (index ? index + 1 : -1)
234 index = (index ? index + 1 : -1)
198 # insert the column after estimated_hours or at the end
235 # insert the column after estimated_hours or at the end
199 @available_columns.insert index, QueryColumn.new(:spent_hours,
236 @available_columns.insert index, QueryColumn.new(:spent_hours,
200 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
237 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
201 :default_order => 'desc',
238 :default_order => 'desc',
202 :caption => :label_spent_time
239 :caption => :label_spent_time
203 )
240 )
204 end
241 end
205
242
206 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
243 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
207 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
244 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
208 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
245 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
209 end
246 end
210
247
211 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
248 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
212 @available_columns.reject! {|column|
249 @available_columns.reject! {|column|
213 disabled_fields.include?(column.name.to_s)
250 disabled_fields.include?(column.name.to_s)
214 }
251 }
215
252
216 @available_columns
253 @available_columns
217 end
254 end
218
255
219 def default_columns_names
256 def default_columns_names
220 @default_columns_names ||= begin
257 @default_columns_names ||= begin
221 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
258 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
222
259
223 project.present? ? default_columns : [:project] | default_columns
260 project.present? ? default_columns : [:project] | default_columns
224 end
261 end
225 end
262 end
226
263
227 # Returns the issue count
264 # Returns the issue count
228 def issue_count
265 def issue_count
229 Issue.visible.joins(:status, :project).where(statement).count
266 Issue.visible.joins(:status, :project).where(statement).count
230 rescue ::ActiveRecord::StatementInvalid => e
267 rescue ::ActiveRecord::StatementInvalid => e
231 raise StatementInvalid.new(e.message)
268 raise StatementInvalid.new(e.message)
232 end
269 end
233
270
234 # Returns the issue count by group or nil if query is not grouped
271 # Returns the issue count by group or nil if query is not grouped
235 def issue_count_by_group
272 def issue_count_by_group
236 r = nil
273 r = nil
237 if grouped?
274 if grouped?
238 begin
275 begin
239 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
276 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
240 r = Issue.visible.
277 r = Issue.visible.
241 joins(:status, :project).
278 joins(:status, :project).
242 where(statement).
279 where(statement).
243 joins(joins_for_order_statement(group_by_statement)).
280 joins(joins_for_order_statement(group_by_statement)).
244 group(group_by_statement).
281 group(group_by_statement).
245 count
282 count
246 rescue ActiveRecord::RecordNotFound
283 rescue ActiveRecord::RecordNotFound
247 r = {nil => issue_count}
284 r = {nil => issue_count}
248 end
285 end
249 c = group_by_column
286 c = group_by_column
250 if c.is_a?(QueryCustomFieldColumn)
287 if c.is_a?(QueryCustomFieldColumn)
251 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
288 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
252 end
289 end
253 end
290 end
254 r
291 r
255 rescue ::ActiveRecord::StatementInvalid => e
292 rescue ::ActiveRecord::StatementInvalid => e
256 raise StatementInvalid.new(e.message)
293 raise StatementInvalid.new(e.message)
257 end
294 end
258
295
259 # Returns the issues
296 # Returns the issues
260 # Valid options are :order, :offset, :limit, :include, :conditions
297 # Valid options are :order, :offset, :limit, :include, :conditions
261 def issues(options={})
298 def issues(options={})
262 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
299 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
263
300
264 issues = Issue.visible.
301 issues = Issue.visible.
265 joins(:status, :project).
302 joins(:status, :project).
266 where(statement).
303 where(statement).
267 includes(([:status, :project] + (options[:include] || [])).uniq).
304 includes(([:status, :project] + (options[:include] || [])).uniq).
268 where(options[:conditions]).
305 where(options[:conditions]).
269 order(order_option).
306 order(order_option).
270 joins(joins_for_order_statement(order_option.join(','))).
307 joins(joins_for_order_statement(order_option.join(','))).
271 limit(options[:limit]).
308 limit(options[:limit]).
272 offset(options[:offset]).
309 offset(options[:offset]).
273 all
310 all
274
311
275 if has_column?(:spent_hours)
312 if has_column?(:spent_hours)
276 Issue.load_visible_spent_hours(issues)
313 Issue.load_visible_spent_hours(issues)
277 end
314 end
278 if has_column?(:relations)
315 if has_column?(:relations)
279 Issue.load_visible_relations(issues)
316 Issue.load_visible_relations(issues)
280 end
317 end
281 issues
318 issues
282 rescue ::ActiveRecord::StatementInvalid => e
319 rescue ::ActiveRecord::StatementInvalid => e
283 raise StatementInvalid.new(e.message)
320 raise StatementInvalid.new(e.message)
284 end
321 end
285
322
286 # Returns the issues ids
323 # Returns the issues ids
287 def issue_ids(options={})
324 def issue_ids(options={})
288 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
325 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
289
326
290 Issue.visible.
327 Issue.visible.
291 joins(:status, :project).
328 joins(:status, :project).
292 where(statement).
329 where(statement).
293 includes(([:status, :project] + (options[:include] || [])).uniq).
330 includes(([:status, :project] + (options[:include] || [])).uniq).
294 where(options[:conditions]).
331 where(options[:conditions]).
295 order(order_option).
332 order(order_option).
296 joins(joins_for_order_statement(order_option.join(','))).
333 joins(joins_for_order_statement(order_option.join(','))).
297 limit(options[:limit]).
334 limit(options[:limit]).
298 offset(options[:offset]).
335 offset(options[:offset]).
299 find_ids
336 find_ids
300 rescue ::ActiveRecord::StatementInvalid => e
337 rescue ::ActiveRecord::StatementInvalid => e
301 raise StatementInvalid.new(e.message)
338 raise StatementInvalid.new(e.message)
302 end
339 end
303
340
304 # Returns the journals
341 # Returns the journals
305 # Valid options are :order, :offset, :limit
342 # Valid options are :order, :offset, :limit
306 def journals(options={})
343 def journals(options={})
307 Journal.visible.
344 Journal.visible.
308 joins(:issue => [:project, :status]).
345 joins(:issue => [:project, :status]).
309 where(statement).
346 where(statement).
310 order(options[:order]).
347 order(options[:order]).
311 limit(options[:limit]).
348 limit(options[:limit]).
312 offset(options[:offset]).
349 offset(options[:offset]).
313 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
350 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
314 all
351 all
315 rescue ::ActiveRecord::StatementInvalid => e
352 rescue ::ActiveRecord::StatementInvalid => e
316 raise StatementInvalid.new(e.message)
353 raise StatementInvalid.new(e.message)
317 end
354 end
318
355
319 # Returns the versions
356 # Returns the versions
320 # Valid options are :conditions
357 # Valid options are :conditions
321 def versions(options={})
358 def versions(options={})
322 Version.visible.
359 Version.visible.
323 where(project_statement).
360 where(project_statement).
324 where(options[:conditions]).
361 where(options[:conditions]).
325 includes(:project).
362 includes(:project).
326 all
363 all
327 rescue ::ActiveRecord::StatementInvalid => e
364 rescue ::ActiveRecord::StatementInvalid => e
328 raise StatementInvalid.new(e.message)
365 raise StatementInvalid.new(e.message)
329 end
366 end
330
367
331 def sql_for_watcher_id_field(field, operator, value)
368 def sql_for_watcher_id_field(field, operator, value)
332 db_table = Watcher.table_name
369 db_table = Watcher.table_name
333 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
370 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
334 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
371 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
335 end
372 end
336
373
337 def sql_for_member_of_group_field(field, operator, value)
374 def sql_for_member_of_group_field(field, operator, value)
338 if operator == '*' # Any group
375 if operator == '*' # Any group
339 groups = Group.all
376 groups = Group.all
340 operator = '=' # Override the operator since we want to find by assigned_to
377 operator = '=' # Override the operator since we want to find by assigned_to
341 elsif operator == "!*"
378 elsif operator == "!*"
342 groups = Group.all
379 groups = Group.all
343 operator = '!' # Override the operator since we want to find by assigned_to
380 operator = '!' # Override the operator since we want to find by assigned_to
344 else
381 else
345 groups = Group.find_all_by_id(value)
382 groups = Group.find_all_by_id(value)
346 end
383 end
347 groups ||= []
384 groups ||= []
348
385
349 members_of_groups = groups.inject([]) {|user_ids, group|
386 members_of_groups = groups.inject([]) {|user_ids, group|
350 user_ids + group.user_ids + [group.id]
387 user_ids + group.user_ids + [group.id]
351 }.uniq.compact.sort.collect(&:to_s)
388 }.uniq.compact.sort.collect(&:to_s)
352
389
353 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
390 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
354 end
391 end
355
392
356 def sql_for_assigned_to_role_field(field, operator, value)
393 def sql_for_assigned_to_role_field(field, operator, value)
357 case operator
394 case operator
358 when "*", "!*" # Member / Not member
395 when "*", "!*" # Member / Not member
359 sw = operator == "!*" ? 'NOT' : ''
396 sw = operator == "!*" ? 'NOT' : ''
360 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
397 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
361 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
398 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
362 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
399 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
363 when "=", "!"
400 when "=", "!"
364 role_cond = value.any? ?
401 role_cond = value.any? ?
365 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
402 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
366 "1=0"
403 "1=0"
367
404
368 sw = operator == "!" ? 'NOT' : ''
405 sw = operator == "!" ? 'NOT' : ''
369 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
406 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
370 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
407 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
371 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
408 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
372 end
409 end
373 end
410 end
374
411
375 def sql_for_is_private_field(field, operator, value)
412 def sql_for_is_private_field(field, operator, value)
376 op = (operator == "=" ? 'IN' : 'NOT IN')
413 op = (operator == "=" ? 'IN' : 'NOT IN')
377 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
414 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
378
415
379 "#{Issue.table_name}.is_private #{op} (#{va})"
416 "#{Issue.table_name}.is_private #{op} (#{va})"
380 end
417 end
381
418
382 def sql_for_relations(field, operator, value, options={})
419 def sql_for_relations(field, operator, value, options={})
383 relation_options = IssueRelation::TYPES[field]
420 relation_options = IssueRelation::TYPES[field]
384 return relation_options unless relation_options
421 return relation_options unless relation_options
385
422
386 relation_type = field
423 relation_type = field
387 join_column, target_join_column = "issue_from_id", "issue_to_id"
424 join_column, target_join_column = "issue_from_id", "issue_to_id"
388 if relation_options[:reverse] || options[:reverse]
425 if relation_options[:reverse] || options[:reverse]
389 relation_type = relation_options[:reverse] || relation_type
426 relation_type = relation_options[:reverse] || relation_type
390 join_column, target_join_column = target_join_column, join_column
427 join_column, target_join_column = target_join_column, join_column
391 end
428 end
392
429
393 sql = case operator
430 sql = case operator
394 when "*", "!*"
431 when "*", "!*"
395 op = (operator == "*" ? 'IN' : 'NOT IN')
432 op = (operator == "*" ? 'IN' : 'NOT IN')
396 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
433 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
397 when "=", "!"
434 when "=", "!"
398 op = (operator == "=" ? 'IN' : 'NOT IN')
435 op = (operator == "=" ? 'IN' : 'NOT IN')
399 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
436 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
400 when "=p", "=!p", "!p"
437 when "=p", "=!p", "!p"
401 op = (operator == "!p" ? 'NOT IN' : 'IN')
438 op = (operator == "!p" ? 'NOT IN' : 'IN')
402 comp = (operator == "=!p" ? '<>' : '=')
439 comp = (operator == "=!p" ? '<>' : '=')
403 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
440 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
404 end
441 end
405
442
406 if relation_options[:sym] == field && !options[:reverse]
443 if relation_options[:sym] == field && !options[:reverse]
407 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
444 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
408 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
445 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
409 else
446 else
410 sql
447 sql
411 end
448 end
412 end
449 end
413
450
414 IssueRelation::TYPES.keys.each do |relation_type|
451 IssueRelation::TYPES.keys.each do |relation_type|
415 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
452 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
416 end
453 end
417 end
454 end
@@ -1,838 +1,853
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @inline = options.key?(:inline) ? options[:inline] : true
30 @inline = options.key?(:inline) ? options[:inline] : true
31 @caption_key = options[:caption] || "field_#{name}".to_sym
31 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @frozen = options[:frozen]
32 @frozen = options[:frozen]
33 end
33 end
34
34
35 def caption
35 def caption
36 @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key
36 @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key
37 end
37 end
38
38
39 # Returns true if the column is sortable, otherwise false
39 # Returns true if the column is sortable, otherwise false
40 def sortable?
40 def sortable?
41 !@sortable.nil?
41 !@sortable.nil?
42 end
42 end
43
43
44 def sortable
44 def sortable
45 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 @sortable.is_a?(Proc) ? @sortable.call : @sortable
46 end
46 end
47
47
48 def inline?
48 def inline?
49 @inline
49 @inline
50 end
50 end
51
51
52 def frozen?
52 def frozen?
53 @frozen
53 @frozen
54 end
54 end
55
55
56 def value(object)
56 def value(object)
57 object.send name
57 object.send name
58 end
58 end
59
59
60 def css_classes
60 def css_classes
61 name
61 name
62 end
62 end
63 end
63 end
64
64
65 class QueryCustomFieldColumn < QueryColumn
65 class QueryCustomFieldColumn < QueryColumn
66
66
67 def initialize(custom_field)
67 def initialize(custom_field)
68 self.name = "cf_#{custom_field.id}".to_sym
68 self.name = "cf_#{custom_field.id}".to_sym
69 self.sortable = custom_field.order_statement || false
69 self.sortable = custom_field.order_statement || false
70 self.groupable = custom_field.group_statement || false
70 self.groupable = custom_field.group_statement || false
71 @inline = true
71 @inline = true
72 @cf = custom_field
72 @cf = custom_field
73 end
73 end
74
74
75 def caption
75 def caption
76 @cf.name
76 @cf.name
77 end
77 end
78
78
79 def custom_field
79 def custom_field
80 @cf
80 @cf
81 end
81 end
82
82
83 def value(object)
83 def value(object)
84 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
84 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
85 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
85 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
86 end
86 end
87
87
88 def css_classes
88 def css_classes
89 @css_classes ||= "#{name} #{@cf.field_format}"
89 @css_classes ||= "#{name} #{@cf.field_format}"
90 end
90 end
91 end
91 end
92
92
93 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
93 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
94
94
95 def initialize(association, custom_field)
95 def initialize(association, custom_field)
96 super(custom_field)
96 super(custom_field)
97 self.name = "#{association}.cf_#{custom_field.id}".to_sym
97 self.name = "#{association}.cf_#{custom_field.id}".to_sym
98 # TODO: support sorting/grouping by association custom field
98 # TODO: support sorting/grouping by association custom field
99 self.sortable = false
99 self.sortable = false
100 self.groupable = false
100 self.groupable = false
101 @association = association
101 @association = association
102 end
102 end
103
103
104 def value(object)
104 def value(object)
105 if assoc = object.send(@association)
105 if assoc = object.send(@association)
106 super(assoc)
106 super(assoc)
107 end
107 end
108 end
108 end
109
109
110 def css_classes
110 def css_classes
111 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
111 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
112 end
112 end
113 end
113 end
114
114
115 class Query < ActiveRecord::Base
115 class Query < ActiveRecord::Base
116 class StatementInvalid < ::ActiveRecord::StatementInvalid
116 class StatementInvalid < ::ActiveRecord::StatementInvalid
117 end
117 end
118
118
119 VISIBILITY_PRIVATE = 0
120 VISIBILITY_ROLES = 1
121 VISIBILITY_PUBLIC = 2
122
119 belongs_to :project
123 belongs_to :project
120 belongs_to :user
124 belongs_to :user
125 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
121 serialize :filters
126 serialize :filters
122 serialize :column_names
127 serialize :column_names
123 serialize :sort_criteria, Array
128 serialize :sort_criteria, Array
124
129
125 attr_protected :project_id, :user_id
130 attr_protected :project_id, :user_id
126
131
127 validates_presence_of :name
132 validates_presence_of :name
128 validates_length_of :name, :maximum => 255
133 validates_length_of :name, :maximum => 255
134 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
129 validate :validate_query_filters
135 validate :validate_query_filters
136 validate do |query|
137 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
138 end
139
140 after_save do |query|
141 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
142 query.roles.clear
143 end
144 end
130
145
131 class_attribute :operators
146 class_attribute :operators
132 self.operators = {
147 self.operators = {
133 "=" => :label_equals,
148 "=" => :label_equals,
134 "!" => :label_not_equals,
149 "!" => :label_not_equals,
135 "o" => :label_open_issues,
150 "o" => :label_open_issues,
136 "c" => :label_closed_issues,
151 "c" => :label_closed_issues,
137 "!*" => :label_none,
152 "!*" => :label_none,
138 "*" => :label_any,
153 "*" => :label_any,
139 ">=" => :label_greater_or_equal,
154 ">=" => :label_greater_or_equal,
140 "<=" => :label_less_or_equal,
155 "<=" => :label_less_or_equal,
141 "><" => :label_between,
156 "><" => :label_between,
142 "<t+" => :label_in_less_than,
157 "<t+" => :label_in_less_than,
143 ">t+" => :label_in_more_than,
158 ">t+" => :label_in_more_than,
144 "><t+"=> :label_in_the_next_days,
159 "><t+"=> :label_in_the_next_days,
145 "t+" => :label_in,
160 "t+" => :label_in,
146 "t" => :label_today,
161 "t" => :label_today,
147 "ld" => :label_yesterday,
162 "ld" => :label_yesterday,
148 "w" => :label_this_week,
163 "w" => :label_this_week,
149 "lw" => :label_last_week,
164 "lw" => :label_last_week,
150 "l2w" => [:label_last_n_weeks, {:count => 2}],
165 "l2w" => [:label_last_n_weeks, {:count => 2}],
151 "m" => :label_this_month,
166 "m" => :label_this_month,
152 "lm" => :label_last_month,
167 "lm" => :label_last_month,
153 "y" => :label_this_year,
168 "y" => :label_this_year,
154 ">t-" => :label_less_than_ago,
169 ">t-" => :label_less_than_ago,
155 "<t-" => :label_more_than_ago,
170 "<t-" => :label_more_than_ago,
156 "><t-"=> :label_in_the_past_days,
171 "><t-"=> :label_in_the_past_days,
157 "t-" => :label_ago,
172 "t-" => :label_ago,
158 "~" => :label_contains,
173 "~" => :label_contains,
159 "!~" => :label_not_contains,
174 "!~" => :label_not_contains,
160 "=p" => :label_any_issues_in_project,
175 "=p" => :label_any_issues_in_project,
161 "=!p" => :label_any_issues_not_in_project,
176 "=!p" => :label_any_issues_not_in_project,
162 "!p" => :label_no_issues_in_project
177 "!p" => :label_no_issues_in_project
163 }
178 }
164
179
165 class_attribute :operators_by_filter_type
180 class_attribute :operators_by_filter_type
166 self.operators_by_filter_type = {
181 self.operators_by_filter_type = {
167 :list => [ "=", "!" ],
182 :list => [ "=", "!" ],
168 :list_status => [ "o", "=", "!", "c", "*" ],
183 :list_status => [ "o", "=", "!", "c", "*" ],
169 :list_optional => [ "=", "!", "!*", "*" ],
184 :list_optional => [ "=", "!", "!*", "*" ],
170 :list_subprojects => [ "*", "!*", "=" ],
185 :list_subprojects => [ "*", "!*", "=" ],
171 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
186 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
172 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
187 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
173 :string => [ "=", "~", "!", "!~", "!*", "*" ],
188 :string => [ "=", "~", "!", "!~", "!*", "*" ],
174 :text => [ "~", "!~", "!*", "*" ],
189 :text => [ "~", "!~", "!*", "*" ],
175 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
190 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
176 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
191 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
177 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
192 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
178 }
193 }
179
194
180 class_attribute :available_columns
195 class_attribute :available_columns
181 self.available_columns = []
196 self.available_columns = []
182
197
183 class_attribute :queried_class
198 class_attribute :queried_class
184
199
185 def queried_table_name
200 def queried_table_name
186 @queried_table_name ||= self.class.queried_class.table_name
201 @queried_table_name ||= self.class.queried_class.table_name
187 end
202 end
188
203
189 def initialize(attributes=nil, *args)
204 def initialize(attributes=nil, *args)
190 super attributes
205 super attributes
191 @is_for_all = project.nil?
206 @is_for_all = project.nil?
192 end
207 end
193
208
194 # Builds the query from the given params
209 # Builds the query from the given params
195 def build_from_params(params)
210 def build_from_params(params)
196 if params[:fields] || params[:f]
211 if params[:fields] || params[:f]
197 self.filters = {}
212 self.filters = {}
198 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
213 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
199 else
214 else
200 available_filters.keys.each do |field|
215 available_filters.keys.each do |field|
201 add_short_filter(field, params[field]) if params[field]
216 add_short_filter(field, params[field]) if params[field]
202 end
217 end
203 end
218 end
204 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
219 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
205 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
220 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
206 self
221 self
207 end
222 end
208
223
209 # Builds a new query from the given params and attributes
224 # Builds a new query from the given params and attributes
210 def self.build_from_params(params, attributes={})
225 def self.build_from_params(params, attributes={})
211 new(attributes).build_from_params(params)
226 new(attributes).build_from_params(params)
212 end
227 end
213
228
214 def validate_query_filters
229 def validate_query_filters
215 filters.each_key do |field|
230 filters.each_key do |field|
216 if values_for(field)
231 if values_for(field)
217 case type_for(field)
232 case type_for(field)
218 when :integer
233 when :integer
219 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
234 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
220 when :float
235 when :float
221 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
236 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
222 when :date, :date_past
237 when :date, :date_past
223 case operator_for(field)
238 case operator_for(field)
224 when "=", ">=", "<=", "><"
239 when "=", ">=", "<=", "><"
225 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
240 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
226 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
241 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
227 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
242 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
228 end
243 end
229 end
244 end
230 end
245 end
231
246
232 add_filter_error(field, :blank) unless
247 add_filter_error(field, :blank) unless
233 # filter requires one or more values
248 # filter requires one or more values
234 (values_for(field) and !values_for(field).first.blank?) or
249 (values_for(field) and !values_for(field).first.blank?) or
235 # filter doesn't require any value
250 # filter doesn't require any value
236 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
251 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
237 end if filters
252 end if filters
238 end
253 end
239
254
240 def add_filter_error(field, message)
255 def add_filter_error(field, message)
241 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
256 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
242 errors.add(:base, m)
257 errors.add(:base, m)
243 end
258 end
244
259
245 def editable_by?(user)
260 def editable_by?(user)
246 return false unless user
261 return false unless user
247 # Admin can edit them all and regular users can edit their private queries
262 # Admin can edit them all and regular users can edit their private queries
248 return true if user.admin? || (!is_public && self.user_id == user.id)
263 return true if user.admin? || (is_private? && self.user_id == user.id)
249 # Members can not edit public queries that are for all project (only admin is allowed to)
264 # Members can not edit public queries that are for all project (only admin is allowed to)
250 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
265 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
251 end
266 end
252
267
253 def trackers
268 def trackers
254 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
269 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
255 end
270 end
256
271
257 # Returns a hash of localized labels for all filter operators
272 # Returns a hash of localized labels for all filter operators
258 def self.operators_labels
273 def self.operators_labels
259 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
274 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
260 end
275 end
261
276
262 # Returns a representation of the available filters for JSON serialization
277 # Returns a representation of the available filters for JSON serialization
263 def available_filters_as_json
278 def available_filters_as_json
264 json = {}
279 json = {}
265 available_filters.each do |field, options|
280 available_filters.each do |field, options|
266 json[field] = options.slice(:type, :name, :values).stringify_keys
281 json[field] = options.slice(:type, :name, :values).stringify_keys
267 end
282 end
268 json
283 json
269 end
284 end
270
285
271 def all_projects
286 def all_projects
272 @all_projects ||= Project.visible.all
287 @all_projects ||= Project.visible.all
273 end
288 end
274
289
275 def all_projects_values
290 def all_projects_values
276 return @all_projects_values if @all_projects_values
291 return @all_projects_values if @all_projects_values
277
292
278 values = []
293 values = []
279 Project.project_tree(all_projects) do |p, level|
294 Project.project_tree(all_projects) do |p, level|
280 prefix = (level > 0 ? ('--' * level + ' ') : '')
295 prefix = (level > 0 ? ('--' * level + ' ') : '')
281 values << ["#{prefix}#{p.name}", p.id.to_s]
296 values << ["#{prefix}#{p.name}", p.id.to_s]
282 end
297 end
283 @all_projects_values = values
298 @all_projects_values = values
284 end
299 end
285
300
286 # Adds available filters
301 # Adds available filters
287 def initialize_available_filters
302 def initialize_available_filters
288 # implemented by sub-classes
303 # implemented by sub-classes
289 end
304 end
290 protected :initialize_available_filters
305 protected :initialize_available_filters
291
306
292 # Adds an available filter
307 # Adds an available filter
293 def add_available_filter(field, options)
308 def add_available_filter(field, options)
294 @available_filters ||= ActiveSupport::OrderedHash.new
309 @available_filters ||= ActiveSupport::OrderedHash.new
295 @available_filters[field] = options
310 @available_filters[field] = options
296 @available_filters
311 @available_filters
297 end
312 end
298
313
299 # Removes an available filter
314 # Removes an available filter
300 def delete_available_filter(field)
315 def delete_available_filter(field)
301 if @available_filters
316 if @available_filters
302 @available_filters.delete(field)
317 @available_filters.delete(field)
303 end
318 end
304 end
319 end
305
320
306 # Return a hash of available filters
321 # Return a hash of available filters
307 def available_filters
322 def available_filters
308 unless @available_filters
323 unless @available_filters
309 initialize_available_filters
324 initialize_available_filters
310 @available_filters.each do |field, options|
325 @available_filters.each do |field, options|
311 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
326 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
312 end
327 end
313 end
328 end
314 @available_filters
329 @available_filters
315 end
330 end
316
331
317 def add_filter(field, operator, values=nil)
332 def add_filter(field, operator, values=nil)
318 # values must be an array
333 # values must be an array
319 return unless values.nil? || values.is_a?(Array)
334 return unless values.nil? || values.is_a?(Array)
320 # check if field is defined as an available filter
335 # check if field is defined as an available filter
321 if available_filters.has_key? field
336 if available_filters.has_key? field
322 filter_options = available_filters[field]
337 filter_options = available_filters[field]
323 filters[field] = {:operator => operator, :values => (values || [''])}
338 filters[field] = {:operator => operator, :values => (values || [''])}
324 end
339 end
325 end
340 end
326
341
327 def add_short_filter(field, expression)
342 def add_short_filter(field, expression)
328 return unless expression && available_filters.has_key?(field)
343 return unless expression && available_filters.has_key?(field)
329 field_type = available_filters[field][:type]
344 field_type = available_filters[field][:type]
330 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
345 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
331 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
346 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
332 values = $1
347 values = $1
333 add_filter field, operator, values.present? ? values.split('|') : ['']
348 add_filter field, operator, values.present? ? values.split('|') : ['']
334 end || add_filter(field, '=', expression.split('|'))
349 end || add_filter(field, '=', expression.split('|'))
335 end
350 end
336
351
337 # Add multiple filters using +add_filter+
352 # Add multiple filters using +add_filter+
338 def add_filters(fields, operators, values)
353 def add_filters(fields, operators, values)
339 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
354 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
340 fields.each do |field|
355 fields.each do |field|
341 add_filter(field, operators[field], values && values[field])
356 add_filter(field, operators[field], values && values[field])
342 end
357 end
343 end
358 end
344 end
359 end
345
360
346 def has_filter?(field)
361 def has_filter?(field)
347 filters and filters[field]
362 filters and filters[field]
348 end
363 end
349
364
350 def type_for(field)
365 def type_for(field)
351 available_filters[field][:type] if available_filters.has_key?(field)
366 available_filters[field][:type] if available_filters.has_key?(field)
352 end
367 end
353
368
354 def operator_for(field)
369 def operator_for(field)
355 has_filter?(field) ? filters[field][:operator] : nil
370 has_filter?(field) ? filters[field][:operator] : nil
356 end
371 end
357
372
358 def values_for(field)
373 def values_for(field)
359 has_filter?(field) ? filters[field][:values] : nil
374 has_filter?(field) ? filters[field][:values] : nil
360 end
375 end
361
376
362 def value_for(field, index=0)
377 def value_for(field, index=0)
363 (values_for(field) || [])[index]
378 (values_for(field) || [])[index]
364 end
379 end
365
380
366 def label_for(field)
381 def label_for(field)
367 label = available_filters[field][:name] if available_filters.has_key?(field)
382 label = available_filters[field][:name] if available_filters.has_key?(field)
368 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
383 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
369 end
384 end
370
385
371 def self.add_available_column(column)
386 def self.add_available_column(column)
372 self.available_columns << (column) if column.is_a?(QueryColumn)
387 self.available_columns << (column) if column.is_a?(QueryColumn)
373 end
388 end
374
389
375 # Returns an array of columns that can be used to group the results
390 # Returns an array of columns that can be used to group the results
376 def groupable_columns
391 def groupable_columns
377 available_columns.select {|c| c.groupable}
392 available_columns.select {|c| c.groupable}
378 end
393 end
379
394
380 # Returns a Hash of columns and the key for sorting
395 # Returns a Hash of columns and the key for sorting
381 def sortable_columns
396 def sortable_columns
382 available_columns.inject({}) {|h, column|
397 available_columns.inject({}) {|h, column|
383 h[column.name.to_s] = column.sortable
398 h[column.name.to_s] = column.sortable
384 h
399 h
385 }
400 }
386 end
401 end
387
402
388 def columns
403 def columns
389 # preserve the column_names order
404 # preserve the column_names order
390 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
405 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
391 available_columns.find { |col| col.name == name }
406 available_columns.find { |col| col.name == name }
392 end.compact
407 end.compact
393 available_columns.select(&:frozen?) | cols
408 available_columns.select(&:frozen?) | cols
394 end
409 end
395
410
396 def inline_columns
411 def inline_columns
397 columns.select(&:inline?)
412 columns.select(&:inline?)
398 end
413 end
399
414
400 def block_columns
415 def block_columns
401 columns.reject(&:inline?)
416 columns.reject(&:inline?)
402 end
417 end
403
418
404 def available_inline_columns
419 def available_inline_columns
405 available_columns.select(&:inline?)
420 available_columns.select(&:inline?)
406 end
421 end
407
422
408 def available_block_columns
423 def available_block_columns
409 available_columns.reject(&:inline?)
424 available_columns.reject(&:inline?)
410 end
425 end
411
426
412 def default_columns_names
427 def default_columns_names
413 []
428 []
414 end
429 end
415
430
416 def column_names=(names)
431 def column_names=(names)
417 if names
432 if names
418 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
433 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
419 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
434 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
420 # Set column_names to nil if default columns
435 # Set column_names to nil if default columns
421 if names == default_columns_names
436 if names == default_columns_names
422 names = nil
437 names = nil
423 end
438 end
424 end
439 end
425 write_attribute(:column_names, names)
440 write_attribute(:column_names, names)
426 end
441 end
427
442
428 def has_column?(column)
443 def has_column?(column)
429 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
444 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
430 end
445 end
431
446
432 def has_default_columns?
447 def has_default_columns?
433 column_names.nil? || column_names.empty?
448 column_names.nil? || column_names.empty?
434 end
449 end
435
450
436 def sort_criteria=(arg)
451 def sort_criteria=(arg)
437 c = []
452 c = []
438 if arg.is_a?(Hash)
453 if arg.is_a?(Hash)
439 arg = arg.keys.sort.collect {|k| arg[k]}
454 arg = arg.keys.sort.collect {|k| arg[k]}
440 end
455 end
441 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
456 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
442 write_attribute(:sort_criteria, c)
457 write_attribute(:sort_criteria, c)
443 end
458 end
444
459
445 def sort_criteria
460 def sort_criteria
446 read_attribute(:sort_criteria) || []
461 read_attribute(:sort_criteria) || []
447 end
462 end
448
463
449 def sort_criteria_key(arg)
464 def sort_criteria_key(arg)
450 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
465 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
451 end
466 end
452
467
453 def sort_criteria_order(arg)
468 def sort_criteria_order(arg)
454 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
469 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
455 end
470 end
456
471
457 def sort_criteria_order_for(key)
472 def sort_criteria_order_for(key)
458 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
473 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
459 end
474 end
460
475
461 # Returns the SQL sort order that should be prepended for grouping
476 # Returns the SQL sort order that should be prepended for grouping
462 def group_by_sort_order
477 def group_by_sort_order
463 if grouped? && (column = group_by_column)
478 if grouped? && (column = group_by_column)
464 order = sort_criteria_order_for(column.name) || column.default_order
479 order = sort_criteria_order_for(column.name) || column.default_order
465 column.sortable.is_a?(Array) ?
480 column.sortable.is_a?(Array) ?
466 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
481 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
467 "#{column.sortable} #{order}"
482 "#{column.sortable} #{order}"
468 end
483 end
469 end
484 end
470
485
471 # Returns true if the query is a grouped query
486 # Returns true if the query is a grouped query
472 def grouped?
487 def grouped?
473 !group_by_column.nil?
488 !group_by_column.nil?
474 end
489 end
475
490
476 def group_by_column
491 def group_by_column
477 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
492 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
478 end
493 end
479
494
480 def group_by_statement
495 def group_by_statement
481 group_by_column.try(:groupable)
496 group_by_column.try(:groupable)
482 end
497 end
483
498
484 def project_statement
499 def project_statement
485 project_clauses = []
500 project_clauses = []
486 if project && !project.descendants.active.empty?
501 if project && !project.descendants.active.empty?
487 ids = [project.id]
502 ids = [project.id]
488 if has_filter?("subproject_id")
503 if has_filter?("subproject_id")
489 case operator_for("subproject_id")
504 case operator_for("subproject_id")
490 when '='
505 when '='
491 # include the selected subprojects
506 # include the selected subprojects
492 ids += values_for("subproject_id").each(&:to_i)
507 ids += values_for("subproject_id").each(&:to_i)
493 when '!*'
508 when '!*'
494 # main project only
509 # main project only
495 else
510 else
496 # all subprojects
511 # all subprojects
497 ids += project.descendants.collect(&:id)
512 ids += project.descendants.collect(&:id)
498 end
513 end
499 elsif Setting.display_subprojects_issues?
514 elsif Setting.display_subprojects_issues?
500 ids += project.descendants.collect(&:id)
515 ids += project.descendants.collect(&:id)
501 end
516 end
502 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
517 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
503 elsif project
518 elsif project
504 project_clauses << "#{Project.table_name}.id = %d" % project.id
519 project_clauses << "#{Project.table_name}.id = %d" % project.id
505 end
520 end
506 project_clauses.any? ? project_clauses.join(' AND ') : nil
521 project_clauses.any? ? project_clauses.join(' AND ') : nil
507 end
522 end
508
523
509 def statement
524 def statement
510 # filters clauses
525 # filters clauses
511 filters_clauses = []
526 filters_clauses = []
512 filters.each_key do |field|
527 filters.each_key do |field|
513 next if field == "subproject_id"
528 next if field == "subproject_id"
514 v = values_for(field).clone
529 v = values_for(field).clone
515 next unless v and !v.empty?
530 next unless v and !v.empty?
516 operator = operator_for(field)
531 operator = operator_for(field)
517
532
518 # "me" value subsitution
533 # "me" value subsitution
519 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
534 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
520 if v.delete("me")
535 if v.delete("me")
521 if User.current.logged?
536 if User.current.logged?
522 v.push(User.current.id.to_s)
537 v.push(User.current.id.to_s)
523 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
538 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
524 else
539 else
525 v.push("0")
540 v.push("0")
526 end
541 end
527 end
542 end
528 end
543 end
529
544
530 if field == 'project_id'
545 if field == 'project_id'
531 if v.delete('mine')
546 if v.delete('mine')
532 v += User.current.memberships.map(&:project_id).map(&:to_s)
547 v += User.current.memberships.map(&:project_id).map(&:to_s)
533 end
548 end
534 end
549 end
535
550
536 if field =~ /cf_(\d+)$/
551 if field =~ /cf_(\d+)$/
537 # custom field
552 # custom field
538 filters_clauses << sql_for_custom_field(field, operator, v, $1)
553 filters_clauses << sql_for_custom_field(field, operator, v, $1)
539 elsif respond_to?("sql_for_#{field}_field")
554 elsif respond_to?("sql_for_#{field}_field")
540 # specific statement
555 # specific statement
541 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
556 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
542 else
557 else
543 # regular field
558 # regular field
544 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
559 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
545 end
560 end
546 end if filters and valid?
561 end if filters and valid?
547
562
548 filters_clauses << project_statement
563 filters_clauses << project_statement
549 filters_clauses.reject!(&:blank?)
564 filters_clauses.reject!(&:blank?)
550
565
551 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
566 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
552 end
567 end
553
568
554 private
569 private
555
570
556 def sql_for_custom_field(field, operator, value, custom_field_id)
571 def sql_for_custom_field(field, operator, value, custom_field_id)
557 db_table = CustomValue.table_name
572 db_table = CustomValue.table_name
558 db_field = 'value'
573 db_field = 'value'
559 filter = @available_filters[field]
574 filter = @available_filters[field]
560 return nil unless filter
575 return nil unless filter
561 if filter[:format] == 'user'
576 if filter[:format] == 'user'
562 if value.delete('me')
577 if value.delete('me')
563 value.push User.current.id.to_s
578 value.push User.current.id.to_s
564 end
579 end
565 end
580 end
566 not_in = nil
581 not_in = nil
567 if operator == '!'
582 if operator == '!'
568 # Makes ! operator work for custom fields with multiple values
583 # Makes ! operator work for custom fields with multiple values
569 operator = '='
584 operator = '='
570 not_in = 'NOT'
585 not_in = 'NOT'
571 end
586 end
572 customized_key = "id"
587 customized_key = "id"
573 customized_class = queried_class
588 customized_class = queried_class
574 if field =~ /^(.+)\.cf_/
589 if field =~ /^(.+)\.cf_/
575 assoc = $1
590 assoc = $1
576 customized_key = "#{assoc}_id"
591 customized_key = "#{assoc}_id"
577 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
592 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
578 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
593 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
579 end
594 end
580 where = sql_for_field(field, operator, value, db_table, db_field, true)
595 where = sql_for_field(field, operator, value, db_table, db_field, true)
581 if operator =~ /[<>]/
596 if operator =~ /[<>]/
582 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
597 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
583 end
598 end
584 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE #{where})"
599 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE #{where})"
585 end
600 end
586
601
587 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
602 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
588 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
603 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
589 sql = ''
604 sql = ''
590 case operator
605 case operator
591 when "="
606 when "="
592 if value.any?
607 if value.any?
593 case type_for(field)
608 case type_for(field)
594 when :date, :date_past
609 when :date, :date_past
595 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
610 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
596 when :integer
611 when :integer
597 if is_custom_filter
612 if is_custom_filter
598 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
613 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
599 else
614 else
600 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
615 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
601 end
616 end
602 when :float
617 when :float
603 if is_custom_filter
618 if is_custom_filter
604 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
619 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
605 else
620 else
606 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
621 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
607 end
622 end
608 else
623 else
609 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
624 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
610 end
625 end
611 else
626 else
612 # IN an empty set
627 # IN an empty set
613 sql = "1=0"
628 sql = "1=0"
614 end
629 end
615 when "!"
630 when "!"
616 if value.any?
631 if value.any?
617 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
632 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
618 else
633 else
619 # NOT IN an empty set
634 # NOT IN an empty set
620 sql = "1=1"
635 sql = "1=1"
621 end
636 end
622 when "!*"
637 when "!*"
623 sql = "#{db_table}.#{db_field} IS NULL"
638 sql = "#{db_table}.#{db_field} IS NULL"
624 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
639 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
625 when "*"
640 when "*"
626 sql = "#{db_table}.#{db_field} IS NOT NULL"
641 sql = "#{db_table}.#{db_field} IS NOT NULL"
627 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
642 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
628 when ">="
643 when ">="
629 if [:date, :date_past].include?(type_for(field))
644 if [:date, :date_past].include?(type_for(field))
630 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
645 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
631 else
646 else
632 if is_custom_filter
647 if is_custom_filter
633 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
648 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
634 else
649 else
635 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
650 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
636 end
651 end
637 end
652 end
638 when "<="
653 when "<="
639 if [:date, :date_past].include?(type_for(field))
654 if [:date, :date_past].include?(type_for(field))
640 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
655 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
641 else
656 else
642 if is_custom_filter
657 if is_custom_filter
643 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
658 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
644 else
659 else
645 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
660 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
646 end
661 end
647 end
662 end
648 when "><"
663 when "><"
649 if [:date, :date_past].include?(type_for(field))
664 if [:date, :date_past].include?(type_for(field))
650 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
665 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
651 else
666 else
652 if is_custom_filter
667 if is_custom_filter
653 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
668 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
654 else
669 else
655 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
670 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
656 end
671 end
657 end
672 end
658 when "o"
673 when "o"
659 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
674 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
660 when "c"
675 when "c"
661 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
676 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
662 when "><t-"
677 when "><t-"
663 # between today - n days and today
678 # between today - n days and today
664 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
679 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
665 when ">t-"
680 when ">t-"
666 # >= today - n days
681 # >= today - n days
667 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
682 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
668 when "<t-"
683 when "<t-"
669 # <= today - n days
684 # <= today - n days
670 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
685 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
671 when "t-"
686 when "t-"
672 # = n days in past
687 # = n days in past
673 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
688 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
674 when "><t+"
689 when "><t+"
675 # between today and today + n days
690 # between today and today + n days
676 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
691 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
677 when ">t+"
692 when ">t+"
678 # >= today + n days
693 # >= today + n days
679 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
694 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
680 when "<t+"
695 when "<t+"
681 # <= today + n days
696 # <= today + n days
682 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
697 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
683 when "t+"
698 when "t+"
684 # = today + n days
699 # = today + n days
685 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
700 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
686 when "t"
701 when "t"
687 # = today
702 # = today
688 sql = relative_date_clause(db_table, db_field, 0, 0)
703 sql = relative_date_clause(db_table, db_field, 0, 0)
689 when "ld"
704 when "ld"
690 # = yesterday
705 # = yesterday
691 sql = relative_date_clause(db_table, db_field, -1, -1)
706 sql = relative_date_clause(db_table, db_field, -1, -1)
692 when "w"
707 when "w"
693 # = this week
708 # = this week
694 first_day_of_week = l(:general_first_day_of_week).to_i
709 first_day_of_week = l(:general_first_day_of_week).to_i
695 day_of_week = Date.today.cwday
710 day_of_week = Date.today.cwday
696 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
711 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
697 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
712 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
698 when "lw"
713 when "lw"
699 # = last week
714 # = last week
700 first_day_of_week = l(:general_first_day_of_week).to_i
715 first_day_of_week = l(:general_first_day_of_week).to_i
701 day_of_week = Date.today.cwday
716 day_of_week = Date.today.cwday
702 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
717 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
703 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
718 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
704 when "l2w"
719 when "l2w"
705 # = last 2 weeks
720 # = last 2 weeks
706 first_day_of_week = l(:general_first_day_of_week).to_i
721 first_day_of_week = l(:general_first_day_of_week).to_i
707 day_of_week = Date.today.cwday
722 day_of_week = Date.today.cwday
708 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
723 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
709 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
724 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
710 when "m"
725 when "m"
711 # = this month
726 # = this month
712 date = Date.today
727 date = Date.today
713 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
728 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
714 when "lm"
729 when "lm"
715 # = last month
730 # = last month
716 date = Date.today.prev_month
731 date = Date.today.prev_month
717 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
732 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
718 when "y"
733 when "y"
719 # = this year
734 # = this year
720 date = Date.today
735 date = Date.today
721 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
736 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
722 when "~"
737 when "~"
723 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
738 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
724 when "!~"
739 when "!~"
725 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
740 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
726 else
741 else
727 raise "Unknown query operator #{operator}"
742 raise "Unknown query operator #{operator}"
728 end
743 end
729
744
730 return sql
745 return sql
731 end
746 end
732
747
733 # Adds a filter for the given custom field
748 # Adds a filter for the given custom field
734 def add_custom_field_filter(field, assoc=nil)
749 def add_custom_field_filter(field, assoc=nil)
735 case field.field_format
750 case field.field_format
736 when "text"
751 when "text"
737 options = { :type => :text }
752 options = { :type => :text }
738 when "list"
753 when "list"
739 options = { :type => :list_optional, :values => field.possible_values }
754 options = { :type => :list_optional, :values => field.possible_values }
740 when "date"
755 when "date"
741 options = { :type => :date }
756 options = { :type => :date }
742 when "bool"
757 when "bool"
743 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
758 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
744 when "int"
759 when "int"
745 options = { :type => :integer }
760 options = { :type => :integer }
746 when "float"
761 when "float"
747 options = { :type => :float }
762 options = { :type => :float }
748 when "user", "version"
763 when "user", "version"
749 return unless project
764 return unless project
750 values = field.possible_values_options(project)
765 values = field.possible_values_options(project)
751 if User.current.logged? && field.field_format == 'user'
766 if User.current.logged? && field.field_format == 'user'
752 values.unshift ["<< #{l(:label_me)} >>", "me"]
767 values.unshift ["<< #{l(:label_me)} >>", "me"]
753 end
768 end
754 options = { :type => :list_optional, :values => values }
769 options = { :type => :list_optional, :values => values }
755 else
770 else
756 options = { :type => :string }
771 options = { :type => :string }
757 end
772 end
758 filter_id = "cf_#{field.id}"
773 filter_id = "cf_#{field.id}"
759 filter_name = field.name
774 filter_name = field.name
760 if assoc.present?
775 if assoc.present?
761 filter_id = "#{assoc}.#{filter_id}"
776 filter_id = "#{assoc}.#{filter_id}"
762 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
777 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
763 end
778 end
764 add_available_filter filter_id, options.merge({
779 add_available_filter filter_id, options.merge({
765 :name => filter_name,
780 :name => filter_name,
766 :format => field.field_format,
781 :format => field.field_format,
767 :field => field
782 :field => field
768 })
783 })
769 end
784 end
770
785
771 # Adds filters for the given custom fields scope
786 # Adds filters for the given custom fields scope
772 def add_custom_fields_filters(scope, assoc=nil)
787 def add_custom_fields_filters(scope, assoc=nil)
773 scope.where(:is_filter => true).sorted.each do |field|
788 scope.where(:is_filter => true).sorted.each do |field|
774 add_custom_field_filter(field, assoc)
789 add_custom_field_filter(field, assoc)
775 end
790 end
776 end
791 end
777
792
778 # Adds filters for the given associations custom fields
793 # Adds filters for the given associations custom fields
779 def add_associations_custom_fields_filters(*associations)
794 def add_associations_custom_fields_filters(*associations)
780 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
795 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
781 associations.each do |assoc|
796 associations.each do |assoc|
782 association_klass = queried_class.reflect_on_association(assoc).klass
797 association_klass = queried_class.reflect_on_association(assoc).klass
783 fields_by_class.each do |field_class, fields|
798 fields_by_class.each do |field_class, fields|
784 if field_class.customized_class <= association_klass
799 if field_class.customized_class <= association_klass
785 fields.sort.each do |field|
800 fields.sort.each do |field|
786 add_custom_field_filter(field, assoc)
801 add_custom_field_filter(field, assoc)
787 end
802 end
788 end
803 end
789 end
804 end
790 end
805 end
791 end
806 end
792
807
793 # Returns a SQL clause for a date or datetime field.
808 # Returns a SQL clause for a date or datetime field.
794 def date_clause(table, field, from, to)
809 def date_clause(table, field, from, to)
795 s = []
810 s = []
796 if from
811 if from
797 from_yesterday = from - 1
812 from_yesterday = from - 1
798 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
813 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
799 if self.class.default_timezone == :utc
814 if self.class.default_timezone == :utc
800 from_yesterday_time = from_yesterday_time.utc
815 from_yesterday_time = from_yesterday_time.utc
801 end
816 end
802 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
817 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
803 end
818 end
804 if to
819 if to
805 to_time = Time.local(to.year, to.month, to.day)
820 to_time = Time.local(to.year, to.month, to.day)
806 if self.class.default_timezone == :utc
821 if self.class.default_timezone == :utc
807 to_time = to_time.utc
822 to_time = to_time.utc
808 end
823 end
809 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
824 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
810 end
825 end
811 s.join(' AND ')
826 s.join(' AND ')
812 end
827 end
813
828
814 # Returns a SQL clause for a date or datetime field using relative dates.
829 # Returns a SQL clause for a date or datetime field using relative dates.
815 def relative_date_clause(table, field, days_from, days_to)
830 def relative_date_clause(table, field, days_from, days_to)
816 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
831 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
817 end
832 end
818
833
819 # Additional joins required for the given sort options
834 # Additional joins required for the given sort options
820 def joins_for_order_statement(order_options)
835 def joins_for_order_statement(order_options)
821 joins = []
836 joins = []
822
837
823 if order_options
838 if order_options
824 if order_options.include?('authors')
839 if order_options.include?('authors')
825 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
840 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
826 end
841 end
827 order_options.scan(/cf_\d+/).uniq.each do |name|
842 order_options.scan(/cf_\d+/).uniq.each do |name|
828 column = available_columns.detect {|c| c.name.to_s == name}
843 column = available_columns.detect {|c| c.name.to_s == name}
829 join = column && column.custom_field.join_for_order_statement
844 join = column && column.custom_field.join_for_order_statement
830 if join
845 if join
831 joins << join
846 joins << join
832 end
847 end
833 end
848 end
834 end
849 end
835
850
836 joins.any? ? joins.join(' ') : nil
851 joins.any? ? joins.join(' ') : nil
837 end
852 end
838 end
853 end
@@ -1,734 +1,734
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstname => {
35 :firstname => {
36 :string => '#{firstname}',
36 :string => '#{firstname}',
37 :order => %w(firstname id),
37 :order => %w(firstname id),
38 :setting_order => 3
38 :setting_order => 3
39 },
39 },
40 :lastname_firstname => {
40 :lastname_firstname => {
41 :string => '#{lastname} #{firstname}',
41 :string => '#{lastname} #{firstname}',
42 :order => %w(lastname firstname id),
42 :order => %w(lastname firstname id),
43 :setting_order => 4
43 :setting_order => 4
44 },
44 },
45 :lastname_coma_firstname => {
45 :lastname_coma_firstname => {
46 :string => '#{lastname}, #{firstname}',
46 :string => '#{lastname}, #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 5
48 :setting_order => 5
49 },
49 },
50 :lastname => {
50 :lastname => {
51 :string => '#{lastname}',
51 :string => '#{lastname}',
52 :order => %w(lastname id),
52 :order => %w(lastname id),
53 :setting_order => 6
53 :setting_order => 6
54 },
54 },
55 :username => {
55 :username => {
56 :string => '#{login}',
56 :string => '#{login}',
57 :order => %w(login id),
57 :order => %w(login id),
58 :setting_order => 7
58 :setting_order => 7
59 },
59 },
60 }
60 }
61
61
62 MAIL_NOTIFICATION_OPTIONS = [
62 MAIL_NOTIFICATION_OPTIONS = [
63 ['all', :label_user_mail_option_all],
63 ['all', :label_user_mail_option_all],
64 ['selected', :label_user_mail_option_selected],
64 ['selected', :label_user_mail_option_selected],
65 ['only_my_events', :label_user_mail_option_only_my_events],
65 ['only_my_events', :label_user_mail_option_only_my_events],
66 ['only_assigned', :label_user_mail_option_only_assigned],
66 ['only_assigned', :label_user_mail_option_only_assigned],
67 ['only_owner', :label_user_mail_option_only_owner],
67 ['only_owner', :label_user_mail_option_only_owner],
68 ['none', :label_user_mail_option_none]
68 ['none', :label_user_mail_option_none]
69 ]
69 ]
70
70
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 has_many :changesets, :dependent => :nullify
73 has_many :changesets, :dependent => :nullify
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 belongs_to :auth_source
77 belongs_to :auth_source
78
78
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81
81
82 acts_as_customizable
82 acts_as_customizable
83
83
84 attr_accessor :password, :password_confirmation, :generate_password
84 attr_accessor :password, :password_confirmation, :generate_password
85 attr_accessor :last_before_login_on
85 attr_accessor :last_before_login_on
86 # Prevents unauthorized assignments
86 # Prevents unauthorized assignments
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88
88
89 LOGIN_LENGTH_LIMIT = 60
89 LOGIN_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
91
91
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 # Login must contain letters, numbers, underscores only
95 # Login must contain letters, numbers, underscores only
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 validates_length_of :firstname, :lastname, :maximum => 30
98 validates_length_of :firstname, :lastname, :maximum => 30
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 validate :validate_password_length
103 validate :validate_password_length
104
104
105 before_create :set_mail_notification
105 before_create :set_mail_notification
106 before_save :generate_password_if_needed, :update_hashed_password
106 before_save :generate_password_if_needed, :update_hashed_password
107 before_destroy :remove_references_before_destroy
107 before_destroy :remove_references_before_destroy
108 after_save :update_notified_project_ids
108 after_save :update_notified_project_ids
109
109
110 scope :in_group, lambda {|group|
110 scope :in_group, lambda {|group|
111 group_id = group.is_a?(Group) ? group.id : group.to_i
111 group_id = group.is_a?(Group) ? group.id : group.to_i
112 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
112 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
113 }
113 }
114 scope :not_in_group, lambda {|group|
114 scope :not_in_group, lambda {|group|
115 group_id = group.is_a?(Group) ? group.id : group.to_i
115 group_id = group.is_a?(Group) ? group.id : group.to_i
116 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
116 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
117 }
117 }
118 scope :sorted, lambda { order(*User.fields_for_order_statement)}
118 scope :sorted, lambda { order(*User.fields_for_order_statement)}
119
119
120 def set_mail_notification
120 def set_mail_notification
121 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
121 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
122 true
122 true
123 end
123 end
124
124
125 def update_hashed_password
125 def update_hashed_password
126 # update hashed_password if password was set
126 # update hashed_password if password was set
127 if self.password && self.auth_source_id.blank?
127 if self.password && self.auth_source_id.blank?
128 salt_password(password)
128 salt_password(password)
129 end
129 end
130 end
130 end
131
131
132 alias :base_reload :reload
132 alias :base_reload :reload
133 def reload(*args)
133 def reload(*args)
134 @name = nil
134 @name = nil
135 @projects_by_role = nil
135 @projects_by_role = nil
136 @membership_by_project_id = nil
136 @membership_by_project_id = nil
137 @notified_projects_ids = nil
137 @notified_projects_ids = nil
138 @notified_projects_ids_changed = false
138 @notified_projects_ids_changed = false
139 @builtin_role = nil
139 @builtin_role = nil
140 base_reload(*args)
140 base_reload(*args)
141 end
141 end
142
142
143 def mail=(arg)
143 def mail=(arg)
144 write_attribute(:mail, arg.to_s.strip)
144 write_attribute(:mail, arg.to_s.strip)
145 end
145 end
146
146
147 def identity_url=(url)
147 def identity_url=(url)
148 if url.blank?
148 if url.blank?
149 write_attribute(:identity_url, '')
149 write_attribute(:identity_url, '')
150 else
150 else
151 begin
151 begin
152 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
152 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
153 rescue OpenIdAuthentication::InvalidOpenId
153 rescue OpenIdAuthentication::InvalidOpenId
154 # Invalid url, don't save
154 # Invalid url, don't save
155 end
155 end
156 end
156 end
157 self.read_attribute(:identity_url)
157 self.read_attribute(:identity_url)
158 end
158 end
159
159
160 # Returns the user that matches provided login and password, or nil
160 # Returns the user that matches provided login and password, or nil
161 def self.try_to_login(login, password, active_only=true)
161 def self.try_to_login(login, password, active_only=true)
162 login = login.to_s
162 login = login.to_s
163 password = password.to_s
163 password = password.to_s
164
164
165 # Make sure no one can sign in with an empty login or password
165 # Make sure no one can sign in with an empty login or password
166 return nil if login.empty? || password.empty?
166 return nil if login.empty? || password.empty?
167 user = find_by_login(login)
167 user = find_by_login(login)
168 if user
168 if user
169 # user is already in local database
169 # user is already in local database
170 return nil unless user.check_password?(password)
170 return nil unless user.check_password?(password)
171 return nil if !user.active? && active_only
171 return nil if !user.active? && active_only
172 else
172 else
173 # user is not yet registered, try to authenticate with available sources
173 # user is not yet registered, try to authenticate with available sources
174 attrs = AuthSource.authenticate(login, password)
174 attrs = AuthSource.authenticate(login, password)
175 if attrs
175 if attrs
176 user = new(attrs)
176 user = new(attrs)
177 user.login = login
177 user.login = login
178 user.language = Setting.default_language
178 user.language = Setting.default_language
179 if user.save
179 if user.save
180 user.reload
180 user.reload
181 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
181 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
182 end
182 end
183 end
183 end
184 end
184 end
185 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
185 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
186 user
186 user
187 rescue => text
187 rescue => text
188 raise text
188 raise text
189 end
189 end
190
190
191 # Returns the user who matches the given autologin +key+ or nil
191 # Returns the user who matches the given autologin +key+ or nil
192 def self.try_to_autologin(key)
192 def self.try_to_autologin(key)
193 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
193 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
194 if user
194 if user
195 user.update_column(:last_login_on, Time.now)
195 user.update_column(:last_login_on, Time.now)
196 user
196 user
197 end
197 end
198 end
198 end
199
199
200 def self.name_formatter(formatter = nil)
200 def self.name_formatter(formatter = nil)
201 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
201 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
202 end
202 end
203
203
204 # Returns an array of fields names than can be used to make an order statement for users
204 # Returns an array of fields names than can be used to make an order statement for users
205 # according to how user names are displayed
205 # according to how user names are displayed
206 # Examples:
206 # Examples:
207 #
207 #
208 # User.fields_for_order_statement => ['users.login', 'users.id']
208 # User.fields_for_order_statement => ['users.login', 'users.id']
209 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
209 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
210 def self.fields_for_order_statement(table=nil)
210 def self.fields_for_order_statement(table=nil)
211 table ||= table_name
211 table ||= table_name
212 name_formatter[:order].map {|field| "#{table}.#{field}"}
212 name_formatter[:order].map {|field| "#{table}.#{field}"}
213 end
213 end
214
214
215 # Return user's full name for display
215 # Return user's full name for display
216 def name(formatter = nil)
216 def name(formatter = nil)
217 f = self.class.name_formatter(formatter)
217 f = self.class.name_formatter(formatter)
218 if formatter
218 if formatter
219 eval('"' + f[:string] + '"')
219 eval('"' + f[:string] + '"')
220 else
220 else
221 @name ||= eval('"' + f[:string] + '"')
221 @name ||= eval('"' + f[:string] + '"')
222 end
222 end
223 end
223 end
224
224
225 def active?
225 def active?
226 self.status == STATUS_ACTIVE
226 self.status == STATUS_ACTIVE
227 end
227 end
228
228
229 def registered?
229 def registered?
230 self.status == STATUS_REGISTERED
230 self.status == STATUS_REGISTERED
231 end
231 end
232
232
233 def locked?
233 def locked?
234 self.status == STATUS_LOCKED
234 self.status == STATUS_LOCKED
235 end
235 end
236
236
237 def activate
237 def activate
238 self.status = STATUS_ACTIVE
238 self.status = STATUS_ACTIVE
239 end
239 end
240
240
241 def register
241 def register
242 self.status = STATUS_REGISTERED
242 self.status = STATUS_REGISTERED
243 end
243 end
244
244
245 def lock
245 def lock
246 self.status = STATUS_LOCKED
246 self.status = STATUS_LOCKED
247 end
247 end
248
248
249 def activate!
249 def activate!
250 update_attribute(:status, STATUS_ACTIVE)
250 update_attribute(:status, STATUS_ACTIVE)
251 end
251 end
252
252
253 def register!
253 def register!
254 update_attribute(:status, STATUS_REGISTERED)
254 update_attribute(:status, STATUS_REGISTERED)
255 end
255 end
256
256
257 def lock!
257 def lock!
258 update_attribute(:status, STATUS_LOCKED)
258 update_attribute(:status, STATUS_LOCKED)
259 end
259 end
260
260
261 # Returns true if +clear_password+ is the correct user's password, otherwise false
261 # Returns true if +clear_password+ is the correct user's password, otherwise false
262 def check_password?(clear_password)
262 def check_password?(clear_password)
263 if auth_source_id.present?
263 if auth_source_id.present?
264 auth_source.authenticate(self.login, clear_password)
264 auth_source.authenticate(self.login, clear_password)
265 else
265 else
266 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
266 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
267 end
267 end
268 end
268 end
269
269
270 # Generates a random salt and computes hashed_password for +clear_password+
270 # Generates a random salt and computes hashed_password for +clear_password+
271 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
271 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
272 def salt_password(clear_password)
272 def salt_password(clear_password)
273 self.salt = User.generate_salt
273 self.salt = User.generate_salt
274 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
274 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
275 end
275 end
276
276
277 # Does the backend storage allow this user to change their password?
277 # Does the backend storage allow this user to change their password?
278 def change_password_allowed?
278 def change_password_allowed?
279 return true if auth_source.nil?
279 return true if auth_source.nil?
280 return auth_source.allow_password_changes?
280 return auth_source.allow_password_changes?
281 end
281 end
282
282
283 def generate_password?
283 def generate_password?
284 generate_password == '1' || generate_password == true
284 generate_password == '1' || generate_password == true
285 end
285 end
286
286
287 # Generate and set a random password on given length
287 # Generate and set a random password on given length
288 def random_password(length=40)
288 def random_password(length=40)
289 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
289 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
290 chars -= %w(0 O 1 l)
290 chars -= %w(0 O 1 l)
291 password = ''
291 password = ''
292 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
292 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
293 self.password = password
293 self.password = password
294 self.password_confirmation = password
294 self.password_confirmation = password
295 self
295 self
296 end
296 end
297
297
298 def pref
298 def pref
299 self.preference ||= UserPreference.new(:user => self)
299 self.preference ||= UserPreference.new(:user => self)
300 end
300 end
301
301
302 def time_zone
302 def time_zone
303 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
303 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
304 end
304 end
305
305
306 def wants_comments_in_reverse_order?
306 def wants_comments_in_reverse_order?
307 self.pref[:comments_sorting] == 'desc'
307 self.pref[:comments_sorting] == 'desc'
308 end
308 end
309
309
310 # Return user's RSS key (a 40 chars long string), used to access feeds
310 # Return user's RSS key (a 40 chars long string), used to access feeds
311 def rss_key
311 def rss_key
312 if rss_token.nil?
312 if rss_token.nil?
313 create_rss_token(:action => 'feeds')
313 create_rss_token(:action => 'feeds')
314 end
314 end
315 rss_token.value
315 rss_token.value
316 end
316 end
317
317
318 # Return user's API key (a 40 chars long string), used to access the API
318 # Return user's API key (a 40 chars long string), used to access the API
319 def api_key
319 def api_key
320 if api_token.nil?
320 if api_token.nil?
321 create_api_token(:action => 'api')
321 create_api_token(:action => 'api')
322 end
322 end
323 api_token.value
323 api_token.value
324 end
324 end
325
325
326 # Return an array of project ids for which the user has explicitly turned mail notifications on
326 # Return an array of project ids for which the user has explicitly turned mail notifications on
327 def notified_projects_ids
327 def notified_projects_ids
328 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
328 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
329 end
329 end
330
330
331 def notified_project_ids=(ids)
331 def notified_project_ids=(ids)
332 @notified_projects_ids_changed = true
332 @notified_projects_ids_changed = true
333 @notified_projects_ids = ids
333 @notified_projects_ids = ids
334 end
334 end
335
335
336 # Updates per project notifications (after_save callback)
336 # Updates per project notifications (after_save callback)
337 def update_notified_project_ids
337 def update_notified_project_ids
338 if @notified_projects_ids_changed
338 if @notified_projects_ids_changed
339 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
339 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
340 members.update_all(:mail_notification => false)
340 members.update_all(:mail_notification => false)
341 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
341 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
342 end
342 end
343 end
343 end
344 private :update_notified_project_ids
344 private :update_notified_project_ids
345
345
346 def valid_notification_options
346 def valid_notification_options
347 self.class.valid_notification_options(self)
347 self.class.valid_notification_options(self)
348 end
348 end
349
349
350 # Only users that belong to more than 1 project can select projects for which they are notified
350 # Only users that belong to more than 1 project can select projects for which they are notified
351 def self.valid_notification_options(user=nil)
351 def self.valid_notification_options(user=nil)
352 # Note that @user.membership.size would fail since AR ignores
352 # Note that @user.membership.size would fail since AR ignores
353 # :include association option when doing a count
353 # :include association option when doing a count
354 if user.nil? || user.memberships.length < 1
354 if user.nil? || user.memberships.length < 1
355 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
355 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
356 else
356 else
357 MAIL_NOTIFICATION_OPTIONS
357 MAIL_NOTIFICATION_OPTIONS
358 end
358 end
359 end
359 end
360
360
361 # Find a user account by matching the exact login and then a case-insensitive
361 # Find a user account by matching the exact login and then a case-insensitive
362 # version. Exact matches will be given priority.
362 # version. Exact matches will be given priority.
363 def self.find_by_login(login)
363 def self.find_by_login(login)
364 if login.present?
364 if login.present?
365 login = login.to_s
365 login = login.to_s
366 # First look for an exact match
366 # First look for an exact match
367 user = where(:login => login).all.detect {|u| u.login == login}
367 user = where(:login => login).all.detect {|u| u.login == login}
368 unless user
368 unless user
369 # Fail over to case-insensitive if none was found
369 # Fail over to case-insensitive if none was found
370 user = where("LOWER(login) = ?", login.downcase).first
370 user = where("LOWER(login) = ?", login.downcase).first
371 end
371 end
372 user
372 user
373 end
373 end
374 end
374 end
375
375
376 def self.find_by_rss_key(key)
376 def self.find_by_rss_key(key)
377 Token.find_active_user('feeds', key)
377 Token.find_active_user('feeds', key)
378 end
378 end
379
379
380 def self.find_by_api_key(key)
380 def self.find_by_api_key(key)
381 Token.find_active_user('api', key)
381 Token.find_active_user('api', key)
382 end
382 end
383
383
384 # Makes find_by_mail case-insensitive
384 # Makes find_by_mail case-insensitive
385 def self.find_by_mail(mail)
385 def self.find_by_mail(mail)
386 where("LOWER(mail) = ?", mail.to_s.downcase).first
386 where("LOWER(mail) = ?", mail.to_s.downcase).first
387 end
387 end
388
388
389 # Returns true if the default admin account can no longer be used
389 # Returns true if the default admin account can no longer be used
390 def self.default_admin_account_changed?
390 def self.default_admin_account_changed?
391 !User.active.find_by_login("admin").try(:check_password?, "admin")
391 !User.active.find_by_login("admin").try(:check_password?, "admin")
392 end
392 end
393
393
394 def to_s
394 def to_s
395 name
395 name
396 end
396 end
397
397
398 CSS_CLASS_BY_STATUS = {
398 CSS_CLASS_BY_STATUS = {
399 STATUS_ANONYMOUS => 'anon',
399 STATUS_ANONYMOUS => 'anon',
400 STATUS_ACTIVE => 'active',
400 STATUS_ACTIVE => 'active',
401 STATUS_REGISTERED => 'registered',
401 STATUS_REGISTERED => 'registered',
402 STATUS_LOCKED => 'locked'
402 STATUS_LOCKED => 'locked'
403 }
403 }
404
404
405 def css_classes
405 def css_classes
406 "user #{CSS_CLASS_BY_STATUS[status]}"
406 "user #{CSS_CLASS_BY_STATUS[status]}"
407 end
407 end
408
408
409 # Returns the current day according to user's time zone
409 # Returns the current day according to user's time zone
410 def today
410 def today
411 if time_zone.nil?
411 if time_zone.nil?
412 Date.today
412 Date.today
413 else
413 else
414 Time.now.in_time_zone(time_zone).to_date
414 Time.now.in_time_zone(time_zone).to_date
415 end
415 end
416 end
416 end
417
417
418 # Returns the day of +time+ according to user's time zone
418 # Returns the day of +time+ according to user's time zone
419 def time_to_date(time)
419 def time_to_date(time)
420 if time_zone.nil?
420 if time_zone.nil?
421 time.to_date
421 time.to_date
422 else
422 else
423 time.in_time_zone(time_zone).to_date
423 time.in_time_zone(time_zone).to_date
424 end
424 end
425 end
425 end
426
426
427 def logged?
427 def logged?
428 true
428 true
429 end
429 end
430
430
431 def anonymous?
431 def anonymous?
432 !logged?
432 !logged?
433 end
433 end
434
434
435 # Returns user's membership for the given project
435 # Returns user's membership for the given project
436 # or nil if the user is not a member of project
436 # or nil if the user is not a member of project
437 def membership(project)
437 def membership(project)
438 project_id = project.is_a?(Project) ? project.id : project
438 project_id = project.is_a?(Project) ? project.id : project
439
439
440 @membership_by_project_id ||= Hash.new {|h, project_id|
440 @membership_by_project_id ||= Hash.new {|h, project_id|
441 h[project_id] = memberships.where(:project_id => project_id).first
441 h[project_id] = memberships.where(:project_id => project_id).first
442 }
442 }
443 @membership_by_project_id[project_id]
443 @membership_by_project_id[project_id]
444 end
444 end
445
445
446 # Returns the user's bult-in role
446 # Returns the user's bult-in role
447 def builtin_role
447 def builtin_role
448 @builtin_role ||= Role.non_member
448 @builtin_role ||= Role.non_member
449 end
449 end
450
450
451 # Return user's roles for project
451 # Return user's roles for project
452 def roles_for_project(project)
452 def roles_for_project(project)
453 roles = []
453 roles = []
454 # No role on archived projects
454 # No role on archived projects
455 return roles if project.nil? || project.archived?
455 return roles if project.nil? || project.archived?
456 if membership = membership(project)
456 if membership = membership(project)
457 roles = membership.roles
457 roles = membership.roles
458 else
458 else
459 roles << builtin_role
459 roles << builtin_role
460 end
460 end
461 roles
461 roles
462 end
462 end
463
463
464 # Return true if the user is a member of project
464 # Return true if the user is a member of project
465 def member_of?(project)
465 def member_of?(project)
466 projects.to_a.include?(project)
466 projects.to_a.include?(project)
467 end
467 end
468
468
469 # Returns a hash of user's projects grouped by roles
469 # Returns a hash of user's projects grouped by roles
470 def projects_by_role
470 def projects_by_role
471 return @projects_by_role if @projects_by_role
471 return @projects_by_role if @projects_by_role
472
472
473 @projects_by_role = Hash.new([])
473 @projects_by_role = Hash.new([])
474 memberships.each do |membership|
474 memberships.each do |membership|
475 if membership.project
475 if membership.project
476 membership.roles.each do |role|
476 membership.roles.each do |role|
477 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
477 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
478 @projects_by_role[role] << membership.project
478 @projects_by_role[role] << membership.project
479 end
479 end
480 end
480 end
481 end
481 end
482 @projects_by_role.each do |role, projects|
482 @projects_by_role.each do |role, projects|
483 projects.uniq!
483 projects.uniq!
484 end
484 end
485
485
486 @projects_by_role
486 @projects_by_role
487 end
487 end
488
488
489 # Returns true if user is arg or belongs to arg
489 # Returns true if user is arg or belongs to arg
490 def is_or_belongs_to?(arg)
490 def is_or_belongs_to?(arg)
491 if arg.is_a?(User)
491 if arg.is_a?(User)
492 self == arg
492 self == arg
493 elsif arg.is_a?(Group)
493 elsif arg.is_a?(Group)
494 arg.users.include?(self)
494 arg.users.include?(self)
495 else
495 else
496 false
496 false
497 end
497 end
498 end
498 end
499
499
500 # Return true if the user is allowed to do the specified action on a specific context
500 # Return true if the user is allowed to do the specified action on a specific context
501 # Action can be:
501 # Action can be:
502 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
502 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
503 # * a permission Symbol (eg. :edit_project)
503 # * a permission Symbol (eg. :edit_project)
504 # Context can be:
504 # Context can be:
505 # * a project : returns true if user is allowed to do the specified action on this project
505 # * a project : returns true if user is allowed to do the specified action on this project
506 # * an array of projects : returns true if user is allowed on every project
506 # * an array of projects : returns true if user is allowed on every project
507 # * nil with options[:global] set : check if user has at least one role allowed for this action,
507 # * nil with options[:global] set : check if user has at least one role allowed for this action,
508 # or falls back to Non Member / Anonymous permissions depending if the user is logged
508 # or falls back to Non Member / Anonymous permissions depending if the user is logged
509 def allowed_to?(action, context, options={}, &block)
509 def allowed_to?(action, context, options={}, &block)
510 if context && context.is_a?(Project)
510 if context && context.is_a?(Project)
511 return false unless context.allows_to?(action)
511 return false unless context.allows_to?(action)
512 # Admin users are authorized for anything else
512 # Admin users are authorized for anything else
513 return true if admin?
513 return true if admin?
514
514
515 roles = roles_for_project(context)
515 roles = roles_for_project(context)
516 return false unless roles
516 return false unless roles
517 roles.any? {|role|
517 roles.any? {|role|
518 (context.is_public? || role.member?) &&
518 (context.is_public? || role.member?) &&
519 role.allowed_to?(action) &&
519 role.allowed_to?(action) &&
520 (block_given? ? yield(role, self) : true)
520 (block_given? ? yield(role, self) : true)
521 }
521 }
522 elsif context && context.is_a?(Array)
522 elsif context && context.is_a?(Array)
523 if context.empty?
523 if context.empty?
524 false
524 false
525 else
525 else
526 # Authorize if user is authorized on every element of the array
526 # Authorize if user is authorized on every element of the array
527 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
527 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
528 end
528 end
529 elsif options[:global]
529 elsif options[:global]
530 # Admin users are always authorized
530 # Admin users are always authorized
531 return true if admin?
531 return true if admin?
532
532
533 # authorize if user has at least one role that has this permission
533 # authorize if user has at least one role that has this permission
534 roles = memberships.collect {|m| m.roles}.flatten.uniq
534 roles = memberships.collect {|m| m.roles}.flatten.uniq
535 roles << (self.logged? ? Role.non_member : Role.anonymous)
535 roles << (self.logged? ? Role.non_member : Role.anonymous)
536 roles.any? {|role|
536 roles.any? {|role|
537 role.allowed_to?(action) &&
537 role.allowed_to?(action) &&
538 (block_given? ? yield(role, self) : true)
538 (block_given? ? yield(role, self) : true)
539 }
539 }
540 else
540 else
541 false
541 false
542 end
542 end
543 end
543 end
544
544
545 # Is the user allowed to do the specified action on any project?
545 # Is the user allowed to do the specified action on any project?
546 # See allowed_to? for the actions and valid options.
546 # See allowed_to? for the actions and valid options.
547 def allowed_to_globally?(action, options, &block)
547 def allowed_to_globally?(action, options, &block)
548 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
548 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
549 end
549 end
550
550
551 # Returns true if the user is allowed to delete the user's own account
551 # Returns true if the user is allowed to delete the user's own account
552 def own_account_deletable?
552 def own_account_deletable?
553 Setting.unsubscribe? &&
553 Setting.unsubscribe? &&
554 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
554 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
555 end
555 end
556
556
557 safe_attributes 'login',
557 safe_attributes 'login',
558 'firstname',
558 'firstname',
559 'lastname',
559 'lastname',
560 'mail',
560 'mail',
561 'mail_notification',
561 'mail_notification',
562 'notified_project_ids',
562 'notified_project_ids',
563 'language',
563 'language',
564 'custom_field_values',
564 'custom_field_values',
565 'custom_fields',
565 'custom_fields',
566 'identity_url'
566 'identity_url'
567
567
568 safe_attributes 'status',
568 safe_attributes 'status',
569 'auth_source_id',
569 'auth_source_id',
570 'generate_password',
570 'generate_password',
571 :if => lambda {|user, current_user| current_user.admin?}
571 :if => lambda {|user, current_user| current_user.admin?}
572
572
573 safe_attributes 'group_ids',
573 safe_attributes 'group_ids',
574 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
574 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
575
575
576 # Utility method to help check if a user should be notified about an
576 # Utility method to help check if a user should be notified about an
577 # event.
577 # event.
578 #
578 #
579 # TODO: only supports Issue events currently
579 # TODO: only supports Issue events currently
580 def notify_about?(object)
580 def notify_about?(object)
581 if mail_notification == 'all'
581 if mail_notification == 'all'
582 true
582 true
583 elsif mail_notification.blank? || mail_notification == 'none'
583 elsif mail_notification.blank? || mail_notification == 'none'
584 false
584 false
585 else
585 else
586 case object
586 case object
587 when Issue
587 when Issue
588 case mail_notification
588 case mail_notification
589 when 'selected', 'only_my_events'
589 when 'selected', 'only_my_events'
590 # user receives notifications for created/assigned issues on unselected projects
590 # user receives notifications for created/assigned issues on unselected projects
591 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
591 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
592 when 'only_assigned'
592 when 'only_assigned'
593 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
593 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
594 when 'only_owner'
594 when 'only_owner'
595 object.author == self
595 object.author == self
596 end
596 end
597 when News
597 when News
598 # always send to project members except when mail_notification is set to 'none'
598 # always send to project members except when mail_notification is set to 'none'
599 true
599 true
600 end
600 end
601 end
601 end
602 end
602 end
603
603
604 def self.current=(user)
604 def self.current=(user)
605 Thread.current[:current_user] = user
605 Thread.current[:current_user] = user
606 end
606 end
607
607
608 def self.current
608 def self.current
609 Thread.current[:current_user] ||= User.anonymous
609 Thread.current[:current_user] ||= User.anonymous
610 end
610 end
611
611
612 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
612 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
613 # one anonymous user per database.
613 # one anonymous user per database.
614 def self.anonymous
614 def self.anonymous
615 anonymous_user = AnonymousUser.first
615 anonymous_user = AnonymousUser.first
616 if anonymous_user.nil?
616 if anonymous_user.nil?
617 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
617 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
618 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
618 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
619 end
619 end
620 anonymous_user
620 anonymous_user
621 end
621 end
622
622
623 # Salts all existing unsalted passwords
623 # Salts all existing unsalted passwords
624 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
624 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
625 # This method is used in the SaltPasswords migration and is to be kept as is
625 # This method is used in the SaltPasswords migration and is to be kept as is
626 def self.salt_unsalted_passwords!
626 def self.salt_unsalted_passwords!
627 transaction do
627 transaction do
628 User.where("salt IS NULL OR salt = ''").find_each do |user|
628 User.where("salt IS NULL OR salt = ''").find_each do |user|
629 next if user.hashed_password.blank?
629 next if user.hashed_password.blank?
630 salt = User.generate_salt
630 salt = User.generate_salt
631 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
631 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
632 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
632 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
633 end
633 end
634 end
634 end
635 end
635 end
636
636
637 protected
637 protected
638
638
639 def validate_password_length
639 def validate_password_length
640 return if password.blank? && generate_password?
640 return if password.blank? && generate_password?
641 # Password length validation based on setting
641 # Password length validation based on setting
642 if !password.nil? && password.size < Setting.password_min_length.to_i
642 if !password.nil? && password.size < Setting.password_min_length.to_i
643 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
643 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
644 end
644 end
645 end
645 end
646
646
647 private
647 private
648
648
649 def generate_password_if_needed
649 def generate_password_if_needed
650 if generate_password? && auth_source.nil?
650 if generate_password? && auth_source.nil?
651 length = [Setting.password_min_length.to_i + 2, 10].max
651 length = [Setting.password_min_length.to_i + 2, 10].max
652 random_password(length)
652 random_password(length)
653 end
653 end
654 end
654 end
655
655
656 # Removes references that are not handled by associations
656 # Removes references that are not handled by associations
657 # Things that are not deleted are reassociated with the anonymous user
657 # Things that are not deleted are reassociated with the anonymous user
658 def remove_references_before_destroy
658 def remove_references_before_destroy
659 return if self.id.nil?
659 return if self.id.nil?
660
660
661 substitute = User.anonymous
661 substitute = User.anonymous
662 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
662 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
663 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
663 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
664 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
664 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
665 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
665 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
666 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
666 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
667 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
667 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
668 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
668 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
669 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
669 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
670 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
670 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
671 # Remove private queries and keep public ones
671 # Remove private queries and keep public ones
672 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
672 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
673 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
673 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
674 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
674 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
675 Token.delete_all ['user_id = ?', id]
675 Token.delete_all ['user_id = ?', id]
676 Watcher.delete_all ['user_id = ?', id]
676 Watcher.delete_all ['user_id = ?', id]
677 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
677 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
678 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
678 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
679 end
679 end
680
680
681 # Return password digest
681 # Return password digest
682 def self.hash_password(clear_password)
682 def self.hash_password(clear_password)
683 Digest::SHA1.hexdigest(clear_password || "")
683 Digest::SHA1.hexdigest(clear_password || "")
684 end
684 end
685
685
686 # Returns a 128bits random salt as a hex string (32 chars long)
686 # Returns a 128bits random salt as a hex string (32 chars long)
687 def self.generate_salt
687 def self.generate_salt
688 Redmine::Utils.random_hex(16)
688 Redmine::Utils.random_hex(16)
689 end
689 end
690
690
691 end
691 end
692
692
693 class AnonymousUser < User
693 class AnonymousUser < User
694 validate :validate_anonymous_uniqueness, :on => :create
694 validate :validate_anonymous_uniqueness, :on => :create
695
695
696 def validate_anonymous_uniqueness
696 def validate_anonymous_uniqueness
697 # There should be only one AnonymousUser in the database
697 # There should be only one AnonymousUser in the database
698 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
698 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
699 end
699 end
700
700
701 def available_custom_fields
701 def available_custom_fields
702 []
702 []
703 end
703 end
704
704
705 # Overrides a few properties
705 # Overrides a few properties
706 def logged?; false end
706 def logged?; false end
707 def admin; false end
707 def admin; false end
708 def name(*args); I18n.t(:label_user_anonymous) end
708 def name(*args); I18n.t(:label_user_anonymous) end
709 def mail; nil end
709 def mail; nil end
710 def time_zone; nil end
710 def time_zone; nil end
711 def rss_key; nil end
711 def rss_key; nil end
712
712
713 def pref
713 def pref
714 UserPreference.new(:user => self)
714 UserPreference.new(:user => self)
715 end
715 end
716
716
717 # Returns the user's bult-in role
717 # Returns the user's bult-in role
718 def builtin_role
718 def builtin_role
719 @builtin_role ||= Role.anonymous
719 @builtin_role ||= Role.anonymous
720 end
720 end
721
721
722 def membership(*args)
722 def membership(*args)
723 nil
723 nil
724 end
724 end
725
725
726 def member_of?(*args)
726 def member_of?(*args)
727 false
727 false
728 end
728 end
729
729
730 # Anonymous user can not be destroyed
730 # Anonymous user can not be destroyed
731 def destroy
731 def destroy
732 false
732 false
733 end
733 end
734 end
734 end
@@ -1,55 +1,72
1 <%= error_messages_for 'query' %>
1 <%= error_messages_for 'query' %>
2
2
3 <div class="box">
3 <div class="box">
4 <div class="tabular">
4 <div class="tabular">
5 <p><label for="query_name"><%=l(:field_name)%></label>
5 <p><label for="query_name"><%=l(:field_name)%></label>
6 <%= text_field 'query', 'name', :size => 80 %></p>
6 <%= text_field 'query', 'name', :size => 80 %></p>
7
7
8 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
8 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
9 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
9 <p><label><%=l(:field_visible)%></label>
10 <%= check_box 'query', 'is_public',
10 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PRIVATE %> <%= l(:label_visibility_private) %></label>
11 :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#query_is_for_all").removeAttr("checked"); $("#query_is_for_all").attr("disabled", true);} else {$("#query_is_for_all").removeAttr("disabled");}') %></p>
11 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label>
12 <% Role.givable.sorted.each do |role| %>
13 <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label>
14 <% end %>
15 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PUBLIC %> <%= l(:label_visibility_public) %></label>
16 <%= hidden_field_tag 'query[role_ids][]', '' %>
17 </p>
12 <% end %>
18 <% end %>
13
19
14 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
20 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
15 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
21 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
16 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
22 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
17
23
24 <fieldset><legend><%= l(:label_options) %></legend>
18 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
25 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
19 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
26 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
20 :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p>
27 :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p>
21
28
22 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
29 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
23 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
30 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
24
31
25 <p><label><%= l(:button_show) %></label>
32 <p><label><%= l(:button_show) %></label>
26 <%= available_block_columns_tags(@query) %></p>
33 <%= available_block_columns_tags(@query) %></p>
34 </fieldset>
27 </div>
35 </div>
28
36
29 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
37 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
30 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
38 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
31 </fieldset>
39 </fieldset>
32
40
33 <fieldset><legend><%= l(:label_sort) %></legend>
41 <fieldset><legend><%= l(:label_sort) %></legend>
34 <% 3.times do |i| %>
42 <% 3.times do |i| %>
35 <%= i+1 %>:
43 <%= i+1 %>:
36 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
44 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
37 l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %>
45 l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %>
38 <%= select_tag("query[sort_criteria][#{i}][]",
46 <%= select_tag("query[sort_criteria][#{i}][]",
39 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)),
47 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)),
40 :id => "query_sort_criteria_attribute_" + i.to_s)%>
48 :id => "query_sort_criteria_attribute_" + i.to_s)%>
41 <%= label_tag "query_sort_criteria_direction_" + i.to_s,
49 <%= label_tag "query_sort_criteria_direction_" + i.to_s,
42 l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
50 l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
43 <%= select_tag("query[sort_criteria][#{i}][]",
51 <%= select_tag("query[sort_criteria][#{i}][]",
44 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
52 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
45 :id => "query_sort_criteria_direction_" + i.to_s) %>
53 :id => "query_sort_criteria_direction_" + i.to_s) %>
46 <br />
54 <br />
47 <% end %>
55 <% end %>
48 </fieldset>
56 </fieldset>
49
57
50 <%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
58 <%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
51 <legend><%= l(:field_column_names) %></legend>
59 <legend><%= l(:field_column_names) %></legend>
52 <%= render_query_columns_selection(query) %>
60 <%= render_query_columns_selection(query) %>
53 <% end %>
61 <% end %>
54
62
55 </div>
63 </div>
64
65 <%= javascript_tag do %>
66 $(document).ready(function(){
67 $("input[name='query[visibility]']").change(function(){
68 var checked = $('#query_visibility_1').is(':checked');
69 $("input[name='query[role_ids][]'][type=checkbox]").attr('disabled', !checked);
70 }).trigger('change');
71 });
72 <% end %>
@@ -1,10 +1,10
1 api.array :queries, api_meta(:total_count => @query_count, :offset => @offset, :limit => @limit) do
1 api.array :queries, api_meta(:total_count => @query_count, :offset => @offset, :limit => @limit) do
2 @queries.each do |query|
2 @queries.each do |query|
3 api.query do
3 api.query do
4 api.id query.id
4 api.id query.id
5 api.name query.name
5 api.name query.name
6 api.is_public query.is_public
6 api.is_public query.is_public?
7 api.project_id query.project_id
7 api.project_id query.project_id
8 end
8 end
9 end
9 end
10 end
10 end
@@ -1,1088 +1,1091
1 en:
1 en:
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 direction: ltr
3 direction: ltr
4 date:
4 date:
5 formats:
5 formats:
6 # Use the strftime parameters for formats.
6 # Use the strftime parameters for formats.
7 # When no format has been given, it uses default.
7 # When no format has been given, it uses default.
8 # You can provide other formats here if you like!
8 # You can provide other formats here if you like!
9 default: "%m/%d/%Y"
9 default: "%m/%d/%Y"
10 short: "%b %d"
10 short: "%b %d"
11 long: "%B %d, %Y"
11 long: "%B %d, %Y"
12
12
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15
15
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 # Used in date_select and datime_select.
19 # Used in date_select and datime_select.
20 order:
20 order:
21 - :year
21 - :year
22 - :month
22 - :month
23 - :day
23 - :day
24
24
25 time:
25 time:
26 formats:
26 formats:
27 default: "%m/%d/%Y %I:%M %p"
27 default: "%m/%d/%Y %I:%M %p"
28 time: "%I:%M %p"
28 time: "%I:%M %p"
29 short: "%d %b %H:%M"
29 short: "%d %b %H:%M"
30 long: "%B %d, %Y %H:%M"
30 long: "%B %d, %Y %H:%M"
31 am: "am"
31 am: "am"
32 pm: "pm"
32 pm: "pm"
33
33
34 datetime:
34 datetime:
35 distance_in_words:
35 distance_in_words:
36 half_a_minute: "half a minute"
36 half_a_minute: "half a minute"
37 less_than_x_seconds:
37 less_than_x_seconds:
38 one: "less than 1 second"
38 one: "less than 1 second"
39 other: "less than %{count} seconds"
39 other: "less than %{count} seconds"
40 x_seconds:
40 x_seconds:
41 one: "1 second"
41 one: "1 second"
42 other: "%{count} seconds"
42 other: "%{count} seconds"
43 less_than_x_minutes:
43 less_than_x_minutes:
44 one: "less than a minute"
44 one: "less than a minute"
45 other: "less than %{count} minutes"
45 other: "less than %{count} minutes"
46 x_minutes:
46 x_minutes:
47 one: "1 minute"
47 one: "1 minute"
48 other: "%{count} minutes"
48 other: "%{count} minutes"
49 about_x_hours:
49 about_x_hours:
50 one: "about 1 hour"
50 one: "about 1 hour"
51 other: "about %{count} hours"
51 other: "about %{count} hours"
52 x_hours:
52 x_hours:
53 one: "1 hour"
53 one: "1 hour"
54 other: "%{count} hours"
54 other: "%{count} hours"
55 x_days:
55 x_days:
56 one: "1 day"
56 one: "1 day"
57 other: "%{count} days"
57 other: "%{count} days"
58 about_x_months:
58 about_x_months:
59 one: "about 1 month"
59 one: "about 1 month"
60 other: "about %{count} months"
60 other: "about %{count} months"
61 x_months:
61 x_months:
62 one: "1 month"
62 one: "1 month"
63 other: "%{count} months"
63 other: "%{count} months"
64 about_x_years:
64 about_x_years:
65 one: "about 1 year"
65 one: "about 1 year"
66 other: "about %{count} years"
66 other: "about %{count} years"
67 over_x_years:
67 over_x_years:
68 one: "over 1 year"
68 one: "over 1 year"
69 other: "over %{count} years"
69 other: "over %{count} years"
70 almost_x_years:
70 almost_x_years:
71 one: "almost 1 year"
71 one: "almost 1 year"
72 other: "almost %{count} years"
72 other: "almost %{count} years"
73
73
74 number:
74 number:
75 format:
75 format:
76 separator: "."
76 separator: "."
77 delimiter: ""
77 delimiter: ""
78 precision: 3
78 precision: 3
79
79
80 human:
80 human:
81 format:
81 format:
82 delimiter: ""
82 delimiter: ""
83 precision: 3
83 precision: 3
84 storage_units:
84 storage_units:
85 format: "%n %u"
85 format: "%n %u"
86 units:
86 units:
87 byte:
87 byte:
88 one: "Byte"
88 one: "Byte"
89 other: "Bytes"
89 other: "Bytes"
90 kb: "KB"
90 kb: "KB"
91 mb: "MB"
91 mb: "MB"
92 gb: "GB"
92 gb: "GB"
93 tb: "TB"
93 tb: "TB"
94
94
95 # Used in array.to_sentence.
95 # Used in array.to_sentence.
96 support:
96 support:
97 array:
97 array:
98 sentence_connector: "and"
98 sentence_connector: "and"
99 skip_last_comma: false
99 skip_last_comma: false
100
100
101 activerecord:
101 activerecord:
102 errors:
102 errors:
103 template:
103 template:
104 header:
104 header:
105 one: "1 error prohibited this %{model} from being saved"
105 one: "1 error prohibited this %{model} from being saved"
106 other: "%{count} errors prohibited this %{model} from being saved"
106 other: "%{count} errors prohibited this %{model} from being saved"
107 messages:
107 messages:
108 inclusion: "is not included in the list"
108 inclusion: "is not included in the list"
109 exclusion: "is reserved"
109 exclusion: "is reserved"
110 invalid: "is invalid"
110 invalid: "is invalid"
111 confirmation: "doesn't match confirmation"
111 confirmation: "doesn't match confirmation"
112 accepted: "must be accepted"
112 accepted: "must be accepted"
113 empty: "can't be empty"
113 empty: "can't be empty"
114 blank: "can't be blank"
114 blank: "can't be blank"
115 too_long: "is too long (maximum is %{count} characters)"
115 too_long: "is too long (maximum is %{count} characters)"
116 too_short: "is too short (minimum is %{count} characters)"
116 too_short: "is too short (minimum is %{count} characters)"
117 wrong_length: "is the wrong length (should be %{count} characters)"
117 wrong_length: "is the wrong length (should be %{count} characters)"
118 taken: "has already been taken"
118 taken: "has already been taken"
119 not_a_number: "is not a number"
119 not_a_number: "is not a number"
120 not_a_date: "is not a valid date"
120 not_a_date: "is not a valid date"
121 greater_than: "must be greater than %{count}"
121 greater_than: "must be greater than %{count}"
122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
122 greater_than_or_equal_to: "must be greater than or equal to %{count}"
123 equal_to: "must be equal to %{count}"
123 equal_to: "must be equal to %{count}"
124 less_than: "must be less than %{count}"
124 less_than: "must be less than %{count}"
125 less_than_or_equal_to: "must be less than or equal to %{count}"
125 less_than_or_equal_to: "must be less than or equal to %{count}"
126 odd: "must be odd"
126 odd: "must be odd"
127 even: "must be even"
127 even: "must be even"
128 greater_than_start_date: "must be greater than start date"
128 greater_than_start_date: "must be greater than start date"
129 not_same_project: "doesn't belong to the same project"
129 not_same_project: "doesn't belong to the same project"
130 circular_dependency: "This relation would create a circular dependency"
130 circular_dependency: "This relation would create a circular dependency"
131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
131 cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks"
132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
132 earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues"
133
133
134 actionview_instancetag_blank_option: Please select
134 actionview_instancetag_blank_option: Please select
135
135
136 general_text_No: 'No'
136 general_text_No: 'No'
137 general_text_Yes: 'Yes'
137 general_text_Yes: 'Yes'
138 general_text_no: 'no'
138 general_text_no: 'no'
139 general_text_yes: 'yes'
139 general_text_yes: 'yes'
140 general_lang_name: 'English'
140 general_lang_name: 'English'
141 general_csv_separator: ','
141 general_csv_separator: ','
142 general_csv_decimal_separator: '.'
142 general_csv_decimal_separator: '.'
143 general_csv_encoding: ISO-8859-1
143 general_csv_encoding: ISO-8859-1
144 general_pdf_encoding: UTF-8
144 general_pdf_encoding: UTF-8
145 general_first_day_of_week: '7'
145 general_first_day_of_week: '7'
146
146
147 notice_account_updated: Account was successfully updated.
147 notice_account_updated: Account was successfully updated.
148 notice_account_invalid_creditentials: Invalid user or password
148 notice_account_invalid_creditentials: Invalid user or password
149 notice_account_password_updated: Password was successfully updated.
149 notice_account_password_updated: Password was successfully updated.
150 notice_account_wrong_password: Wrong password
150 notice_account_wrong_password: Wrong password
151 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
151 notice_account_register_done: Account was successfully created. An email containing the instructions to activate your account was sent to %{email}.
152 notice_account_unknown_email: Unknown user.
152 notice_account_unknown_email: Unknown user.
153 notice_account_not_activated_yet: You haven't activated your account yet. If you want to receive a new activation email, please <a href="%{url}">click this link</a>.
153 notice_account_not_activated_yet: You haven't activated your account yet. If you want to receive a new activation email, please <a href="%{url}">click this link</a>.
154 notice_account_locked: Your account is locked.
154 notice_account_locked: Your account is locked.
155 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
155 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
156 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
156 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
157 notice_account_activated: Your account has been activated. You can now log in.
157 notice_account_activated: Your account has been activated. You can now log in.
158 notice_successful_create: Successful creation.
158 notice_successful_create: Successful creation.
159 notice_successful_update: Successful update.
159 notice_successful_update: Successful update.
160 notice_successful_delete: Successful deletion.
160 notice_successful_delete: Successful deletion.
161 notice_successful_connection: Successful connection.
161 notice_successful_connection: Successful connection.
162 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
162 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
163 notice_locking_conflict: Data has been updated by another user.
163 notice_locking_conflict: Data has been updated by another user.
164 notice_not_authorized: You are not authorized to access this page.
164 notice_not_authorized: You are not authorized to access this page.
165 notice_not_authorized_archived_project: The project you're trying to access has been archived.
165 notice_not_authorized_archived_project: The project you're trying to access has been archived.
166 notice_email_sent: "An email was sent to %{value}"
166 notice_email_sent: "An email was sent to %{value}"
167 notice_email_error: "An error occurred while sending mail (%{value})"
167 notice_email_error: "An error occurred while sending mail (%{value})"
168 notice_feeds_access_key_reseted: Your Atom access key was reset.
168 notice_feeds_access_key_reseted: Your Atom access key was reset.
169 notice_api_access_key_reseted: Your API access key was reset.
169 notice_api_access_key_reseted: Your API access key was reset.
170 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
170 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
171 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
171 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
172 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
172 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
173 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
173 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
174 notice_account_pending: "Your account was created and is now pending administrator approval."
174 notice_account_pending: "Your account was created and is now pending administrator approval."
175 notice_default_data_loaded: Default configuration successfully loaded.
175 notice_default_data_loaded: Default configuration successfully loaded.
176 notice_unable_delete_version: Unable to delete version.
176 notice_unable_delete_version: Unable to delete version.
177 notice_unable_delete_time_entry: Unable to delete time log entry.
177 notice_unable_delete_time_entry: Unable to delete time log entry.
178 notice_issue_done_ratios_updated: Issue done ratios updated.
178 notice_issue_done_ratios_updated: Issue done ratios updated.
179 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
179 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
180 notice_issue_successful_create: "Issue %{id} created."
180 notice_issue_successful_create: "Issue %{id} created."
181 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
181 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
182 notice_account_deleted: "Your account has been permanently deleted."
182 notice_account_deleted: "Your account has been permanently deleted."
183 notice_user_successful_create: "User %{id} created."
183 notice_user_successful_create: "User %{id} created."
184
184
185 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
185 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
186 error_scm_not_found: "The entry or revision was not found in the repository."
186 error_scm_not_found: "The entry or revision was not found in the repository."
187 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
187 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
188 error_scm_annotate: "The entry does not exist or cannot be annotated."
188 error_scm_annotate: "The entry does not exist or cannot be annotated."
189 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
189 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
190 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
190 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
191 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
191 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
192 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
192 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
193 error_can_not_delete_custom_field: Unable to delete custom field
193 error_can_not_delete_custom_field: Unable to delete custom field
194 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
194 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
195 error_can_not_remove_role: "This role is in use and cannot be deleted."
195 error_can_not_remove_role: "This role is in use and cannot be deleted."
196 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
196 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
197 error_can_not_archive_project: This project cannot be archived
197 error_can_not_archive_project: This project cannot be archived
198 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
198 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
199 error_workflow_copy_source: 'Please select a source tracker or role'
199 error_workflow_copy_source: 'Please select a source tracker or role'
200 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
200 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
201 error_unable_delete_issue_status: 'Unable to delete issue status'
201 error_unable_delete_issue_status: 'Unable to delete issue status'
202 error_unable_to_connect: "Unable to connect (%{value})"
202 error_unable_to_connect: "Unable to connect (%{value})"
203 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
203 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
204 error_session_expired: "Your session has expired. Please login again."
204 error_session_expired: "Your session has expired. Please login again."
205 warning_attachments_not_saved: "%{count} file(s) could not be saved."
205 warning_attachments_not_saved: "%{count} file(s) could not be saved."
206
206
207 mail_subject_lost_password: "Your %{value} password"
207 mail_subject_lost_password: "Your %{value} password"
208 mail_body_lost_password: 'To change your password, click on the following link:'
208 mail_body_lost_password: 'To change your password, click on the following link:'
209 mail_subject_register: "Your %{value} account activation"
209 mail_subject_register: "Your %{value} account activation"
210 mail_body_register: 'To activate your account, click on the following link:'
210 mail_body_register: 'To activate your account, click on the following link:'
211 mail_body_account_information_external: "You can use your %{value} account to log in."
211 mail_body_account_information_external: "You can use your %{value} account to log in."
212 mail_body_account_information: Your account information
212 mail_body_account_information: Your account information
213 mail_subject_account_activation_request: "%{value} account activation request"
213 mail_subject_account_activation_request: "%{value} account activation request"
214 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
214 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
215 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
215 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
216 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
216 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
217 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
217 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
218 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
218 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
219 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
219 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
220 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
220 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
221
221
222 field_name: Name
222 field_name: Name
223 field_description: Description
223 field_description: Description
224 field_summary: Summary
224 field_summary: Summary
225 field_is_required: Required
225 field_is_required: Required
226 field_firstname: First name
226 field_firstname: First name
227 field_lastname: Last name
227 field_lastname: Last name
228 field_mail: Email
228 field_mail: Email
229 field_filename: File
229 field_filename: File
230 field_filesize: Size
230 field_filesize: Size
231 field_downloads: Downloads
231 field_downloads: Downloads
232 field_author: Author
232 field_author: Author
233 field_created_on: Created
233 field_created_on: Created
234 field_updated_on: Updated
234 field_updated_on: Updated
235 field_closed_on: Closed
235 field_closed_on: Closed
236 field_field_format: Format
236 field_field_format: Format
237 field_is_for_all: For all projects
237 field_is_for_all: For all projects
238 field_possible_values: Possible values
238 field_possible_values: Possible values
239 field_regexp: Regular expression
239 field_regexp: Regular expression
240 field_min_length: Minimum length
240 field_min_length: Minimum length
241 field_max_length: Maximum length
241 field_max_length: Maximum length
242 field_value: Value
242 field_value: Value
243 field_category: Category
243 field_category: Category
244 field_title: Title
244 field_title: Title
245 field_project: Project
245 field_project: Project
246 field_issue: Issue
246 field_issue: Issue
247 field_status: Status
247 field_status: Status
248 field_notes: Notes
248 field_notes: Notes
249 field_is_closed: Issue closed
249 field_is_closed: Issue closed
250 field_is_default: Default value
250 field_is_default: Default value
251 field_tracker: Tracker
251 field_tracker: Tracker
252 field_subject: Subject
252 field_subject: Subject
253 field_due_date: Due date
253 field_due_date: Due date
254 field_assigned_to: Assignee
254 field_assigned_to: Assignee
255 field_priority: Priority
255 field_priority: Priority
256 field_fixed_version: Target version
256 field_fixed_version: Target version
257 field_user: User
257 field_user: User
258 field_principal: Principal
258 field_principal: Principal
259 field_role: Role
259 field_role: Role
260 field_homepage: Homepage
260 field_homepage: Homepage
261 field_is_public: Public
261 field_is_public: Public
262 field_parent: Subproject of
262 field_parent: Subproject of
263 field_is_in_roadmap: Issues displayed in roadmap
263 field_is_in_roadmap: Issues displayed in roadmap
264 field_login: Login
264 field_login: Login
265 field_mail_notification: Email notifications
265 field_mail_notification: Email notifications
266 field_admin: Administrator
266 field_admin: Administrator
267 field_last_login_on: Last connection
267 field_last_login_on: Last connection
268 field_language: Language
268 field_language: Language
269 field_effective_date: Date
269 field_effective_date: Date
270 field_password: Password
270 field_password: Password
271 field_new_password: New password
271 field_new_password: New password
272 field_password_confirmation: Confirmation
272 field_password_confirmation: Confirmation
273 field_version: Version
273 field_version: Version
274 field_type: Type
274 field_type: Type
275 field_host: Host
275 field_host: Host
276 field_port: Port
276 field_port: Port
277 field_account: Account
277 field_account: Account
278 field_base_dn: Base DN
278 field_base_dn: Base DN
279 field_attr_login: Login attribute
279 field_attr_login: Login attribute
280 field_attr_firstname: Firstname attribute
280 field_attr_firstname: Firstname attribute
281 field_attr_lastname: Lastname attribute
281 field_attr_lastname: Lastname attribute
282 field_attr_mail: Email attribute
282 field_attr_mail: Email attribute
283 field_onthefly: On-the-fly user creation
283 field_onthefly: On-the-fly user creation
284 field_start_date: Start date
284 field_start_date: Start date
285 field_done_ratio: "% Done"
285 field_done_ratio: "% Done"
286 field_auth_source: Authentication mode
286 field_auth_source: Authentication mode
287 field_hide_mail: Hide my email address
287 field_hide_mail: Hide my email address
288 field_comments: Comment
288 field_comments: Comment
289 field_url: URL
289 field_url: URL
290 field_start_page: Start page
290 field_start_page: Start page
291 field_subproject: Subproject
291 field_subproject: Subproject
292 field_hours: Hours
292 field_hours: Hours
293 field_activity: Activity
293 field_activity: Activity
294 field_spent_on: Date
294 field_spent_on: Date
295 field_identifier: Identifier
295 field_identifier: Identifier
296 field_is_filter: Used as a filter
296 field_is_filter: Used as a filter
297 field_issue_to: Related issue
297 field_issue_to: Related issue
298 field_delay: Delay
298 field_delay: Delay
299 field_assignable: Issues can be assigned to this role
299 field_assignable: Issues can be assigned to this role
300 field_redirect_existing_links: Redirect existing links
300 field_redirect_existing_links: Redirect existing links
301 field_estimated_hours: Estimated time
301 field_estimated_hours: Estimated time
302 field_column_names: Columns
302 field_column_names: Columns
303 field_time_entries: Log time
303 field_time_entries: Log time
304 field_time_zone: Time zone
304 field_time_zone: Time zone
305 field_searchable: Searchable
305 field_searchable: Searchable
306 field_default_value: Default value
306 field_default_value: Default value
307 field_comments_sorting: Display comments
307 field_comments_sorting: Display comments
308 field_parent_title: Parent page
308 field_parent_title: Parent page
309 field_editable: Editable
309 field_editable: Editable
310 field_watcher: Watcher
310 field_watcher: Watcher
311 field_identity_url: OpenID URL
311 field_identity_url: OpenID URL
312 field_content: Content
312 field_content: Content
313 field_group_by: Group results by
313 field_group_by: Group results by
314 field_sharing: Sharing
314 field_sharing: Sharing
315 field_parent_issue: Parent task
315 field_parent_issue: Parent task
316 field_member_of_group: "Assignee's group"
316 field_member_of_group: "Assignee's group"
317 field_assigned_to_role: "Assignee's role"
317 field_assigned_to_role: "Assignee's role"
318 field_text: Text field
318 field_text: Text field
319 field_visible: Visible
319 field_visible: Visible
320 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
320 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
321 field_issues_visibility: Issues visibility
321 field_issues_visibility: Issues visibility
322 field_is_private: Private
322 field_is_private: Private
323 field_commit_logs_encoding: Commit messages encoding
323 field_commit_logs_encoding: Commit messages encoding
324 field_scm_path_encoding: Path encoding
324 field_scm_path_encoding: Path encoding
325 field_path_to_repository: Path to repository
325 field_path_to_repository: Path to repository
326 field_root_directory: Root directory
326 field_root_directory: Root directory
327 field_cvsroot: CVSROOT
327 field_cvsroot: CVSROOT
328 field_cvs_module: Module
328 field_cvs_module: Module
329 field_repository_is_default: Main repository
329 field_repository_is_default: Main repository
330 field_multiple: Multiple values
330 field_multiple: Multiple values
331 field_auth_source_ldap_filter: LDAP filter
331 field_auth_source_ldap_filter: LDAP filter
332 field_core_fields: Standard fields
332 field_core_fields: Standard fields
333 field_timeout: "Timeout (in seconds)"
333 field_timeout: "Timeout (in seconds)"
334 field_board_parent: Parent forum
334 field_board_parent: Parent forum
335 field_private_notes: Private notes
335 field_private_notes: Private notes
336 field_inherit_members: Inherit members
336 field_inherit_members: Inherit members
337 field_generate_password: Generate password
337 field_generate_password: Generate password
338
338
339 setting_app_title: Application title
339 setting_app_title: Application title
340 setting_app_subtitle: Application subtitle
340 setting_app_subtitle: Application subtitle
341 setting_welcome_text: Welcome text
341 setting_welcome_text: Welcome text
342 setting_default_language: Default language
342 setting_default_language: Default language
343 setting_login_required: Authentication required
343 setting_login_required: Authentication required
344 setting_self_registration: Self-registration
344 setting_self_registration: Self-registration
345 setting_attachment_max_size: Maximum attachment size
345 setting_attachment_max_size: Maximum attachment size
346 setting_issues_export_limit: Issues export limit
346 setting_issues_export_limit: Issues export limit
347 setting_mail_from: Emission email address
347 setting_mail_from: Emission email address
348 setting_bcc_recipients: Blind carbon copy recipients (bcc)
348 setting_bcc_recipients: Blind carbon copy recipients (bcc)
349 setting_plain_text_mail: Plain text mail (no HTML)
349 setting_plain_text_mail: Plain text mail (no HTML)
350 setting_host_name: Host name and path
350 setting_host_name: Host name and path
351 setting_text_formatting: Text formatting
351 setting_text_formatting: Text formatting
352 setting_wiki_compression: Wiki history compression
352 setting_wiki_compression: Wiki history compression
353 setting_feeds_limit: Maximum number of items in Atom feeds
353 setting_feeds_limit: Maximum number of items in Atom feeds
354 setting_default_projects_public: New projects are public by default
354 setting_default_projects_public: New projects are public by default
355 setting_autofetch_changesets: Fetch commits automatically
355 setting_autofetch_changesets: Fetch commits automatically
356 setting_sys_api_enabled: Enable WS for repository management
356 setting_sys_api_enabled: Enable WS for repository management
357 setting_commit_ref_keywords: Referencing keywords
357 setting_commit_ref_keywords: Referencing keywords
358 setting_commit_fix_keywords: Fixing keywords
358 setting_commit_fix_keywords: Fixing keywords
359 setting_autologin: Autologin
359 setting_autologin: Autologin
360 setting_date_format: Date format
360 setting_date_format: Date format
361 setting_time_format: Time format
361 setting_time_format: Time format
362 setting_cross_project_issue_relations: Allow cross-project issue relations
362 setting_cross_project_issue_relations: Allow cross-project issue relations
363 setting_cross_project_subtasks: Allow cross-project subtasks
363 setting_cross_project_subtasks: Allow cross-project subtasks
364 setting_issue_list_default_columns: Default columns displayed on the issue list
364 setting_issue_list_default_columns: Default columns displayed on the issue list
365 setting_repositories_encodings: Attachments and repositories encodings
365 setting_repositories_encodings: Attachments and repositories encodings
366 setting_emails_header: Email header
366 setting_emails_header: Email header
367 setting_emails_footer: Email footer
367 setting_emails_footer: Email footer
368 setting_protocol: Protocol
368 setting_protocol: Protocol
369 setting_per_page_options: Objects per page options
369 setting_per_page_options: Objects per page options
370 setting_user_format: Users display format
370 setting_user_format: Users display format
371 setting_activity_days_default: Days displayed on project activity
371 setting_activity_days_default: Days displayed on project activity
372 setting_display_subprojects_issues: Display subprojects issues on main projects by default
372 setting_display_subprojects_issues: Display subprojects issues on main projects by default
373 setting_enabled_scm: Enabled SCM
373 setting_enabled_scm: Enabled SCM
374 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
374 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
375 setting_mail_handler_api_enabled: Enable WS for incoming emails
375 setting_mail_handler_api_enabled: Enable WS for incoming emails
376 setting_mail_handler_api_key: API key
376 setting_mail_handler_api_key: API key
377 setting_sequential_project_identifiers: Generate sequential project identifiers
377 setting_sequential_project_identifiers: Generate sequential project identifiers
378 setting_gravatar_enabled: Use Gravatar user icons
378 setting_gravatar_enabled: Use Gravatar user icons
379 setting_gravatar_default: Default Gravatar image
379 setting_gravatar_default: Default Gravatar image
380 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
380 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
381 setting_file_max_size_displayed: Maximum size of text files displayed inline
381 setting_file_max_size_displayed: Maximum size of text files displayed inline
382 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
382 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
383 setting_openid: Allow OpenID login and registration
383 setting_openid: Allow OpenID login and registration
384 setting_password_min_length: Minimum password length
384 setting_password_min_length: Minimum password length
385 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
385 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
386 setting_default_projects_modules: Default enabled modules for new projects
386 setting_default_projects_modules: Default enabled modules for new projects
387 setting_issue_done_ratio: Calculate the issue done ratio with
387 setting_issue_done_ratio: Calculate the issue done ratio with
388 setting_issue_done_ratio_issue_field: Use the issue field
388 setting_issue_done_ratio_issue_field: Use the issue field
389 setting_issue_done_ratio_issue_status: Use the issue status
389 setting_issue_done_ratio_issue_status: Use the issue status
390 setting_start_of_week: Start calendars on
390 setting_start_of_week: Start calendars on
391 setting_rest_api_enabled: Enable REST web service
391 setting_rest_api_enabled: Enable REST web service
392 setting_cache_formatted_text: Cache formatted text
392 setting_cache_formatted_text: Cache formatted text
393 setting_default_notification_option: Default notification option
393 setting_default_notification_option: Default notification option
394 setting_commit_logtime_enabled: Enable time logging
394 setting_commit_logtime_enabled: Enable time logging
395 setting_commit_logtime_activity_id: Activity for logged time
395 setting_commit_logtime_activity_id: Activity for logged time
396 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
396 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
397 setting_issue_group_assignment: Allow issue assignment to groups
397 setting_issue_group_assignment: Allow issue assignment to groups
398 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
398 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
399 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
399 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
400 setting_unsubscribe: Allow users to delete their own account
400 setting_unsubscribe: Allow users to delete their own account
401 setting_session_lifetime: Session maximum lifetime
401 setting_session_lifetime: Session maximum lifetime
402 setting_session_timeout: Session inactivity timeout
402 setting_session_timeout: Session inactivity timeout
403 setting_thumbnails_enabled: Display attachment thumbnails
403 setting_thumbnails_enabled: Display attachment thumbnails
404 setting_thumbnails_size: Thumbnails size (in pixels)
404 setting_thumbnails_size: Thumbnails size (in pixels)
405 setting_non_working_week_days: Non-working days
405 setting_non_working_week_days: Non-working days
406 setting_jsonp_enabled: Enable JSONP support
406 setting_jsonp_enabled: Enable JSONP support
407 setting_default_projects_tracker_ids: Default trackers for new projects
407 setting_default_projects_tracker_ids: Default trackers for new projects
408
408
409 permission_add_project: Create project
409 permission_add_project: Create project
410 permission_add_subprojects: Create subprojects
410 permission_add_subprojects: Create subprojects
411 permission_edit_project: Edit project
411 permission_edit_project: Edit project
412 permission_close_project: Close / reopen the project
412 permission_close_project: Close / reopen the project
413 permission_select_project_modules: Select project modules
413 permission_select_project_modules: Select project modules
414 permission_manage_members: Manage members
414 permission_manage_members: Manage members
415 permission_manage_project_activities: Manage project activities
415 permission_manage_project_activities: Manage project activities
416 permission_manage_versions: Manage versions
416 permission_manage_versions: Manage versions
417 permission_manage_categories: Manage issue categories
417 permission_manage_categories: Manage issue categories
418 permission_view_issues: View Issues
418 permission_view_issues: View Issues
419 permission_add_issues: Add issues
419 permission_add_issues: Add issues
420 permission_edit_issues: Edit issues
420 permission_edit_issues: Edit issues
421 permission_manage_issue_relations: Manage issue relations
421 permission_manage_issue_relations: Manage issue relations
422 permission_set_issues_private: Set issues public or private
422 permission_set_issues_private: Set issues public or private
423 permission_set_own_issues_private: Set own issues public or private
423 permission_set_own_issues_private: Set own issues public or private
424 permission_add_issue_notes: Add notes
424 permission_add_issue_notes: Add notes
425 permission_edit_issue_notes: Edit notes
425 permission_edit_issue_notes: Edit notes
426 permission_edit_own_issue_notes: Edit own notes
426 permission_edit_own_issue_notes: Edit own notes
427 permission_view_private_notes: View private notes
427 permission_view_private_notes: View private notes
428 permission_set_notes_private: Set notes as private
428 permission_set_notes_private: Set notes as private
429 permission_move_issues: Move issues
429 permission_move_issues: Move issues
430 permission_delete_issues: Delete issues
430 permission_delete_issues: Delete issues
431 permission_manage_public_queries: Manage public queries
431 permission_manage_public_queries: Manage public queries
432 permission_save_queries: Save queries
432 permission_save_queries: Save queries
433 permission_view_gantt: View gantt chart
433 permission_view_gantt: View gantt chart
434 permission_view_calendar: View calendar
434 permission_view_calendar: View calendar
435 permission_view_issue_watchers: View watchers list
435 permission_view_issue_watchers: View watchers list
436 permission_add_issue_watchers: Add watchers
436 permission_add_issue_watchers: Add watchers
437 permission_delete_issue_watchers: Delete watchers
437 permission_delete_issue_watchers: Delete watchers
438 permission_log_time: Log spent time
438 permission_log_time: Log spent time
439 permission_view_time_entries: View spent time
439 permission_view_time_entries: View spent time
440 permission_edit_time_entries: Edit time logs
440 permission_edit_time_entries: Edit time logs
441 permission_edit_own_time_entries: Edit own time logs
441 permission_edit_own_time_entries: Edit own time logs
442 permission_manage_news: Manage news
442 permission_manage_news: Manage news
443 permission_comment_news: Comment news
443 permission_comment_news: Comment news
444 permission_view_documents: View documents
444 permission_view_documents: View documents
445 permission_add_documents: Add documents
445 permission_add_documents: Add documents
446 permission_edit_documents: Edit documents
446 permission_edit_documents: Edit documents
447 permission_delete_documents: Delete documents
447 permission_delete_documents: Delete documents
448 permission_manage_files: Manage files
448 permission_manage_files: Manage files
449 permission_view_files: View files
449 permission_view_files: View files
450 permission_manage_wiki: Manage wiki
450 permission_manage_wiki: Manage wiki
451 permission_rename_wiki_pages: Rename wiki pages
451 permission_rename_wiki_pages: Rename wiki pages
452 permission_delete_wiki_pages: Delete wiki pages
452 permission_delete_wiki_pages: Delete wiki pages
453 permission_view_wiki_pages: View wiki
453 permission_view_wiki_pages: View wiki
454 permission_view_wiki_edits: View wiki history
454 permission_view_wiki_edits: View wiki history
455 permission_edit_wiki_pages: Edit wiki pages
455 permission_edit_wiki_pages: Edit wiki pages
456 permission_delete_wiki_pages_attachments: Delete attachments
456 permission_delete_wiki_pages_attachments: Delete attachments
457 permission_protect_wiki_pages: Protect wiki pages
457 permission_protect_wiki_pages: Protect wiki pages
458 permission_manage_repository: Manage repository
458 permission_manage_repository: Manage repository
459 permission_browse_repository: Browse repository
459 permission_browse_repository: Browse repository
460 permission_view_changesets: View changesets
460 permission_view_changesets: View changesets
461 permission_commit_access: Commit access
461 permission_commit_access: Commit access
462 permission_manage_boards: Manage forums
462 permission_manage_boards: Manage forums
463 permission_view_messages: View messages
463 permission_view_messages: View messages
464 permission_add_messages: Post messages
464 permission_add_messages: Post messages
465 permission_edit_messages: Edit messages
465 permission_edit_messages: Edit messages
466 permission_edit_own_messages: Edit own messages
466 permission_edit_own_messages: Edit own messages
467 permission_delete_messages: Delete messages
467 permission_delete_messages: Delete messages
468 permission_delete_own_messages: Delete own messages
468 permission_delete_own_messages: Delete own messages
469 permission_export_wiki_pages: Export wiki pages
469 permission_export_wiki_pages: Export wiki pages
470 permission_manage_subtasks: Manage subtasks
470 permission_manage_subtasks: Manage subtasks
471 permission_manage_related_issues: Manage related issues
471 permission_manage_related_issues: Manage related issues
472
472
473 project_module_issue_tracking: Issue tracking
473 project_module_issue_tracking: Issue tracking
474 project_module_time_tracking: Time tracking
474 project_module_time_tracking: Time tracking
475 project_module_news: News
475 project_module_news: News
476 project_module_documents: Documents
476 project_module_documents: Documents
477 project_module_files: Files
477 project_module_files: Files
478 project_module_wiki: Wiki
478 project_module_wiki: Wiki
479 project_module_repository: Repository
479 project_module_repository: Repository
480 project_module_boards: Forums
480 project_module_boards: Forums
481 project_module_calendar: Calendar
481 project_module_calendar: Calendar
482 project_module_gantt: Gantt
482 project_module_gantt: Gantt
483
483
484 label_user: User
484 label_user: User
485 label_user_plural: Users
485 label_user_plural: Users
486 label_user_new: New user
486 label_user_new: New user
487 label_user_anonymous: Anonymous
487 label_user_anonymous: Anonymous
488 label_project: Project
488 label_project: Project
489 label_project_new: New project
489 label_project_new: New project
490 label_project_plural: Projects
490 label_project_plural: Projects
491 label_x_projects:
491 label_x_projects:
492 zero: no projects
492 zero: no projects
493 one: 1 project
493 one: 1 project
494 other: "%{count} projects"
494 other: "%{count} projects"
495 label_project_all: All Projects
495 label_project_all: All Projects
496 label_project_latest: Latest projects
496 label_project_latest: Latest projects
497 label_issue: Issue
497 label_issue: Issue
498 label_issue_new: New issue
498 label_issue_new: New issue
499 label_issue_plural: Issues
499 label_issue_plural: Issues
500 label_issue_view_all: View all issues
500 label_issue_view_all: View all issues
501 label_issues_by: "Issues by %{value}"
501 label_issues_by: "Issues by %{value}"
502 label_issue_added: Issue added
502 label_issue_added: Issue added
503 label_issue_updated: Issue updated
503 label_issue_updated: Issue updated
504 label_issue_note_added: Note added
504 label_issue_note_added: Note added
505 label_issue_status_updated: Status updated
505 label_issue_status_updated: Status updated
506 label_issue_priority_updated: Priority updated
506 label_issue_priority_updated: Priority updated
507 label_document: Document
507 label_document: Document
508 label_document_new: New document
508 label_document_new: New document
509 label_document_plural: Documents
509 label_document_plural: Documents
510 label_document_added: Document added
510 label_document_added: Document added
511 label_role: Role
511 label_role: Role
512 label_role_plural: Roles
512 label_role_plural: Roles
513 label_role_new: New role
513 label_role_new: New role
514 label_role_and_permissions: Roles and permissions
514 label_role_and_permissions: Roles and permissions
515 label_role_anonymous: Anonymous
515 label_role_anonymous: Anonymous
516 label_role_non_member: Non member
516 label_role_non_member: Non member
517 label_member: Member
517 label_member: Member
518 label_member_new: New member
518 label_member_new: New member
519 label_member_plural: Members
519 label_member_plural: Members
520 label_tracker: Tracker
520 label_tracker: Tracker
521 label_tracker_plural: Trackers
521 label_tracker_plural: Trackers
522 label_tracker_new: New tracker
522 label_tracker_new: New tracker
523 label_workflow: Workflow
523 label_workflow: Workflow
524 label_issue_status: Issue status
524 label_issue_status: Issue status
525 label_issue_status_plural: Issue statuses
525 label_issue_status_plural: Issue statuses
526 label_issue_status_new: New status
526 label_issue_status_new: New status
527 label_issue_category: Issue category
527 label_issue_category: Issue category
528 label_issue_category_plural: Issue categories
528 label_issue_category_plural: Issue categories
529 label_issue_category_new: New category
529 label_issue_category_new: New category
530 label_custom_field: Custom field
530 label_custom_field: Custom field
531 label_custom_field_plural: Custom fields
531 label_custom_field_plural: Custom fields
532 label_custom_field_new: New custom field
532 label_custom_field_new: New custom field
533 label_enumerations: Enumerations
533 label_enumerations: Enumerations
534 label_enumeration_new: New value
534 label_enumeration_new: New value
535 label_information: Information
535 label_information: Information
536 label_information_plural: Information
536 label_information_plural: Information
537 label_please_login: Please log in
537 label_please_login: Please log in
538 label_register: Register
538 label_register: Register
539 label_login_with_open_id_option: or login with OpenID
539 label_login_with_open_id_option: or login with OpenID
540 label_password_lost: Lost password
540 label_password_lost: Lost password
541 label_home: Home
541 label_home: Home
542 label_my_page: My page
542 label_my_page: My page
543 label_my_account: My account
543 label_my_account: My account
544 label_my_projects: My projects
544 label_my_projects: My projects
545 label_my_page_block: My page block
545 label_my_page_block: My page block
546 label_administration: Administration
546 label_administration: Administration
547 label_login: Sign in
547 label_login: Sign in
548 label_logout: Sign out
548 label_logout: Sign out
549 label_help: Help
549 label_help: Help
550 label_reported_issues: Reported issues
550 label_reported_issues: Reported issues
551 label_assigned_to_me_issues: Issues assigned to me
551 label_assigned_to_me_issues: Issues assigned to me
552 label_last_login: Last connection
552 label_last_login: Last connection
553 label_registered_on: Registered on
553 label_registered_on: Registered on
554 label_activity: Activity
554 label_activity: Activity
555 label_overall_activity: Overall activity
555 label_overall_activity: Overall activity
556 label_user_activity: "%{value}'s activity"
556 label_user_activity: "%{value}'s activity"
557 label_new: New
557 label_new: New
558 label_logged_as: Logged in as
558 label_logged_as: Logged in as
559 label_environment: Environment
559 label_environment: Environment
560 label_authentication: Authentication
560 label_authentication: Authentication
561 label_auth_source: Authentication mode
561 label_auth_source: Authentication mode
562 label_auth_source_new: New authentication mode
562 label_auth_source_new: New authentication mode
563 label_auth_source_plural: Authentication modes
563 label_auth_source_plural: Authentication modes
564 label_subproject_plural: Subprojects
564 label_subproject_plural: Subprojects
565 label_subproject_new: New subproject
565 label_subproject_new: New subproject
566 label_and_its_subprojects: "%{value} and its subprojects"
566 label_and_its_subprojects: "%{value} and its subprojects"
567 label_min_max_length: Min - Max length
567 label_min_max_length: Min - Max length
568 label_list: List
568 label_list: List
569 label_date: Date
569 label_date: Date
570 label_integer: Integer
570 label_integer: Integer
571 label_float: Float
571 label_float: Float
572 label_boolean: Boolean
572 label_boolean: Boolean
573 label_string: Text
573 label_string: Text
574 label_text: Long text
574 label_text: Long text
575 label_attribute: Attribute
575 label_attribute: Attribute
576 label_attribute_plural: Attributes
576 label_attribute_plural: Attributes
577 label_no_data: No data to display
577 label_no_data: No data to display
578 label_change_status: Change status
578 label_change_status: Change status
579 label_history: History
579 label_history: History
580 label_attachment: File
580 label_attachment: File
581 label_attachment_new: New file
581 label_attachment_new: New file
582 label_attachment_delete: Delete file
582 label_attachment_delete: Delete file
583 label_attachment_plural: Files
583 label_attachment_plural: Files
584 label_file_added: File added
584 label_file_added: File added
585 label_report: Report
585 label_report: Report
586 label_report_plural: Reports
586 label_report_plural: Reports
587 label_news: News
587 label_news: News
588 label_news_new: Add news
588 label_news_new: Add news
589 label_news_plural: News
589 label_news_plural: News
590 label_news_latest: Latest news
590 label_news_latest: Latest news
591 label_news_view_all: View all news
591 label_news_view_all: View all news
592 label_news_added: News added
592 label_news_added: News added
593 label_news_comment_added: Comment added to a news
593 label_news_comment_added: Comment added to a news
594 label_settings: Settings
594 label_settings: Settings
595 label_overview: Overview
595 label_overview: Overview
596 label_version: Version
596 label_version: Version
597 label_version_new: New version
597 label_version_new: New version
598 label_version_plural: Versions
598 label_version_plural: Versions
599 label_close_versions: Close completed versions
599 label_close_versions: Close completed versions
600 label_confirmation: Confirmation
600 label_confirmation: Confirmation
601 label_export_to: 'Also available in:'
601 label_export_to: 'Also available in:'
602 label_read: Read...
602 label_read: Read...
603 label_public_projects: Public projects
603 label_public_projects: Public projects
604 label_open_issues: open
604 label_open_issues: open
605 label_open_issues_plural: open
605 label_open_issues_plural: open
606 label_closed_issues: closed
606 label_closed_issues: closed
607 label_closed_issues_plural: closed
607 label_closed_issues_plural: closed
608 label_x_open_issues_abbr_on_total:
608 label_x_open_issues_abbr_on_total:
609 zero: 0 open / %{total}
609 zero: 0 open / %{total}
610 one: 1 open / %{total}
610 one: 1 open / %{total}
611 other: "%{count} open / %{total}"
611 other: "%{count} open / %{total}"
612 label_x_open_issues_abbr:
612 label_x_open_issues_abbr:
613 zero: 0 open
613 zero: 0 open
614 one: 1 open
614 one: 1 open
615 other: "%{count} open"
615 other: "%{count} open"
616 label_x_closed_issues_abbr:
616 label_x_closed_issues_abbr:
617 zero: 0 closed
617 zero: 0 closed
618 one: 1 closed
618 one: 1 closed
619 other: "%{count} closed"
619 other: "%{count} closed"
620 label_x_issues:
620 label_x_issues:
621 zero: 0 issues
621 zero: 0 issues
622 one: 1 issue
622 one: 1 issue
623 other: "%{count} issues"
623 other: "%{count} issues"
624 label_total: Total
624 label_total: Total
625 label_total_time: Total time
625 label_total_time: Total time
626 label_permissions: Permissions
626 label_permissions: Permissions
627 label_current_status: Current status
627 label_current_status: Current status
628 label_new_statuses_allowed: New statuses allowed
628 label_new_statuses_allowed: New statuses allowed
629 label_all: all
629 label_all: all
630 label_any: any
630 label_any: any
631 label_none: none
631 label_none: none
632 label_nobody: nobody
632 label_nobody: nobody
633 label_next: Next
633 label_next: Next
634 label_previous: Previous
634 label_previous: Previous
635 label_used_by: Used by
635 label_used_by: Used by
636 label_details: Details
636 label_details: Details
637 label_add_note: Add a note
637 label_add_note: Add a note
638 label_per_page: Per page
638 label_per_page: Per page
639 label_calendar: Calendar
639 label_calendar: Calendar
640 label_months_from: months from
640 label_months_from: months from
641 label_gantt: Gantt
641 label_gantt: Gantt
642 label_internal: Internal
642 label_internal: Internal
643 label_last_changes: "last %{count} changes"
643 label_last_changes: "last %{count} changes"
644 label_change_view_all: View all changes
644 label_change_view_all: View all changes
645 label_personalize_page: Personalize this page
645 label_personalize_page: Personalize this page
646 label_comment: Comment
646 label_comment: Comment
647 label_comment_plural: Comments
647 label_comment_plural: Comments
648 label_x_comments:
648 label_x_comments:
649 zero: no comments
649 zero: no comments
650 one: 1 comment
650 one: 1 comment
651 other: "%{count} comments"
651 other: "%{count} comments"
652 label_comment_add: Add a comment
652 label_comment_add: Add a comment
653 label_comment_added: Comment added
653 label_comment_added: Comment added
654 label_comment_delete: Delete comments
654 label_comment_delete: Delete comments
655 label_query: Custom query
655 label_query: Custom query
656 label_query_plural: Custom queries
656 label_query_plural: Custom queries
657 label_query_new: New query
657 label_query_new: New query
658 label_my_queries: My custom queries
658 label_my_queries: My custom queries
659 label_filter_add: Add filter
659 label_filter_add: Add filter
660 label_filter_plural: Filters
660 label_filter_plural: Filters
661 label_equals: is
661 label_equals: is
662 label_not_equals: is not
662 label_not_equals: is not
663 label_in_less_than: in less than
663 label_in_less_than: in less than
664 label_in_more_than: in more than
664 label_in_more_than: in more than
665 label_in_the_next_days: in the next
665 label_in_the_next_days: in the next
666 label_in_the_past_days: in the past
666 label_in_the_past_days: in the past
667 label_greater_or_equal: '>='
667 label_greater_or_equal: '>='
668 label_less_or_equal: '<='
668 label_less_or_equal: '<='
669 label_between: between
669 label_between: between
670 label_in: in
670 label_in: in
671 label_today: today
671 label_today: today
672 label_all_time: all time
672 label_all_time: all time
673 label_yesterday: yesterday
673 label_yesterday: yesterday
674 label_this_week: this week
674 label_this_week: this week
675 label_last_week: last week
675 label_last_week: last week
676 label_last_n_weeks: "last %{count} weeks"
676 label_last_n_weeks: "last %{count} weeks"
677 label_last_n_days: "last %{count} days"
677 label_last_n_days: "last %{count} days"
678 label_this_month: this month
678 label_this_month: this month
679 label_last_month: last month
679 label_last_month: last month
680 label_this_year: this year
680 label_this_year: this year
681 label_date_range: Date range
681 label_date_range: Date range
682 label_less_than_ago: less than days ago
682 label_less_than_ago: less than days ago
683 label_more_than_ago: more than days ago
683 label_more_than_ago: more than days ago
684 label_ago: days ago
684 label_ago: days ago
685 label_contains: contains
685 label_contains: contains
686 label_not_contains: doesn't contain
686 label_not_contains: doesn't contain
687 label_any_issues_in_project: any issues in project
687 label_any_issues_in_project: any issues in project
688 label_any_issues_not_in_project: any issues not in project
688 label_any_issues_not_in_project: any issues not in project
689 label_no_issues_in_project: no issues in project
689 label_no_issues_in_project: no issues in project
690 label_day_plural: days
690 label_day_plural: days
691 label_repository: Repository
691 label_repository: Repository
692 label_repository_new: New repository
692 label_repository_new: New repository
693 label_repository_plural: Repositories
693 label_repository_plural: Repositories
694 label_browse: Browse
694 label_browse: Browse
695 label_branch: Branch
695 label_branch: Branch
696 label_tag: Tag
696 label_tag: Tag
697 label_revision: Revision
697 label_revision: Revision
698 label_revision_plural: Revisions
698 label_revision_plural: Revisions
699 label_revision_id: "Revision %{value}"
699 label_revision_id: "Revision %{value}"
700 label_associated_revisions: Associated revisions
700 label_associated_revisions: Associated revisions
701 label_added: added
701 label_added: added
702 label_modified: modified
702 label_modified: modified
703 label_copied: copied
703 label_copied: copied
704 label_renamed: renamed
704 label_renamed: renamed
705 label_deleted: deleted
705 label_deleted: deleted
706 label_latest_revision: Latest revision
706 label_latest_revision: Latest revision
707 label_latest_revision_plural: Latest revisions
707 label_latest_revision_plural: Latest revisions
708 label_view_revisions: View revisions
708 label_view_revisions: View revisions
709 label_view_all_revisions: View all revisions
709 label_view_all_revisions: View all revisions
710 label_max_size: Maximum size
710 label_max_size: Maximum size
711 label_sort_highest: Move to top
711 label_sort_highest: Move to top
712 label_sort_higher: Move up
712 label_sort_higher: Move up
713 label_sort_lower: Move down
713 label_sort_lower: Move down
714 label_sort_lowest: Move to bottom
714 label_sort_lowest: Move to bottom
715 label_roadmap: Roadmap
715 label_roadmap: Roadmap
716 label_roadmap_due_in: "Due in %{value}"
716 label_roadmap_due_in: "Due in %{value}"
717 label_roadmap_overdue: "%{value} late"
717 label_roadmap_overdue: "%{value} late"
718 label_roadmap_no_issues: No issues for this version
718 label_roadmap_no_issues: No issues for this version
719 label_search: Search
719 label_search: Search
720 label_result_plural: Results
720 label_result_plural: Results
721 label_all_words: All words
721 label_all_words: All words
722 label_wiki: Wiki
722 label_wiki: Wiki
723 label_wiki_edit: Wiki edit
723 label_wiki_edit: Wiki edit
724 label_wiki_edit_plural: Wiki edits
724 label_wiki_edit_plural: Wiki edits
725 label_wiki_page: Wiki page
725 label_wiki_page: Wiki page
726 label_wiki_page_plural: Wiki pages
726 label_wiki_page_plural: Wiki pages
727 label_index_by_title: Index by title
727 label_index_by_title: Index by title
728 label_index_by_date: Index by date
728 label_index_by_date: Index by date
729 label_current_version: Current version
729 label_current_version: Current version
730 label_preview: Preview
730 label_preview: Preview
731 label_feed_plural: Feeds
731 label_feed_plural: Feeds
732 label_changes_details: Details of all changes
732 label_changes_details: Details of all changes
733 label_issue_tracking: Issue tracking
733 label_issue_tracking: Issue tracking
734 label_spent_time: Spent time
734 label_spent_time: Spent time
735 label_overall_spent_time: Overall spent time
735 label_overall_spent_time: Overall spent time
736 label_f_hour: "%{value} hour"
736 label_f_hour: "%{value} hour"
737 label_f_hour_plural: "%{value} hours"
737 label_f_hour_plural: "%{value} hours"
738 label_time_tracking: Time tracking
738 label_time_tracking: Time tracking
739 label_change_plural: Changes
739 label_change_plural: Changes
740 label_statistics: Statistics
740 label_statistics: Statistics
741 label_commits_per_month: Commits per month
741 label_commits_per_month: Commits per month
742 label_commits_per_author: Commits per author
742 label_commits_per_author: Commits per author
743 label_diff: diff
743 label_diff: diff
744 label_view_diff: View differences
744 label_view_diff: View differences
745 label_diff_inline: inline
745 label_diff_inline: inline
746 label_diff_side_by_side: side by side
746 label_diff_side_by_side: side by side
747 label_options: Options
747 label_options: Options
748 label_copy_workflow_from: Copy workflow from
748 label_copy_workflow_from: Copy workflow from
749 label_permissions_report: Permissions report
749 label_permissions_report: Permissions report
750 label_watched_issues: Watched issues
750 label_watched_issues: Watched issues
751 label_related_issues: Related issues
751 label_related_issues: Related issues
752 label_applied_status: Applied status
752 label_applied_status: Applied status
753 label_loading: Loading...
753 label_loading: Loading...
754 label_relation_new: New relation
754 label_relation_new: New relation
755 label_relation_delete: Delete relation
755 label_relation_delete: Delete relation
756 label_relates_to: Related to
756 label_relates_to: Related to
757 label_duplicates: Duplicates
757 label_duplicates: Duplicates
758 label_duplicated_by: Duplicated by
758 label_duplicated_by: Duplicated by
759 label_blocks: Blocks
759 label_blocks: Blocks
760 label_blocked_by: Blocked by
760 label_blocked_by: Blocked by
761 label_precedes: Precedes
761 label_precedes: Precedes
762 label_follows: Follows
762 label_follows: Follows
763 label_copied_to: Copied to
763 label_copied_to: Copied to
764 label_copied_from: Copied from
764 label_copied_from: Copied from
765 label_end_to_start: end to start
765 label_end_to_start: end to start
766 label_end_to_end: end to end
766 label_end_to_end: end to end
767 label_start_to_start: start to start
767 label_start_to_start: start to start
768 label_start_to_end: start to end
768 label_start_to_end: start to end
769 label_stay_logged_in: Stay logged in
769 label_stay_logged_in: Stay logged in
770 label_disabled: disabled
770 label_disabled: disabled
771 label_show_completed_versions: Show completed versions
771 label_show_completed_versions: Show completed versions
772 label_me: me
772 label_me: me
773 label_board: Forum
773 label_board: Forum
774 label_board_new: New forum
774 label_board_new: New forum
775 label_board_plural: Forums
775 label_board_plural: Forums
776 label_board_locked: Locked
776 label_board_locked: Locked
777 label_board_sticky: Sticky
777 label_board_sticky: Sticky
778 label_topic_plural: Topics
778 label_topic_plural: Topics
779 label_message_plural: Messages
779 label_message_plural: Messages
780 label_message_last: Last message
780 label_message_last: Last message
781 label_message_new: New message
781 label_message_new: New message
782 label_message_posted: Message added
782 label_message_posted: Message added
783 label_reply_plural: Replies
783 label_reply_plural: Replies
784 label_send_information: Send account information to the user
784 label_send_information: Send account information to the user
785 label_year: Year
785 label_year: Year
786 label_month: Month
786 label_month: Month
787 label_week: Week
787 label_week: Week
788 label_date_from: From
788 label_date_from: From
789 label_date_to: To
789 label_date_to: To
790 label_language_based: Based on user's language
790 label_language_based: Based on user's language
791 label_sort_by: "Sort by %{value}"
791 label_sort_by: "Sort by %{value}"
792 label_send_test_email: Send a test email
792 label_send_test_email: Send a test email
793 label_feeds_access_key: Atom access key
793 label_feeds_access_key: Atom access key
794 label_missing_feeds_access_key: Missing a Atom access key
794 label_missing_feeds_access_key: Missing a Atom access key
795 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
795 label_feeds_access_key_created_on: "Atom access key created %{value} ago"
796 label_module_plural: Modules
796 label_module_plural: Modules
797 label_added_time_by: "Added by %{author} %{age} ago"
797 label_added_time_by: "Added by %{author} %{age} ago"
798 label_updated_time_by: "Updated by %{author} %{age} ago"
798 label_updated_time_by: "Updated by %{author} %{age} ago"
799 label_updated_time: "Updated %{value} ago"
799 label_updated_time: "Updated %{value} ago"
800 label_jump_to_a_project: Jump to a project...
800 label_jump_to_a_project: Jump to a project...
801 label_file_plural: Files
801 label_file_plural: Files
802 label_changeset_plural: Changesets
802 label_changeset_plural: Changesets
803 label_default_columns: Default columns
803 label_default_columns: Default columns
804 label_no_change_option: (No change)
804 label_no_change_option: (No change)
805 label_bulk_edit_selected_issues: Bulk edit selected issues
805 label_bulk_edit_selected_issues: Bulk edit selected issues
806 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
806 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
807 label_theme: Theme
807 label_theme: Theme
808 label_default: Default
808 label_default: Default
809 label_search_titles_only: Search titles only
809 label_search_titles_only: Search titles only
810 label_user_mail_option_all: "For any event on all my projects"
810 label_user_mail_option_all: "For any event on all my projects"
811 label_user_mail_option_selected: "For any event on the selected projects only..."
811 label_user_mail_option_selected: "For any event on the selected projects only..."
812 label_user_mail_option_none: "No events"
812 label_user_mail_option_none: "No events"
813 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
813 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
814 label_user_mail_option_only_assigned: "Only for things I am assigned to"
814 label_user_mail_option_only_assigned: "Only for things I am assigned to"
815 label_user_mail_option_only_owner: "Only for things I am the owner of"
815 label_user_mail_option_only_owner: "Only for things I am the owner of"
816 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
816 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
817 label_registration_activation_by_email: account activation by email
817 label_registration_activation_by_email: account activation by email
818 label_registration_manual_activation: manual account activation
818 label_registration_manual_activation: manual account activation
819 label_registration_automatic_activation: automatic account activation
819 label_registration_automatic_activation: automatic account activation
820 label_display_per_page: "Per page: %{value}"
820 label_display_per_page: "Per page: %{value}"
821 label_age: Age
821 label_age: Age
822 label_change_properties: Change properties
822 label_change_properties: Change properties
823 label_general: General
823 label_general: General
824 label_more: More
824 label_more: More
825 label_scm: SCM
825 label_scm: SCM
826 label_plugins: Plugins
826 label_plugins: Plugins
827 label_ldap_authentication: LDAP authentication
827 label_ldap_authentication: LDAP authentication
828 label_downloads_abbr: D/L
828 label_downloads_abbr: D/L
829 label_optional_description: Optional description
829 label_optional_description: Optional description
830 label_add_another_file: Add another file
830 label_add_another_file: Add another file
831 label_preferences: Preferences
831 label_preferences: Preferences
832 label_chronological_order: In chronological order
832 label_chronological_order: In chronological order
833 label_reverse_chronological_order: In reverse chronological order
833 label_reverse_chronological_order: In reverse chronological order
834 label_planning: Planning
834 label_planning: Planning
835 label_incoming_emails: Incoming emails
835 label_incoming_emails: Incoming emails
836 label_generate_key: Generate a key
836 label_generate_key: Generate a key
837 label_issue_watchers: Watchers
837 label_issue_watchers: Watchers
838 label_example: Example
838 label_example: Example
839 label_display: Display
839 label_display: Display
840 label_sort: Sort
840 label_sort: Sort
841 label_ascending: Ascending
841 label_ascending: Ascending
842 label_descending: Descending
842 label_descending: Descending
843 label_date_from_to: From %{start} to %{end}
843 label_date_from_to: From %{start} to %{end}
844 label_wiki_content_added: Wiki page added
844 label_wiki_content_added: Wiki page added
845 label_wiki_content_updated: Wiki page updated
845 label_wiki_content_updated: Wiki page updated
846 label_group: Group
846 label_group: Group
847 label_group_plural: Groups
847 label_group_plural: Groups
848 label_group_new: New group
848 label_group_new: New group
849 label_time_entry_plural: Spent time
849 label_time_entry_plural: Spent time
850 label_version_sharing_none: Not shared
850 label_version_sharing_none: Not shared
851 label_version_sharing_descendants: With subprojects
851 label_version_sharing_descendants: With subprojects
852 label_version_sharing_hierarchy: With project hierarchy
852 label_version_sharing_hierarchy: With project hierarchy
853 label_version_sharing_tree: With project tree
853 label_version_sharing_tree: With project tree
854 label_version_sharing_system: With all projects
854 label_version_sharing_system: With all projects
855 label_update_issue_done_ratios: Update issue done ratios
855 label_update_issue_done_ratios: Update issue done ratios
856 label_copy_source: Source
856 label_copy_source: Source
857 label_copy_target: Target
857 label_copy_target: Target
858 label_copy_same_as_target: Same as target
858 label_copy_same_as_target: Same as target
859 label_display_used_statuses_only: Only display statuses that are used by this tracker
859 label_display_used_statuses_only: Only display statuses that are used by this tracker
860 label_api_access_key: API access key
860 label_api_access_key: API access key
861 label_missing_api_access_key: Missing an API access key
861 label_missing_api_access_key: Missing an API access key
862 label_api_access_key_created_on: "API access key created %{value} ago"
862 label_api_access_key_created_on: "API access key created %{value} ago"
863 label_profile: Profile
863 label_profile: Profile
864 label_subtask_plural: Subtasks
864 label_subtask_plural: Subtasks
865 label_project_copy_notifications: Send email notifications during the project copy
865 label_project_copy_notifications: Send email notifications during the project copy
866 label_principal_search: "Search for user or group:"
866 label_principal_search: "Search for user or group:"
867 label_user_search: "Search for user:"
867 label_user_search: "Search for user:"
868 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
868 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
869 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
869 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
870 label_issues_visibility_all: All issues
870 label_issues_visibility_all: All issues
871 label_issues_visibility_public: All non private issues
871 label_issues_visibility_public: All non private issues
872 label_issues_visibility_own: Issues created by or assigned to the user
872 label_issues_visibility_own: Issues created by or assigned to the user
873 label_git_report_last_commit: Report last commit for files and directories
873 label_git_report_last_commit: Report last commit for files and directories
874 label_parent_revision: Parent
874 label_parent_revision: Parent
875 label_child_revision: Child
875 label_child_revision: Child
876 label_export_options: "%{export_format} export options"
876 label_export_options: "%{export_format} export options"
877 label_copy_attachments: Copy attachments
877 label_copy_attachments: Copy attachments
878 label_copy_subtasks: Copy subtasks
878 label_copy_subtasks: Copy subtasks
879 label_item_position: "%{position} of %{count}"
879 label_item_position: "%{position} of %{count}"
880 label_completed_versions: Completed versions
880 label_completed_versions: Completed versions
881 label_search_for_watchers: Search for watchers to add
881 label_search_for_watchers: Search for watchers to add
882 label_session_expiration: Session expiration
882 label_session_expiration: Session expiration
883 label_show_closed_projects: View closed projects
883 label_show_closed_projects: View closed projects
884 label_status_transitions: Status transitions
884 label_status_transitions: Status transitions
885 label_fields_permissions: Fields permissions
885 label_fields_permissions: Fields permissions
886 label_readonly: Read-only
886 label_readonly: Read-only
887 label_required: Required
887 label_required: Required
888 label_attribute_of_project: "Project's %{name}"
888 label_attribute_of_project: "Project's %{name}"
889 label_attribute_of_issue: "Issue's %{name}"
889 label_attribute_of_issue: "Issue's %{name}"
890 label_attribute_of_author: "Author's %{name}"
890 label_attribute_of_author: "Author's %{name}"
891 label_attribute_of_assigned_to: "Assignee's %{name}"
891 label_attribute_of_assigned_to: "Assignee's %{name}"
892 label_attribute_of_user: "User's %{name}"
892 label_attribute_of_user: "User's %{name}"
893 label_attribute_of_fixed_version: "Target version's %{name}"
893 label_attribute_of_fixed_version: "Target version's %{name}"
894 label_cross_project_descendants: With subprojects
894 label_cross_project_descendants: With subprojects
895 label_cross_project_tree: With project tree
895 label_cross_project_tree: With project tree
896 label_cross_project_hierarchy: With project hierarchy
896 label_cross_project_hierarchy: With project hierarchy
897 label_cross_project_system: With all projects
897 label_cross_project_system: With all projects
898 label_gantt_progress_line: Progress line
898 label_gantt_progress_line: Progress line
899 label_visibility_private: to me only
900 label_visibility_roles: to these roles only
901 label_visibility_public: to any users
899
902
900 button_login: Login
903 button_login: Login
901 button_submit: Submit
904 button_submit: Submit
902 button_save: Save
905 button_save: Save
903 button_check_all: Check all
906 button_check_all: Check all
904 button_uncheck_all: Uncheck all
907 button_uncheck_all: Uncheck all
905 button_collapse_all: Collapse all
908 button_collapse_all: Collapse all
906 button_expand_all: Expand all
909 button_expand_all: Expand all
907 button_delete: Delete
910 button_delete: Delete
908 button_create: Create
911 button_create: Create
909 button_create_and_continue: Create and continue
912 button_create_and_continue: Create and continue
910 button_test: Test
913 button_test: Test
911 button_edit: Edit
914 button_edit: Edit
912 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
915 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
913 button_add: Add
916 button_add: Add
914 button_change: Change
917 button_change: Change
915 button_apply: Apply
918 button_apply: Apply
916 button_clear: Clear
919 button_clear: Clear
917 button_lock: Lock
920 button_lock: Lock
918 button_unlock: Unlock
921 button_unlock: Unlock
919 button_download: Download
922 button_download: Download
920 button_list: List
923 button_list: List
921 button_view: View
924 button_view: View
922 button_move: Move
925 button_move: Move
923 button_move_and_follow: Move and follow
926 button_move_and_follow: Move and follow
924 button_back: Back
927 button_back: Back
925 button_cancel: Cancel
928 button_cancel: Cancel
926 button_activate: Activate
929 button_activate: Activate
927 button_sort: Sort
930 button_sort: Sort
928 button_log_time: Log time
931 button_log_time: Log time
929 button_rollback: Rollback to this version
932 button_rollback: Rollback to this version
930 button_watch: Watch
933 button_watch: Watch
931 button_unwatch: Unwatch
934 button_unwatch: Unwatch
932 button_reply: Reply
935 button_reply: Reply
933 button_archive: Archive
936 button_archive: Archive
934 button_unarchive: Unarchive
937 button_unarchive: Unarchive
935 button_reset: Reset
938 button_reset: Reset
936 button_rename: Rename
939 button_rename: Rename
937 button_change_password: Change password
940 button_change_password: Change password
938 button_copy: Copy
941 button_copy: Copy
939 button_copy_and_follow: Copy and follow
942 button_copy_and_follow: Copy and follow
940 button_annotate: Annotate
943 button_annotate: Annotate
941 button_update: Update
944 button_update: Update
942 button_configure: Configure
945 button_configure: Configure
943 button_quote: Quote
946 button_quote: Quote
944 button_duplicate: Duplicate
947 button_duplicate: Duplicate
945 button_show: Show
948 button_show: Show
946 button_hide: Hide
949 button_hide: Hide
947 button_edit_section: Edit this section
950 button_edit_section: Edit this section
948 button_export: Export
951 button_export: Export
949 button_delete_my_account: Delete my account
952 button_delete_my_account: Delete my account
950 button_close: Close
953 button_close: Close
951 button_reopen: Reopen
954 button_reopen: Reopen
952
955
953 status_active: active
956 status_active: active
954 status_registered: registered
957 status_registered: registered
955 status_locked: locked
958 status_locked: locked
956
959
957 project_status_active: active
960 project_status_active: active
958 project_status_closed: closed
961 project_status_closed: closed
959 project_status_archived: archived
962 project_status_archived: archived
960
963
961 version_status_open: open
964 version_status_open: open
962 version_status_locked: locked
965 version_status_locked: locked
963 version_status_closed: closed
966 version_status_closed: closed
964
967
965 field_active: Active
968 field_active: Active
966
969
967 text_select_mail_notifications: Select actions for which email notifications should be sent.
970 text_select_mail_notifications: Select actions for which email notifications should be sent.
968 text_regexp_info: eg. ^[A-Z0-9]+$
971 text_regexp_info: eg. ^[A-Z0-9]+$
969 text_min_max_length_info: 0 means no restriction
972 text_min_max_length_info: 0 means no restriction
970 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
973 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
971 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
974 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
972 text_workflow_edit: Select a role and a tracker to edit the workflow
975 text_workflow_edit: Select a role and a tracker to edit the workflow
973 text_are_you_sure: Are you sure?
976 text_are_you_sure: Are you sure?
974 text_journal_changed: "%{label} changed from %{old} to %{new}"
977 text_journal_changed: "%{label} changed from %{old} to %{new}"
975 text_journal_changed_no_detail: "%{label} updated"
978 text_journal_changed_no_detail: "%{label} updated"
976 text_journal_set_to: "%{label} set to %{value}"
979 text_journal_set_to: "%{label} set to %{value}"
977 text_journal_deleted: "%{label} deleted (%{old})"
980 text_journal_deleted: "%{label} deleted (%{old})"
978 text_journal_added: "%{label} %{value} added"
981 text_journal_added: "%{label} %{value} added"
979 text_tip_issue_begin_day: issue beginning this day
982 text_tip_issue_begin_day: issue beginning this day
980 text_tip_issue_end_day: issue ending this day
983 text_tip_issue_end_day: issue ending this day
981 text_tip_issue_begin_end_day: issue beginning and ending this day
984 text_tip_issue_begin_end_day: issue beginning and ending this day
982 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed.'
985 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed.'
983 text_caracters_maximum: "%{count} characters maximum."
986 text_caracters_maximum: "%{count} characters maximum."
984 text_caracters_minimum: "Must be at least %{count} characters long."
987 text_caracters_minimum: "Must be at least %{count} characters long."
985 text_length_between: "Length between %{min} and %{max} characters."
988 text_length_between: "Length between %{min} and %{max} characters."
986 text_tracker_no_workflow: No workflow defined for this tracker
989 text_tracker_no_workflow: No workflow defined for this tracker
987 text_unallowed_characters: Unallowed characters
990 text_unallowed_characters: Unallowed characters
988 text_comma_separated: Multiple values allowed (comma separated).
991 text_comma_separated: Multiple values allowed (comma separated).
989 text_line_separated: Multiple values allowed (one line for each value).
992 text_line_separated: Multiple values allowed (one line for each value).
990 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
993 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
991 text_issue_added: "Issue %{id} has been reported by %{author}."
994 text_issue_added: "Issue %{id} has been reported by %{author}."
992 text_issue_updated: "Issue %{id} has been updated by %{author}."
995 text_issue_updated: "Issue %{id} has been updated by %{author}."
993 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
996 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
994 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
997 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
995 text_issue_category_destroy_assignments: Remove category assignments
998 text_issue_category_destroy_assignments: Remove category assignments
996 text_issue_category_reassign_to: Reassign issues to this category
999 text_issue_category_reassign_to: Reassign issues to this category
997 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
1000 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
998 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
1001 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
999 text_load_default_configuration: Load the default configuration
1002 text_load_default_configuration: Load the default configuration
1000 text_status_changed_by_changeset: "Applied in changeset %{value}."
1003 text_status_changed_by_changeset: "Applied in changeset %{value}."
1001 text_time_logged_by_changeset: "Applied in changeset %{value}."
1004 text_time_logged_by_changeset: "Applied in changeset %{value}."
1002 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1005 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
1003 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1006 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
1004 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1007 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
1005 text_select_project_modules: 'Select modules to enable for this project:'
1008 text_select_project_modules: 'Select modules to enable for this project:'
1006 text_default_administrator_account_changed: Default administrator account changed
1009 text_default_administrator_account_changed: Default administrator account changed
1007 text_file_repository_writable: Attachments directory writable
1010 text_file_repository_writable: Attachments directory writable
1008 text_plugin_assets_writable: Plugin assets directory writable
1011 text_plugin_assets_writable: Plugin assets directory writable
1009 text_rmagick_available: RMagick available (optional)
1012 text_rmagick_available: RMagick available (optional)
1010 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1013 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
1011 text_destroy_time_entries: Delete reported hours
1014 text_destroy_time_entries: Delete reported hours
1012 text_assign_time_entries_to_project: Assign reported hours to the project
1015 text_assign_time_entries_to_project: Assign reported hours to the project
1013 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1016 text_reassign_time_entries: 'Reassign reported hours to this issue:'
1014 text_user_wrote: "%{value} wrote:"
1017 text_user_wrote: "%{value} wrote:"
1015 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1018 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
1016 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1019 text_enumeration_category_reassign_to: 'Reassign them to this value:'
1017 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them."
1020 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them."
1018 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
1021 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
1019 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1022 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
1020 text_custom_field_possible_values_info: 'One line for each value'
1023 text_custom_field_possible_values_info: 'One line for each value'
1021 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1024 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1022 text_wiki_page_nullify_children: "Keep child pages as root pages"
1025 text_wiki_page_nullify_children: "Keep child pages as root pages"
1023 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1026 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1024 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1027 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1025 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
1028 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
1026 text_zoom_in: Zoom in
1029 text_zoom_in: Zoom in
1027 text_zoom_out: Zoom out
1030 text_zoom_out: Zoom out
1028 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1031 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1029 text_scm_path_encoding_note: "Default: UTF-8"
1032 text_scm_path_encoding_note: "Default: UTF-8"
1030 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1033 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1031 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1034 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1032 text_scm_command: Command
1035 text_scm_command: Command
1033 text_scm_command_version: Version
1036 text_scm_command_version: Version
1034 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1037 text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it.
1035 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1038 text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel.
1036 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1039 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1037 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1040 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1038 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1041 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1039 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1042 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1040 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1043 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1041 text_project_closed: This project is closed and read-only.
1044 text_project_closed: This project is closed and read-only.
1042 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1045 text_turning_multiple_off: "If you disable multiple values, multiple values will be removed in order to preserve only one value per item."
1043
1046
1044 default_role_manager: Manager
1047 default_role_manager: Manager
1045 default_role_developer: Developer
1048 default_role_developer: Developer
1046 default_role_reporter: Reporter
1049 default_role_reporter: Reporter
1047 default_tracker_bug: Bug
1050 default_tracker_bug: Bug
1048 default_tracker_feature: Feature
1051 default_tracker_feature: Feature
1049 default_tracker_support: Support
1052 default_tracker_support: Support
1050 default_issue_status_new: New
1053 default_issue_status_new: New
1051 default_issue_status_in_progress: In Progress
1054 default_issue_status_in_progress: In Progress
1052 default_issue_status_resolved: Resolved
1055 default_issue_status_resolved: Resolved
1053 default_issue_status_feedback: Feedback
1056 default_issue_status_feedback: Feedback
1054 default_issue_status_closed: Closed
1057 default_issue_status_closed: Closed
1055 default_issue_status_rejected: Rejected
1058 default_issue_status_rejected: Rejected
1056 default_doc_category_user: User documentation
1059 default_doc_category_user: User documentation
1057 default_doc_category_tech: Technical documentation
1060 default_doc_category_tech: Technical documentation
1058 default_priority_low: Low
1061 default_priority_low: Low
1059 default_priority_normal: Normal
1062 default_priority_normal: Normal
1060 default_priority_high: High
1063 default_priority_high: High
1061 default_priority_urgent: Urgent
1064 default_priority_urgent: Urgent
1062 default_priority_immediate: Immediate
1065 default_priority_immediate: Immediate
1063 default_activity_design: Design
1066 default_activity_design: Design
1064 default_activity_development: Development
1067 default_activity_development: Development
1065
1068
1066 enumeration_issue_priorities: Issue priorities
1069 enumeration_issue_priorities: Issue priorities
1067 enumeration_doc_categories: Document categories
1070 enumeration_doc_categories: Document categories
1068 enumeration_activities: Activities (time tracking)
1071 enumeration_activities: Activities (time tracking)
1069 enumeration_system_activity: System Activity
1072 enumeration_system_activity: System Activity
1070 description_filter: Filter
1073 description_filter: Filter
1071 description_search: Searchfield
1074 description_search: Searchfield
1072 description_choose_project: Projects
1075 description_choose_project: Projects
1073 description_project_scope: Search scope
1076 description_project_scope: Search scope
1074 description_notes: Notes
1077 description_notes: Notes
1075 description_message_content: Message content
1078 description_message_content: Message content
1076 description_query_sort_criteria_attribute: Sort attribute
1079 description_query_sort_criteria_attribute: Sort attribute
1077 description_query_sort_criteria_direction: Sort direction
1080 description_query_sort_criteria_direction: Sort direction
1078 description_user_mail_notification: Mail notification settings
1081 description_user_mail_notification: Mail notification settings
1079 description_available_columns: Available Columns
1082 description_available_columns: Available Columns
1080 description_selected_columns: Selected Columns
1083 description_selected_columns: Selected Columns
1081 description_all_columns: All Columns
1084 description_all_columns: All Columns
1082 description_issue_category_reassign: Choose issue category
1085 description_issue_category_reassign: Choose issue category
1083 description_wiki_subpages_reassign: Choose new parent page
1086 description_wiki_subpages_reassign: Choose new parent page
1084 description_date_range_list: Choose range from list
1087 description_date_range_list: Choose range from list
1085 description_date_range_interval: Choose range by selecting start and end date
1088 description_date_range_interval: Choose range by selecting start and end date
1086 description_date_from: Enter start date
1089 description_date_from: Enter start date
1087 description_date_to: Enter end date
1090 description_date_to: Enter end date
1088 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
1091 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
@@ -1,1106 +1,1109
1 # French translations for Ruby on Rails
1 # French translations for Ruby on Rails
2 # by Christian Lescuyer (christian@flyingcoders.com)
2 # by Christian Lescuyer (christian@flyingcoders.com)
3 # contributor: Sebastien Grosjean - ZenCocoon.com
3 # contributor: Sebastien Grosjean - ZenCocoon.com
4 # contributor: Thibaut Cuvelier - Developpez.com
4 # contributor: Thibaut Cuvelier - Developpez.com
5
5
6 fr:
6 fr:
7 direction: ltr
7 direction: ltr
8 date:
8 date:
9 formats:
9 formats:
10 default: "%d/%m/%Y"
10 default: "%d/%m/%Y"
11 short: "%e %b"
11 short: "%e %b"
12 long: "%e %B %Y"
12 long: "%e %B %Y"
13 long_ordinal: "%e %B %Y"
13 long_ordinal: "%e %B %Y"
14 only_day: "%e"
14 only_day: "%e"
15
15
16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
16 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
17 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
18 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
18 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
19 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
19 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
20 order:
20 order:
21 - :day
21 - :day
22 - :month
22 - :month
23 - :year
23 - :year
24
24
25 time:
25 time:
26 formats:
26 formats:
27 default: "%d/%m/%Y %H:%M"
27 default: "%d/%m/%Y %H:%M"
28 time: "%H:%M"
28 time: "%H:%M"
29 short: "%d %b %H:%M"
29 short: "%d %b %H:%M"
30 long: "%A %d %B %Y %H:%M:%S %Z"
30 long: "%A %d %B %Y %H:%M:%S %Z"
31 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
31 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
32 only_second: "%S"
32 only_second: "%S"
33 am: 'am'
33 am: 'am'
34 pm: 'pm'
34 pm: 'pm'
35
35
36 datetime:
36 datetime:
37 distance_in_words:
37 distance_in_words:
38 half_a_minute: "30 secondes"
38 half_a_minute: "30 secondes"
39 less_than_x_seconds:
39 less_than_x_seconds:
40 zero: "moins d'une seconde"
40 zero: "moins d'une seconde"
41 one: "moins d'une seconde"
41 one: "moins d'une seconde"
42 other: "moins de %{count} secondes"
42 other: "moins de %{count} secondes"
43 x_seconds:
43 x_seconds:
44 one: "1 seconde"
44 one: "1 seconde"
45 other: "%{count} secondes"
45 other: "%{count} secondes"
46 less_than_x_minutes:
46 less_than_x_minutes:
47 zero: "moins d'une minute"
47 zero: "moins d'une minute"
48 one: "moins d'une minute"
48 one: "moins d'une minute"
49 other: "moins de %{count} minutes"
49 other: "moins de %{count} minutes"
50 x_minutes:
50 x_minutes:
51 one: "1 minute"
51 one: "1 minute"
52 other: "%{count} minutes"
52 other: "%{count} minutes"
53 about_x_hours:
53 about_x_hours:
54 one: "environ une heure"
54 one: "environ une heure"
55 other: "environ %{count} heures"
55 other: "environ %{count} heures"
56 x_hours:
56 x_hours:
57 one: "une heure"
57 one: "une heure"
58 other: "%{count} heures"
58 other: "%{count} heures"
59 x_days:
59 x_days:
60 one: "un jour"
60 one: "un jour"
61 other: "%{count} jours"
61 other: "%{count} jours"
62 about_x_months:
62 about_x_months:
63 one: "environ un mois"
63 one: "environ un mois"
64 other: "environ %{count} mois"
64 other: "environ %{count} mois"
65 x_months:
65 x_months:
66 one: "un mois"
66 one: "un mois"
67 other: "%{count} mois"
67 other: "%{count} mois"
68 about_x_years:
68 about_x_years:
69 one: "environ un an"
69 one: "environ un an"
70 other: "environ %{count} ans"
70 other: "environ %{count} ans"
71 over_x_years:
71 over_x_years:
72 one: "plus d'un an"
72 one: "plus d'un an"
73 other: "plus de %{count} ans"
73 other: "plus de %{count} ans"
74 almost_x_years:
74 almost_x_years:
75 one: "presqu'un an"
75 one: "presqu'un an"
76 other: "presque %{count} ans"
76 other: "presque %{count} ans"
77 prompts:
77 prompts:
78 year: "Année"
78 year: "Année"
79 month: "Mois"
79 month: "Mois"
80 day: "Jour"
80 day: "Jour"
81 hour: "Heure"
81 hour: "Heure"
82 minute: "Minute"
82 minute: "Minute"
83 second: "Seconde"
83 second: "Seconde"
84
84
85 number:
85 number:
86 format:
86 format:
87 precision: 3
87 precision: 3
88 separator: ','
88 separator: ','
89 delimiter: ' '
89 delimiter: ' '
90 currency:
90 currency:
91 format:
91 format:
92 unit: '€'
92 unit: '€'
93 precision: 2
93 precision: 2
94 format: '%n %u'
94 format: '%n %u'
95 human:
95 human:
96 format:
96 format:
97 precision: 3
97 precision: 3
98 storage_units:
98 storage_units:
99 format: "%n %u"
99 format: "%n %u"
100 units:
100 units:
101 byte:
101 byte:
102 one: "octet"
102 one: "octet"
103 other: "octet"
103 other: "octet"
104 kb: "ko"
104 kb: "ko"
105 mb: "Mo"
105 mb: "Mo"
106 gb: "Go"
106 gb: "Go"
107 tb: "To"
107 tb: "To"
108
108
109 support:
109 support:
110 array:
110 array:
111 sentence_connector: 'et'
111 sentence_connector: 'et'
112 skip_last_comma: true
112 skip_last_comma: true
113 word_connector: ", "
113 word_connector: ", "
114 two_words_connector: " et "
114 two_words_connector: " et "
115 last_word_connector: " et "
115 last_word_connector: " et "
116
116
117 activerecord:
117 activerecord:
118 errors:
118 errors:
119 template:
119 template:
120 header:
120 header:
121 one: "Impossible d'enregistrer %{model} : une erreur"
121 one: "Impossible d'enregistrer %{model} : une erreur"
122 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
122 other: "Impossible d'enregistrer %{model} : %{count} erreurs."
123 body: "Veuillez vérifier les champs suivants :"
123 body: "Veuillez vérifier les champs suivants :"
124 messages:
124 messages:
125 inclusion: "n'est pas inclus(e) dans la liste"
125 inclusion: "n'est pas inclus(e) dans la liste"
126 exclusion: "n'est pas disponible"
126 exclusion: "n'est pas disponible"
127 invalid: "n'est pas valide"
127 invalid: "n'est pas valide"
128 confirmation: "ne concorde pas avec la confirmation"
128 confirmation: "ne concorde pas avec la confirmation"
129 accepted: "doit être accepté(e)"
129 accepted: "doit être accepté(e)"
130 empty: "doit être renseigné(e)"
130 empty: "doit être renseigné(e)"
131 blank: "doit être renseigné(e)"
131 blank: "doit être renseigné(e)"
132 too_long: "est trop long (pas plus de %{count} caractères)"
132 too_long: "est trop long (pas plus de %{count} caractères)"
133 too_short: "est trop court (au moins %{count} caractères)"
133 too_short: "est trop court (au moins %{count} caractères)"
134 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
134 wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)"
135 taken: "est déjà utilisé"
135 taken: "est déjà utilisé"
136 not_a_number: "n'est pas un nombre"
136 not_a_number: "n'est pas un nombre"
137 not_a_date: "n'est pas une date valide"
137 not_a_date: "n'est pas une date valide"
138 greater_than: "doit être supérieur à %{count}"
138 greater_than: "doit être supérieur à %{count}"
139 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
139 greater_than_or_equal_to: "doit être supérieur ou égal à %{count}"
140 equal_to: "doit être égal à %{count}"
140 equal_to: "doit être égal à %{count}"
141 less_than: "doit être inférieur à %{count}"
141 less_than: "doit être inférieur à %{count}"
142 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
142 less_than_or_equal_to: "doit être inférieur ou égal à %{count}"
143 odd: "doit être impair"
143 odd: "doit être impair"
144 even: "doit être pair"
144 even: "doit être pair"
145 greater_than_start_date: "doit être postérieure à la date de début"
145 greater_than_start_date: "doit être postérieure à la date de début"
146 not_same_project: "n'appartient pas au même projet"
146 not_same_project: "n'appartient pas au même projet"
147 circular_dependency: "Cette relation créerait une dépendance circulaire"
147 circular_dependency: "Cette relation créerait une dépendance circulaire"
148 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
148 cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches"
149 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précédent"
149 earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précédent"
150
150
151 actionview_instancetag_blank_option: Choisir
151 actionview_instancetag_blank_option: Choisir
152
152
153 general_text_No: 'Non'
153 general_text_No: 'Non'
154 general_text_Yes: 'Oui'
154 general_text_Yes: 'Oui'
155 general_text_no: 'non'
155 general_text_no: 'non'
156 general_text_yes: 'oui'
156 general_text_yes: 'oui'
157 general_lang_name: 'Français'
157 general_lang_name: 'Français'
158 general_csv_separator: ';'
158 general_csv_separator: ';'
159 general_csv_decimal_separator: ','
159 general_csv_decimal_separator: ','
160 general_csv_encoding: ISO-8859-1
160 general_csv_encoding: ISO-8859-1
161 general_pdf_encoding: UTF-8
161 general_pdf_encoding: UTF-8
162 general_first_day_of_week: '1'
162 general_first_day_of_week: '1'
163
163
164 notice_account_updated: Le compte a été mis à jour avec succès.
164 notice_account_updated: Le compte a été mis à jour avec succès.
165 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
165 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
166 notice_account_password_updated: Mot de passe mis à jour avec succès.
166 notice_account_password_updated: Mot de passe mis à jour avec succès.
167 notice_account_wrong_password: Mot de passe incorrect
167 notice_account_wrong_password: Mot de passe incorrect
168 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
168 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}.
169 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
169 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
170 notice_account_not_activated_yet: Vous n'avez pas encore activé votre compte. Si vous voulez recevoir un nouveau message d'activation, veuillez <a href="%{url}">cliquer sur ce lien</a>.
170 notice_account_not_activated_yet: Vous n'avez pas encore activé votre compte. Si vous voulez recevoir un nouveau message d'activation, veuillez <a href="%{url}">cliquer sur ce lien</a>.
171 notice_account_locked: Votre compte est verrouillé.
171 notice_account_locked: Votre compte est verrouillé.
172 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
172 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
173 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
173 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
174 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
174 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
175 notice_successful_create: Création effectuée avec succès.
175 notice_successful_create: Création effectuée avec succès.
176 notice_successful_update: Mise à jour effectuée avec succès.
176 notice_successful_update: Mise à jour effectuée avec succès.
177 notice_successful_delete: Suppression effectuée avec succès.
177 notice_successful_delete: Suppression effectuée avec succès.
178 notice_successful_connection: Connexion réussie.
178 notice_successful_connection: Connexion réussie.
179 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
179 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
180 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
180 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
181 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
181 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
182 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
182 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
183 notice_email_sent: "Un email a été envoyé à %{value}"
183 notice_email_sent: "Un email a été envoyé à %{value}"
184 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
184 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
185 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
185 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée."
186 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
186 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
187 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
187 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
188 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
188 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
189 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
189 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
190 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
190 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
191 notice_unable_delete_version: Impossible de supprimer cette version.
191 notice_unable_delete_version: Impossible de supprimer cette version.
192 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
192 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
193 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
193 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
194 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
194 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
195 notice_issue_successful_create: "Demande %{id} créée."
195 notice_issue_successful_create: "Demande %{id} créée."
196 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
196 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
197 notice_account_deleted: "Votre compte a été définitivement supprimé."
197 notice_account_deleted: "Votre compte a été définitivement supprimé."
198 notice_user_successful_create: "Utilisateur %{id} créé."
198 notice_user_successful_create: "Utilisateur %{id} créé."
199
199
200 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
200 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
201 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
201 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
202 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
202 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
203 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
203 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
204 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
204 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
205 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
205 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
206 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
206 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
207 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
207 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
208 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
208 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
209 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
209 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
210 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
210 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
211 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
211 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
212
212
213 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
213 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
214
214
215 mail_subject_lost_password: "Votre mot de passe %{value}"
215 mail_subject_lost_password: "Votre mot de passe %{value}"
216 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
216 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
217 mail_subject_register: "Activation de votre compte %{value}"
217 mail_subject_register: "Activation de votre compte %{value}"
218 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
218 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
219 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
219 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
220 mail_body_account_information: Paramètres de connexion de votre compte
220 mail_body_account_information: Paramètres de connexion de votre compte
221 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
221 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
222 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
222 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
223 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
223 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
224 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
224 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
225 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
225 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
226 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
226 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
227 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
227 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
228 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
228 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
229
229
230
230
231 field_name: Nom
231 field_name: Nom
232 field_description: Description
232 field_description: Description
233 field_summary: Résumé
233 field_summary: Résumé
234 field_is_required: Obligatoire
234 field_is_required: Obligatoire
235 field_firstname: Prénom
235 field_firstname: Prénom
236 field_lastname: Nom
236 field_lastname: Nom
237 field_mail: "Email "
237 field_mail: "Email "
238 field_filename: Fichier
238 field_filename: Fichier
239 field_filesize: Taille
239 field_filesize: Taille
240 field_downloads: Téléchargements
240 field_downloads: Téléchargements
241 field_author: Auteur
241 field_author: Auteur
242 field_created_on: "Créé "
242 field_created_on: "Créé "
243 field_updated_on: "Mis-à-jour "
243 field_updated_on: "Mis-à-jour "
244 field_closed_on: Fermé
244 field_closed_on: Fermé
245 field_field_format: Format
245 field_field_format: Format
246 field_is_for_all: Pour tous les projets
246 field_is_for_all: Pour tous les projets
247 field_possible_values: Valeurs possibles
247 field_possible_values: Valeurs possibles
248 field_regexp: Expression régulière
248 field_regexp: Expression régulière
249 field_min_length: Longueur minimum
249 field_min_length: Longueur minimum
250 field_max_length: Longueur maximum
250 field_max_length: Longueur maximum
251 field_value: Valeur
251 field_value: Valeur
252 field_category: Catégorie
252 field_category: Catégorie
253 field_title: Titre
253 field_title: Titre
254 field_project: Projet
254 field_project: Projet
255 field_issue: Demande
255 field_issue: Demande
256 field_status: Statut
256 field_status: Statut
257 field_notes: Notes
257 field_notes: Notes
258 field_is_closed: Demande fermée
258 field_is_closed: Demande fermée
259 field_is_default: Valeur par défaut
259 field_is_default: Valeur par défaut
260 field_tracker: Tracker
260 field_tracker: Tracker
261 field_subject: Sujet
261 field_subject: Sujet
262 field_due_date: Echéance
262 field_due_date: Echéance
263 field_assigned_to: Assigné à
263 field_assigned_to: Assigné à
264 field_priority: Priorité
264 field_priority: Priorité
265 field_fixed_version: Version cible
265 field_fixed_version: Version cible
266 field_user: Utilisateur
266 field_user: Utilisateur
267 field_role: Rôle
267 field_role: Rôle
268 field_homepage: "Site web "
268 field_homepage: "Site web "
269 field_is_public: Public
269 field_is_public: Public
270 field_parent: Sous-projet de
270 field_parent: Sous-projet de
271 field_is_in_roadmap: Demandes affichées dans la roadmap
271 field_is_in_roadmap: Demandes affichées dans la roadmap
272 field_login: "Identifiant "
272 field_login: "Identifiant "
273 field_mail_notification: Notifications par mail
273 field_mail_notification: Notifications par mail
274 field_admin: Administrateur
274 field_admin: Administrateur
275 field_last_login_on: "Dernière connexion "
275 field_last_login_on: "Dernière connexion "
276 field_language: Langue
276 field_language: Langue
277 field_effective_date: Date
277 field_effective_date: Date
278 field_password: Mot de passe
278 field_password: Mot de passe
279 field_new_password: Nouveau mot de passe
279 field_new_password: Nouveau mot de passe
280 field_password_confirmation: Confirmation
280 field_password_confirmation: Confirmation
281 field_version: Version
281 field_version: Version
282 field_type: Type
282 field_type: Type
283 field_host: Hôte
283 field_host: Hôte
284 field_port: Port
284 field_port: Port
285 field_account: Compte
285 field_account: Compte
286 field_base_dn: Base DN
286 field_base_dn: Base DN
287 field_attr_login: Attribut Identifiant
287 field_attr_login: Attribut Identifiant
288 field_attr_firstname: Attribut Prénom
288 field_attr_firstname: Attribut Prénom
289 field_attr_lastname: Attribut Nom
289 field_attr_lastname: Attribut Nom
290 field_attr_mail: Attribut Email
290 field_attr_mail: Attribut Email
291 field_onthefly: Création des utilisateurs à la volée
291 field_onthefly: Création des utilisateurs à la volée
292 field_start_date: Début
292 field_start_date: Début
293 field_done_ratio: "% réalisé"
293 field_done_ratio: "% réalisé"
294 field_auth_source: Mode d'authentification
294 field_auth_source: Mode d'authentification
295 field_hide_mail: Cacher mon adresse mail
295 field_hide_mail: Cacher mon adresse mail
296 field_comments: Commentaire
296 field_comments: Commentaire
297 field_url: URL
297 field_url: URL
298 field_start_page: Page de démarrage
298 field_start_page: Page de démarrage
299 field_subproject: Sous-projet
299 field_subproject: Sous-projet
300 field_hours: Heures
300 field_hours: Heures
301 field_activity: Activité
301 field_activity: Activité
302 field_spent_on: Date
302 field_spent_on: Date
303 field_identifier: Identifiant
303 field_identifier: Identifiant
304 field_is_filter: Utilisé comme filtre
304 field_is_filter: Utilisé comme filtre
305 field_issue_to: Demande liée
305 field_issue_to: Demande liée
306 field_delay: Retard
306 field_delay: Retard
307 field_assignable: Demandes assignables à ce rôle
307 field_assignable: Demandes assignables à ce rôle
308 field_redirect_existing_links: Rediriger les liens existants
308 field_redirect_existing_links: Rediriger les liens existants
309 field_estimated_hours: Temps estimé
309 field_estimated_hours: Temps estimé
310 field_column_names: Colonnes
310 field_column_names: Colonnes
311 field_time_zone: Fuseau horaire
311 field_time_zone: Fuseau horaire
312 field_searchable: Utilisé pour les recherches
312 field_searchable: Utilisé pour les recherches
313 field_default_value: Valeur par défaut
313 field_default_value: Valeur par défaut
314 field_comments_sorting: Afficher les commentaires
314 field_comments_sorting: Afficher les commentaires
315 field_parent_title: Page parent
315 field_parent_title: Page parent
316 field_editable: Modifiable
316 field_editable: Modifiable
317 field_watcher: Observateur
317 field_watcher: Observateur
318 field_identity_url: URL OpenID
318 field_identity_url: URL OpenID
319 field_content: Contenu
319 field_content: Contenu
320 field_group_by: Grouper par
320 field_group_by: Grouper par
321 field_sharing: Partage
321 field_sharing: Partage
322 field_active: Actif
322 field_active: Actif
323 field_parent_issue: Tâche parente
323 field_parent_issue: Tâche parente
324 field_visible: Visible
324 field_visible: Visible
325 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
325 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
326 field_issues_visibility: Visibilité des demandes
326 field_issues_visibility: Visibilité des demandes
327 field_is_private: Privée
327 field_is_private: Privée
328 field_commit_logs_encoding: Encodage des messages de commit
328 field_commit_logs_encoding: Encodage des messages de commit
329 field_repository_is_default: Dépôt principal
329 field_repository_is_default: Dépôt principal
330 field_multiple: Valeurs multiples
330 field_multiple: Valeurs multiples
331 field_auth_source_ldap_filter: Filtre LDAP
331 field_auth_source_ldap_filter: Filtre LDAP
332 field_core_fields: Champs standards
332 field_core_fields: Champs standards
333 field_timeout: "Timeout (en secondes)"
333 field_timeout: "Timeout (en secondes)"
334 field_board_parent: Forum parent
334 field_board_parent: Forum parent
335 field_private_notes: Notes privées
335 field_private_notes: Notes privées
336 field_inherit_members: Hériter les membres
336 field_inherit_members: Hériter les membres
337 field_generate_password: Générer un mot de passe
337 field_generate_password: Générer un mot de passe
338
338
339 setting_app_title: Titre de l'application
339 setting_app_title: Titre de l'application
340 setting_app_subtitle: Sous-titre de l'application
340 setting_app_subtitle: Sous-titre de l'application
341 setting_welcome_text: Texte d'accueil
341 setting_welcome_text: Texte d'accueil
342 setting_default_language: Langue par défaut
342 setting_default_language: Langue par défaut
343 setting_login_required: Authentification obligatoire
343 setting_login_required: Authentification obligatoire
344 setting_self_registration: Inscription des nouveaux utilisateurs
344 setting_self_registration: Inscription des nouveaux utilisateurs
345 setting_attachment_max_size: Taille maximale des fichiers
345 setting_attachment_max_size: Taille maximale des fichiers
346 setting_issues_export_limit: Limite d'exportation des demandes
346 setting_issues_export_limit: Limite d'exportation des demandes
347 setting_mail_from: Adresse d'émission
347 setting_mail_from: Adresse d'émission
348 setting_bcc_recipients: Destinataires en copie cachée (cci)
348 setting_bcc_recipients: Destinataires en copie cachée (cci)
349 setting_plain_text_mail: Mail en texte brut (non HTML)
349 setting_plain_text_mail: Mail en texte brut (non HTML)
350 setting_host_name: Nom d'hôte et chemin
350 setting_host_name: Nom d'hôte et chemin
351 setting_text_formatting: Formatage du texte
351 setting_text_formatting: Formatage du texte
352 setting_wiki_compression: Compression de l'historique des pages wiki
352 setting_wiki_compression: Compression de l'historique des pages wiki
353 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
353 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
354 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
354 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
355 setting_autofetch_changesets: Récupération automatique des commits
355 setting_autofetch_changesets: Récupération automatique des commits
356 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
356 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
357 setting_commit_ref_keywords: Mots-clés de référencement
357 setting_commit_ref_keywords: Mots-clés de référencement
358 setting_commit_fix_keywords: Mots-clés de résolution
358 setting_commit_fix_keywords: Mots-clés de résolution
359 setting_autologin: Durée maximale de connexion automatique
359 setting_autologin: Durée maximale de connexion automatique
360 setting_date_format: Format de date
360 setting_date_format: Format de date
361 setting_time_format: Format d'heure
361 setting_time_format: Format d'heure
362 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
362 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
363 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
363 setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents
364 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
364 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
365 setting_emails_footer: Pied-de-page des emails
365 setting_emails_footer: Pied-de-page des emails
366 setting_protocol: Protocole
366 setting_protocol: Protocole
367 setting_per_page_options: Options d'objets affichés par page
367 setting_per_page_options: Options d'objets affichés par page
368 setting_user_format: Format d'affichage des utilisateurs
368 setting_user_format: Format d'affichage des utilisateurs
369 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
369 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
370 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
370 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
371 setting_enabled_scm: SCM activés
371 setting_enabled_scm: SCM activés
372 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
372 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
373 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
373 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
374 setting_mail_handler_api_key: Clé de protection de l'API
374 setting_mail_handler_api_key: Clé de protection de l'API
375 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
375 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
376 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
376 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
377 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
377 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
378 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
378 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
379 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
379 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
380 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
380 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
381 setting_password_min_length: Longueur minimum des mots de passe
381 setting_password_min_length: Longueur minimum des mots de passe
382 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
382 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
383 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
383 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
384 setting_issue_done_ratio: Calcul de l'avancement des demandes
384 setting_issue_done_ratio: Calcul de l'avancement des demandes
385 setting_issue_done_ratio_issue_status: Utiliser le statut
385 setting_issue_done_ratio_issue_status: Utiliser le statut
386 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
386 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
387 setting_rest_api_enabled: Activer l'API REST
387 setting_rest_api_enabled: Activer l'API REST
388 setting_gravatar_default: Image Gravatar par défaut
388 setting_gravatar_default: Image Gravatar par défaut
389 setting_start_of_week: Jour de début des calendriers
389 setting_start_of_week: Jour de début des calendriers
390 setting_cache_formatted_text: Mettre en cache le texte formaté
390 setting_cache_formatted_text: Mettre en cache le texte formaté
391 setting_commit_logtime_enabled: Permettre la saisie de temps
391 setting_commit_logtime_enabled: Permettre la saisie de temps
392 setting_commit_logtime_activity_id: Activité pour le temps saisi
392 setting_commit_logtime_activity_id: Activité pour le temps saisi
393 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
393 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
394 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
394 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
395 setting_default_issue_start_date_to_creation_date: Donner à la date de début d'une nouvelle demande la valeur de la date du jour
395 setting_default_issue_start_date_to_creation_date: Donner à la date de début d'une nouvelle demande la valeur de la date du jour
396 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
396 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
397 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
397 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
398 setting_session_lifetime: Durée de vie maximale des sessions
398 setting_session_lifetime: Durée de vie maximale des sessions
399 setting_session_timeout: Durée maximale d'inactivité
399 setting_session_timeout: Durée maximale d'inactivité
400 setting_thumbnails_enabled: Afficher les vignettes des images
400 setting_thumbnails_enabled: Afficher les vignettes des images
401 setting_thumbnails_size: Taille des vignettes (en pixels)
401 setting_thumbnails_size: Taille des vignettes (en pixels)
402 setting_non_working_week_days: Jours non travaillés
402 setting_non_working_week_days: Jours non travaillés
403 setting_jsonp_enabled: Activer le support JSONP
403 setting_jsonp_enabled: Activer le support JSONP
404 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
404 setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets
405
405
406 permission_add_project: Créer un projet
406 permission_add_project: Créer un projet
407 permission_add_subprojects: Créer des sous-projets
407 permission_add_subprojects: Créer des sous-projets
408 permission_edit_project: Modifier le projet
408 permission_edit_project: Modifier le projet
409 permission_close_project: Fermer / réouvrir le projet
409 permission_close_project: Fermer / réouvrir le projet
410 permission_select_project_modules: Choisir les modules
410 permission_select_project_modules: Choisir les modules
411 permission_manage_members: Gérer les membres
411 permission_manage_members: Gérer les membres
412 permission_manage_versions: Gérer les versions
412 permission_manage_versions: Gérer les versions
413 permission_manage_categories: Gérer les catégories de demandes
413 permission_manage_categories: Gérer les catégories de demandes
414 permission_view_issues: Voir les demandes
414 permission_view_issues: Voir les demandes
415 permission_add_issues: Créer des demandes
415 permission_add_issues: Créer des demandes
416 permission_edit_issues: Modifier les demandes
416 permission_edit_issues: Modifier les demandes
417 permission_manage_issue_relations: Gérer les relations
417 permission_manage_issue_relations: Gérer les relations
418 permission_set_issues_private: Rendre les demandes publiques ou privées
418 permission_set_issues_private: Rendre les demandes publiques ou privées
419 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
419 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
420 permission_add_issue_notes: Ajouter des notes
420 permission_add_issue_notes: Ajouter des notes
421 permission_edit_issue_notes: Modifier les notes
421 permission_edit_issue_notes: Modifier les notes
422 permission_edit_own_issue_notes: Modifier ses propres notes
422 permission_edit_own_issue_notes: Modifier ses propres notes
423 permission_view_private_notes: Voir les notes privées
423 permission_view_private_notes: Voir les notes privées
424 permission_set_notes_private: Rendre les notes privées
424 permission_set_notes_private: Rendre les notes privées
425 permission_move_issues: Déplacer les demandes
425 permission_move_issues: Déplacer les demandes
426 permission_delete_issues: Supprimer les demandes
426 permission_delete_issues: Supprimer les demandes
427 permission_manage_public_queries: Gérer les requêtes publiques
427 permission_manage_public_queries: Gérer les requêtes publiques
428 permission_save_queries: Sauvegarder les requêtes
428 permission_save_queries: Sauvegarder les requêtes
429 permission_view_gantt: Voir le gantt
429 permission_view_gantt: Voir le gantt
430 permission_view_calendar: Voir le calendrier
430 permission_view_calendar: Voir le calendrier
431 permission_view_issue_watchers: Voir la liste des observateurs
431 permission_view_issue_watchers: Voir la liste des observateurs
432 permission_add_issue_watchers: Ajouter des observateurs
432 permission_add_issue_watchers: Ajouter des observateurs
433 permission_delete_issue_watchers: Supprimer des observateurs
433 permission_delete_issue_watchers: Supprimer des observateurs
434 permission_log_time: Saisir le temps passé
434 permission_log_time: Saisir le temps passé
435 permission_view_time_entries: Voir le temps passé
435 permission_view_time_entries: Voir le temps passé
436 permission_edit_time_entries: Modifier les temps passés
436 permission_edit_time_entries: Modifier les temps passés
437 permission_edit_own_time_entries: Modifier son propre temps passé
437 permission_edit_own_time_entries: Modifier son propre temps passé
438 permission_manage_news: Gérer les annonces
438 permission_manage_news: Gérer les annonces
439 permission_comment_news: Commenter les annonces
439 permission_comment_news: Commenter les annonces
440 permission_view_documents: Voir les documents
440 permission_view_documents: Voir les documents
441 permission_add_documents: Ajouter des documents
441 permission_add_documents: Ajouter des documents
442 permission_edit_documents: Modifier les documents
442 permission_edit_documents: Modifier les documents
443 permission_delete_documents: Supprimer les documents
443 permission_delete_documents: Supprimer les documents
444 permission_manage_files: Gérer les fichiers
444 permission_manage_files: Gérer les fichiers
445 permission_view_files: Voir les fichiers
445 permission_view_files: Voir les fichiers
446 permission_manage_wiki: Gérer le wiki
446 permission_manage_wiki: Gérer le wiki
447 permission_rename_wiki_pages: Renommer les pages
447 permission_rename_wiki_pages: Renommer les pages
448 permission_delete_wiki_pages: Supprimer les pages
448 permission_delete_wiki_pages: Supprimer les pages
449 permission_view_wiki_pages: Voir le wiki
449 permission_view_wiki_pages: Voir le wiki
450 permission_view_wiki_edits: "Voir l'historique des modifications"
450 permission_view_wiki_edits: "Voir l'historique des modifications"
451 permission_edit_wiki_pages: Modifier les pages
451 permission_edit_wiki_pages: Modifier les pages
452 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
452 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
453 permission_protect_wiki_pages: Protéger les pages
453 permission_protect_wiki_pages: Protéger les pages
454 permission_manage_repository: Gérer le dépôt de sources
454 permission_manage_repository: Gérer le dépôt de sources
455 permission_browse_repository: Parcourir les sources
455 permission_browse_repository: Parcourir les sources
456 permission_view_changesets: Voir les révisions
456 permission_view_changesets: Voir les révisions
457 permission_commit_access: Droit de commit
457 permission_commit_access: Droit de commit
458 permission_manage_boards: Gérer les forums
458 permission_manage_boards: Gérer les forums
459 permission_view_messages: Voir les messages
459 permission_view_messages: Voir les messages
460 permission_add_messages: Poster un message
460 permission_add_messages: Poster un message
461 permission_edit_messages: Modifier les messages
461 permission_edit_messages: Modifier les messages
462 permission_edit_own_messages: Modifier ses propres messages
462 permission_edit_own_messages: Modifier ses propres messages
463 permission_delete_messages: Supprimer les messages
463 permission_delete_messages: Supprimer les messages
464 permission_delete_own_messages: Supprimer ses propres messages
464 permission_delete_own_messages: Supprimer ses propres messages
465 permission_export_wiki_pages: Exporter les pages
465 permission_export_wiki_pages: Exporter les pages
466 permission_manage_project_activities: Gérer les activités
466 permission_manage_project_activities: Gérer les activités
467 permission_manage_subtasks: Gérer les sous-tâches
467 permission_manage_subtasks: Gérer les sous-tâches
468 permission_manage_related_issues: Gérer les demandes associées
468 permission_manage_related_issues: Gérer les demandes associées
469
469
470 project_module_issue_tracking: Suivi des demandes
470 project_module_issue_tracking: Suivi des demandes
471 project_module_time_tracking: Suivi du temps passé
471 project_module_time_tracking: Suivi du temps passé
472 project_module_news: Publication d'annonces
472 project_module_news: Publication d'annonces
473 project_module_documents: Publication de documents
473 project_module_documents: Publication de documents
474 project_module_files: Publication de fichiers
474 project_module_files: Publication de fichiers
475 project_module_wiki: Wiki
475 project_module_wiki: Wiki
476 project_module_repository: Dépôt de sources
476 project_module_repository: Dépôt de sources
477 project_module_boards: Forums de discussion
477 project_module_boards: Forums de discussion
478
478
479 label_user: Utilisateur
479 label_user: Utilisateur
480 label_user_plural: Utilisateurs
480 label_user_plural: Utilisateurs
481 label_user_new: Nouvel utilisateur
481 label_user_new: Nouvel utilisateur
482 label_user_anonymous: Anonyme
482 label_user_anonymous: Anonyme
483 label_project: Projet
483 label_project: Projet
484 label_project_new: Nouveau projet
484 label_project_new: Nouveau projet
485 label_project_plural: Projets
485 label_project_plural: Projets
486 label_x_projects:
486 label_x_projects:
487 zero: aucun projet
487 zero: aucun projet
488 one: un projet
488 one: un projet
489 other: "%{count} projets"
489 other: "%{count} projets"
490 label_project_all: Tous les projets
490 label_project_all: Tous les projets
491 label_project_latest: Derniers projets
491 label_project_latest: Derniers projets
492 label_issue: Demande
492 label_issue: Demande
493 label_issue_new: Nouvelle demande
493 label_issue_new: Nouvelle demande
494 label_issue_plural: Demandes
494 label_issue_plural: Demandes
495 label_issue_view_all: Voir toutes les demandes
495 label_issue_view_all: Voir toutes les demandes
496 label_issue_added: Demande ajoutée
496 label_issue_added: Demande ajoutée
497 label_issue_updated: Demande mise à jour
497 label_issue_updated: Demande mise à jour
498 label_issue_note_added: Note ajoutée
498 label_issue_note_added: Note ajoutée
499 label_issue_status_updated: Statut changé
499 label_issue_status_updated: Statut changé
500 label_issue_priority_updated: Priorité changée
500 label_issue_priority_updated: Priorité changée
501 label_issues_by: "Demandes par %{value}"
501 label_issues_by: "Demandes par %{value}"
502 label_document: Document
502 label_document: Document
503 label_document_new: Nouveau document
503 label_document_new: Nouveau document
504 label_document_plural: Documents
504 label_document_plural: Documents
505 label_document_added: Document ajouté
505 label_document_added: Document ajouté
506 label_role: Rôle
506 label_role: Rôle
507 label_role_plural: Rôles
507 label_role_plural: Rôles
508 label_role_new: Nouveau rôle
508 label_role_new: Nouveau rôle
509 label_role_and_permissions: Rôles et permissions
509 label_role_and_permissions: Rôles et permissions
510 label_role_anonymous: Anonyme
510 label_role_anonymous: Anonyme
511 label_role_non_member: Non membre
511 label_role_non_member: Non membre
512 label_member: Membre
512 label_member: Membre
513 label_member_new: Nouveau membre
513 label_member_new: Nouveau membre
514 label_member_plural: Membres
514 label_member_plural: Membres
515 label_tracker: Tracker
515 label_tracker: Tracker
516 label_tracker_plural: Trackers
516 label_tracker_plural: Trackers
517 label_tracker_new: Nouveau tracker
517 label_tracker_new: Nouveau tracker
518 label_workflow: Workflow
518 label_workflow: Workflow
519 label_issue_status: Statut de demandes
519 label_issue_status: Statut de demandes
520 label_issue_status_plural: Statuts de demandes
520 label_issue_status_plural: Statuts de demandes
521 label_issue_status_new: Nouveau statut
521 label_issue_status_new: Nouveau statut
522 label_issue_category: Catégorie de demandes
522 label_issue_category: Catégorie de demandes
523 label_issue_category_plural: Catégories de demandes
523 label_issue_category_plural: Catégories de demandes
524 label_issue_category_new: Nouvelle catégorie
524 label_issue_category_new: Nouvelle catégorie
525 label_custom_field: Champ personnalisé
525 label_custom_field: Champ personnalisé
526 label_custom_field_plural: Champs personnalisés
526 label_custom_field_plural: Champs personnalisés
527 label_custom_field_new: Nouveau champ personnalisé
527 label_custom_field_new: Nouveau champ personnalisé
528 label_enumerations: Listes de valeurs
528 label_enumerations: Listes de valeurs
529 label_enumeration_new: Nouvelle valeur
529 label_enumeration_new: Nouvelle valeur
530 label_information: Information
530 label_information: Information
531 label_information_plural: Informations
531 label_information_plural: Informations
532 label_please_login: Identification
532 label_please_login: Identification
533 label_register: S'enregistrer
533 label_register: S'enregistrer
534 label_login_with_open_id_option: S'authentifier avec OpenID
534 label_login_with_open_id_option: S'authentifier avec OpenID
535 label_password_lost: Mot de passe perdu
535 label_password_lost: Mot de passe perdu
536 label_home: Accueil
536 label_home: Accueil
537 label_my_page: Ma page
537 label_my_page: Ma page
538 label_my_account: Mon compte
538 label_my_account: Mon compte
539 label_my_projects: Mes projets
539 label_my_projects: Mes projets
540 label_my_page_block: Blocs disponibles
540 label_my_page_block: Blocs disponibles
541 label_administration: Administration
541 label_administration: Administration
542 label_login: Connexion
542 label_login: Connexion
543 label_logout: Déconnexion
543 label_logout: Déconnexion
544 label_help: Aide
544 label_help: Aide
545 label_reported_issues: "Demandes soumises "
545 label_reported_issues: "Demandes soumises "
546 label_assigned_to_me_issues: Demandes qui me sont assignées
546 label_assigned_to_me_issues: Demandes qui me sont assignées
547 label_last_login: "Dernière connexion "
547 label_last_login: "Dernière connexion "
548 label_registered_on: "Inscrit le "
548 label_registered_on: "Inscrit le "
549 label_activity: Activité
549 label_activity: Activité
550 label_overall_activity: Activité globale
550 label_overall_activity: Activité globale
551 label_user_activity: "Activité de %{value}"
551 label_user_activity: "Activité de %{value}"
552 label_new: Nouveau
552 label_new: Nouveau
553 label_logged_as: Connecté en tant que
553 label_logged_as: Connecté en tant que
554 label_environment: Environnement
554 label_environment: Environnement
555 label_authentication: Authentification
555 label_authentication: Authentification
556 label_auth_source: Mode d'authentification
556 label_auth_source: Mode d'authentification
557 label_auth_source_new: Nouveau mode d'authentification
557 label_auth_source_new: Nouveau mode d'authentification
558 label_auth_source_plural: Modes d'authentification
558 label_auth_source_plural: Modes d'authentification
559 label_subproject_plural: Sous-projets
559 label_subproject_plural: Sous-projets
560 label_subproject_new: Nouveau sous-projet
560 label_subproject_new: Nouveau sous-projet
561 label_and_its_subprojects: "%{value} et ses sous-projets"
561 label_and_its_subprojects: "%{value} et ses sous-projets"
562 label_min_max_length: Longueurs mini - maxi
562 label_min_max_length: Longueurs mini - maxi
563 label_list: Liste
563 label_list: Liste
564 label_date: Date
564 label_date: Date
565 label_integer: Entier
565 label_integer: Entier
566 label_float: Nombre décimal
566 label_float: Nombre décimal
567 label_boolean: Booléen
567 label_boolean: Booléen
568 label_string: Texte
568 label_string: Texte
569 label_text: Texte long
569 label_text: Texte long
570 label_attribute: Attribut
570 label_attribute: Attribut
571 label_attribute_plural: Attributs
571 label_attribute_plural: Attributs
572 label_no_data: Aucune donnée à afficher
572 label_no_data: Aucune donnée à afficher
573 label_change_status: Changer le statut
573 label_change_status: Changer le statut
574 label_history: Historique
574 label_history: Historique
575 label_attachment: Fichier
575 label_attachment: Fichier
576 label_attachment_new: Nouveau fichier
576 label_attachment_new: Nouveau fichier
577 label_attachment_delete: Supprimer le fichier
577 label_attachment_delete: Supprimer le fichier
578 label_attachment_plural: Fichiers
578 label_attachment_plural: Fichiers
579 label_file_added: Fichier ajouté
579 label_file_added: Fichier ajouté
580 label_report: Rapport
580 label_report: Rapport
581 label_report_plural: Rapports
581 label_report_plural: Rapports
582 label_news: Annonce
582 label_news: Annonce
583 label_news_new: Nouvelle annonce
583 label_news_new: Nouvelle annonce
584 label_news_plural: Annonces
584 label_news_plural: Annonces
585 label_news_latest: Dernières annonces
585 label_news_latest: Dernières annonces
586 label_news_view_all: Voir toutes les annonces
586 label_news_view_all: Voir toutes les annonces
587 label_news_added: Annonce ajoutée
587 label_news_added: Annonce ajoutée
588 label_news_comment_added: Commentaire ajouté à une annonce
588 label_news_comment_added: Commentaire ajouté à une annonce
589 label_settings: Configuration
589 label_settings: Configuration
590 label_overview: Aperçu
590 label_overview: Aperçu
591 label_version: Version
591 label_version: Version
592 label_version_new: Nouvelle version
592 label_version_new: Nouvelle version
593 label_version_plural: Versions
593 label_version_plural: Versions
594 label_confirmation: Confirmation
594 label_confirmation: Confirmation
595 label_export_to: 'Formats disponibles :'
595 label_export_to: 'Formats disponibles :'
596 label_read: Lire...
596 label_read: Lire...
597 label_public_projects: Projets publics
597 label_public_projects: Projets publics
598 label_open_issues: ouvert
598 label_open_issues: ouvert
599 label_open_issues_plural: ouverts
599 label_open_issues_plural: ouverts
600 label_closed_issues: fermé
600 label_closed_issues: fermé
601 label_closed_issues_plural: fermés
601 label_closed_issues_plural: fermés
602 label_x_open_issues_abbr_on_total:
602 label_x_open_issues_abbr_on_total:
603 zero: 0 ouverte sur %{total}
603 zero: 0 ouverte sur %{total}
604 one: 1 ouverte sur %{total}
604 one: 1 ouverte sur %{total}
605 other: "%{count} ouvertes sur %{total}"
605 other: "%{count} ouvertes sur %{total}"
606 label_x_open_issues_abbr:
606 label_x_open_issues_abbr:
607 zero: 0 ouverte
607 zero: 0 ouverte
608 one: 1 ouverte
608 one: 1 ouverte
609 other: "%{count} ouvertes"
609 other: "%{count} ouvertes"
610 label_x_closed_issues_abbr:
610 label_x_closed_issues_abbr:
611 zero: 0 fermée
611 zero: 0 fermée
612 one: 1 fermée
612 one: 1 fermée
613 other: "%{count} fermées"
613 other: "%{count} fermées"
614 label_x_issues:
614 label_x_issues:
615 zero: 0 demande
615 zero: 0 demande
616 one: 1 demande
616 one: 1 demande
617 other: "%{count} demandes"
617 other: "%{count} demandes"
618 label_total: Total
618 label_total: Total
619 label_total_time: Temps total
619 label_total_time: Temps total
620 label_permissions: Permissions
620 label_permissions: Permissions
621 label_current_status: Statut actuel
621 label_current_status: Statut actuel
622 label_new_statuses_allowed: Nouveaux statuts autorisés
622 label_new_statuses_allowed: Nouveaux statuts autorisés
623 label_all: tous
623 label_all: tous
624 label_any: tous
624 label_any: tous
625 label_none: aucun
625 label_none: aucun
626 label_nobody: personne
626 label_nobody: personne
627 label_next: Suivant
627 label_next: Suivant
628 label_previous: Précédent
628 label_previous: Précédent
629 label_used_by: Utilisé par
629 label_used_by: Utilisé par
630 label_details: Détails
630 label_details: Détails
631 label_add_note: Ajouter une note
631 label_add_note: Ajouter une note
632 label_per_page: Par page
632 label_per_page: Par page
633 label_calendar: Calendrier
633 label_calendar: Calendrier
634 label_months_from: mois depuis
634 label_months_from: mois depuis
635 label_gantt: Gantt
635 label_gantt: Gantt
636 label_internal: Interne
636 label_internal: Interne
637 label_last_changes: "%{count} derniers changements"
637 label_last_changes: "%{count} derniers changements"
638 label_change_view_all: Voir tous les changements
638 label_change_view_all: Voir tous les changements
639 label_personalize_page: Personnaliser cette page
639 label_personalize_page: Personnaliser cette page
640 label_comment: Commentaire
640 label_comment: Commentaire
641 label_comment_plural: Commentaires
641 label_comment_plural: Commentaires
642 label_x_comments:
642 label_x_comments:
643 zero: aucun commentaire
643 zero: aucun commentaire
644 one: un commentaire
644 one: un commentaire
645 other: "%{count} commentaires"
645 other: "%{count} commentaires"
646 label_comment_add: Ajouter un commentaire
646 label_comment_add: Ajouter un commentaire
647 label_comment_added: Commentaire ajouté
647 label_comment_added: Commentaire ajouté
648 label_comment_delete: Supprimer les commentaires
648 label_comment_delete: Supprimer les commentaires
649 label_query: Rapport personnalisé
649 label_query: Rapport personnalisé
650 label_query_plural: Rapports personnalisés
650 label_query_plural: Rapports personnalisés
651 label_query_new: Nouveau rapport
651 label_query_new: Nouveau rapport
652 label_my_queries: Mes rapports personnalisés
652 label_my_queries: Mes rapports personnalisés
653 label_filter_add: "Ajouter le filtre "
653 label_filter_add: "Ajouter le filtre "
654 label_filter_plural: Filtres
654 label_filter_plural: Filtres
655 label_equals: égal
655 label_equals: égal
656 label_not_equals: différent
656 label_not_equals: différent
657 label_in_less_than: dans moins de
657 label_in_less_than: dans moins de
658 label_in_more_than: dans plus de
658 label_in_more_than: dans plus de
659 label_in_the_next_days: dans les prochains jours
659 label_in_the_next_days: dans les prochains jours
660 label_in_the_past_days: dans les derniers jours
660 label_in_the_past_days: dans les derniers jours
661 label_in: dans
661 label_in: dans
662 label_today: aujourd'hui
662 label_today: aujourd'hui
663 label_all_time: toute la période
663 label_all_time: toute la période
664 label_yesterday: hier
664 label_yesterday: hier
665 label_this_week: cette semaine
665 label_this_week: cette semaine
666 label_last_week: la semaine dernière
666 label_last_week: la semaine dernière
667 label_last_n_weeks: "les %{count} dernières semaines"
667 label_last_n_weeks: "les %{count} dernières semaines"
668 label_last_n_days: "les %{count} derniers jours"
668 label_last_n_days: "les %{count} derniers jours"
669 label_this_month: ce mois-ci
669 label_this_month: ce mois-ci
670 label_last_month: le mois dernier
670 label_last_month: le mois dernier
671 label_this_year: cette année
671 label_this_year: cette année
672 label_date_range: Période
672 label_date_range: Période
673 label_less_than_ago: il y a moins de
673 label_less_than_ago: il y a moins de
674 label_more_than_ago: il y a plus de
674 label_more_than_ago: il y a plus de
675 label_ago: il y a
675 label_ago: il y a
676 label_contains: contient
676 label_contains: contient
677 label_not_contains: ne contient pas
677 label_not_contains: ne contient pas
678 label_any_issues_in_project: une demande du projet
678 label_any_issues_in_project: une demande du projet
679 label_any_issues_not_in_project: une demande hors du projet
679 label_any_issues_not_in_project: une demande hors du projet
680 label_no_issues_in_project: aucune demande du projet
680 label_no_issues_in_project: aucune demande du projet
681 label_day_plural: jours
681 label_day_plural: jours
682 label_repository: Dépôt
682 label_repository: Dépôt
683 label_repository_new: Nouveau dépôt
683 label_repository_new: Nouveau dépôt
684 label_repository_plural: Dépôts
684 label_repository_plural: Dépôts
685 label_browse: Parcourir
685 label_browse: Parcourir
686 label_revision: "Révision "
686 label_revision: "Révision "
687 label_revision_plural: Révisions
687 label_revision_plural: Révisions
688 label_associated_revisions: Révisions associées
688 label_associated_revisions: Révisions associées
689 label_added: ajouté
689 label_added: ajouté
690 label_modified: modifié
690 label_modified: modifié
691 label_copied: copié
691 label_copied: copié
692 label_renamed: renommé
692 label_renamed: renommé
693 label_deleted: supprimé
693 label_deleted: supprimé
694 label_latest_revision: Dernière révision
694 label_latest_revision: Dernière révision
695 label_latest_revision_plural: Dernières révisions
695 label_latest_revision_plural: Dernières révisions
696 label_view_revisions: Voir les révisions
696 label_view_revisions: Voir les révisions
697 label_max_size: Taille maximale
697 label_max_size: Taille maximale
698 label_sort_highest: Remonter en premier
698 label_sort_highest: Remonter en premier
699 label_sort_higher: Remonter
699 label_sort_higher: Remonter
700 label_sort_lower: Descendre
700 label_sort_lower: Descendre
701 label_sort_lowest: Descendre en dernier
701 label_sort_lowest: Descendre en dernier
702 label_roadmap: Roadmap
702 label_roadmap: Roadmap
703 label_roadmap_due_in: "Échéance dans %{value}"
703 label_roadmap_due_in: "Échéance dans %{value}"
704 label_roadmap_overdue: "En retard de %{value}"
704 label_roadmap_overdue: "En retard de %{value}"
705 label_roadmap_no_issues: Aucune demande pour cette version
705 label_roadmap_no_issues: Aucune demande pour cette version
706 label_search: "Recherche "
706 label_search: "Recherche "
707 label_result_plural: Résultats
707 label_result_plural: Résultats
708 label_all_words: Tous les mots
708 label_all_words: Tous les mots
709 label_wiki: Wiki
709 label_wiki: Wiki
710 label_wiki_edit: Révision wiki
710 label_wiki_edit: Révision wiki
711 label_wiki_edit_plural: Révisions wiki
711 label_wiki_edit_plural: Révisions wiki
712 label_wiki_page: Page wiki
712 label_wiki_page: Page wiki
713 label_wiki_page_plural: Pages wiki
713 label_wiki_page_plural: Pages wiki
714 label_index_by_title: Index par titre
714 label_index_by_title: Index par titre
715 label_index_by_date: Index par date
715 label_index_by_date: Index par date
716 label_current_version: Version actuelle
716 label_current_version: Version actuelle
717 label_preview: Prévisualisation
717 label_preview: Prévisualisation
718 label_feed_plural: Flux Atom
718 label_feed_plural: Flux Atom
719 label_changes_details: Détails de tous les changements
719 label_changes_details: Détails de tous les changements
720 label_issue_tracking: Suivi des demandes
720 label_issue_tracking: Suivi des demandes
721 label_spent_time: Temps passé
721 label_spent_time: Temps passé
722 label_f_hour: "%{value} heure"
722 label_f_hour: "%{value} heure"
723 label_f_hour_plural: "%{value} heures"
723 label_f_hour_plural: "%{value} heures"
724 label_time_tracking: Suivi du temps
724 label_time_tracking: Suivi du temps
725 label_change_plural: Changements
725 label_change_plural: Changements
726 label_statistics: Statistiques
726 label_statistics: Statistiques
727 label_commits_per_month: Commits par mois
727 label_commits_per_month: Commits par mois
728 label_commits_per_author: Commits par auteur
728 label_commits_per_author: Commits par auteur
729 label_view_diff: Voir les différences
729 label_view_diff: Voir les différences
730 label_diff_inline: en ligne
730 label_diff_inline: en ligne
731 label_diff_side_by_side: côte à côte
731 label_diff_side_by_side: côte à côte
732 label_options: Options
732 label_options: Options
733 label_copy_workflow_from: Copier le workflow de
733 label_copy_workflow_from: Copier le workflow de
734 label_permissions_report: Synthèse des permissions
734 label_permissions_report: Synthèse des permissions
735 label_watched_issues: Demandes surveillées
735 label_watched_issues: Demandes surveillées
736 label_related_issues: Demandes liées
736 label_related_issues: Demandes liées
737 label_applied_status: Statut appliqué
737 label_applied_status: Statut appliqué
738 label_loading: Chargement...
738 label_loading: Chargement...
739 label_relation_new: Nouvelle relation
739 label_relation_new: Nouvelle relation
740 label_relation_delete: Supprimer la relation
740 label_relation_delete: Supprimer la relation
741 label_relates_to: Lié à
741 label_relates_to: Lié à
742 label_duplicates: Duplique
742 label_duplicates: Duplique
743 label_duplicated_by: Dupliqué par
743 label_duplicated_by: Dupliqué par
744 label_blocks: Bloque
744 label_blocks: Bloque
745 label_blocked_by: Bloqué par
745 label_blocked_by: Bloqué par
746 label_precedes: Précède
746 label_precedes: Précède
747 label_follows: Suit
747 label_follows: Suit
748 label_copied_to: Copié vers
748 label_copied_to: Copié vers
749 label_copied_from: Copié depuis
749 label_copied_from: Copié depuis
750 label_end_to_start: fin à début
750 label_end_to_start: fin à début
751 label_end_to_end: fin à fin
751 label_end_to_end: fin à fin
752 label_start_to_start: début à début
752 label_start_to_start: début à début
753 label_start_to_end: début à fin
753 label_start_to_end: début à fin
754 label_stay_logged_in: Rester connecté
754 label_stay_logged_in: Rester connecté
755 label_disabled: désactivé
755 label_disabled: désactivé
756 label_show_completed_versions: Voir les versions passées
756 label_show_completed_versions: Voir les versions passées
757 label_me: moi
757 label_me: moi
758 label_board: Forum
758 label_board: Forum
759 label_board_new: Nouveau forum
759 label_board_new: Nouveau forum
760 label_board_plural: Forums
760 label_board_plural: Forums
761 label_topic_plural: Discussions
761 label_topic_plural: Discussions
762 label_message_plural: Messages
762 label_message_plural: Messages
763 label_message_last: Dernier message
763 label_message_last: Dernier message
764 label_message_new: Nouveau message
764 label_message_new: Nouveau message
765 label_message_posted: Message ajouté
765 label_message_posted: Message ajouté
766 label_reply_plural: Réponses
766 label_reply_plural: Réponses
767 label_send_information: Envoyer les informations à l'utilisateur
767 label_send_information: Envoyer les informations à l'utilisateur
768 label_year: Année
768 label_year: Année
769 label_month: Mois
769 label_month: Mois
770 label_week: Semaine
770 label_week: Semaine
771 label_date_from: Du
771 label_date_from: Du
772 label_date_to: Au
772 label_date_to: Au
773 label_language_based: Basé sur la langue de l'utilisateur
773 label_language_based: Basé sur la langue de l'utilisateur
774 label_sort_by: "Trier par %{value}"
774 label_sort_by: "Trier par %{value}"
775 label_send_test_email: Envoyer un email de test
775 label_send_test_email: Envoyer un email de test
776 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
776 label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}"
777 label_module_plural: Modules
777 label_module_plural: Modules
778 label_added_time_by: "Ajouté par %{author} il y a %{age}"
778 label_added_time_by: "Ajouté par %{author} il y a %{age}"
779 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
779 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
780 label_updated_time: "Mis à jour il y a %{value}"
780 label_updated_time: "Mis à jour il y a %{value}"
781 label_jump_to_a_project: Aller à un projet...
781 label_jump_to_a_project: Aller à un projet...
782 label_file_plural: Fichiers
782 label_file_plural: Fichiers
783 label_changeset_plural: Révisions
783 label_changeset_plural: Révisions
784 label_default_columns: Colonnes par défaut
784 label_default_columns: Colonnes par défaut
785 label_no_change_option: (Pas de changement)
785 label_no_change_option: (Pas de changement)
786 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
786 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
787 label_theme: Thème
787 label_theme: Thème
788 label_default: Défaut
788 label_default: Défaut
789 label_search_titles_only: Uniquement dans les titres
789 label_search_titles_only: Uniquement dans les titres
790 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
790 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
791 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
791 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
792 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
792 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
793 label_registration_activation_by_email: activation du compte par email
793 label_registration_activation_by_email: activation du compte par email
794 label_registration_manual_activation: activation manuelle du compte
794 label_registration_manual_activation: activation manuelle du compte
795 label_registration_automatic_activation: activation automatique du compte
795 label_registration_automatic_activation: activation automatique du compte
796 label_display_per_page: "Par page : %{value}"
796 label_display_per_page: "Par page : %{value}"
797 label_age: Âge
797 label_age: Âge
798 label_change_properties: Changer les propriétés
798 label_change_properties: Changer les propriétés
799 label_general: Général
799 label_general: Général
800 label_more: Plus
800 label_more: Plus
801 label_scm: SCM
801 label_scm: SCM
802 label_plugins: Plugins
802 label_plugins: Plugins
803 label_ldap_authentication: Authentification LDAP
803 label_ldap_authentication: Authentification LDAP
804 label_downloads_abbr: D/L
804 label_downloads_abbr: D/L
805 label_optional_description: Description facultative
805 label_optional_description: Description facultative
806 label_add_another_file: Ajouter un autre fichier
806 label_add_another_file: Ajouter un autre fichier
807 label_preferences: Préférences
807 label_preferences: Préférences
808 label_chronological_order: Dans l'ordre chronologique
808 label_chronological_order: Dans l'ordre chronologique
809 label_reverse_chronological_order: Dans l'ordre chronologique inverse
809 label_reverse_chronological_order: Dans l'ordre chronologique inverse
810 label_planning: Planning
810 label_planning: Planning
811 label_incoming_emails: Emails entrants
811 label_incoming_emails: Emails entrants
812 label_generate_key: Générer une clé
812 label_generate_key: Générer une clé
813 label_issue_watchers: Observateurs
813 label_issue_watchers: Observateurs
814 label_example: Exemple
814 label_example: Exemple
815 label_display: Affichage
815 label_display: Affichage
816 label_sort: Tri
816 label_sort: Tri
817 label_ascending: Croissant
817 label_ascending: Croissant
818 label_descending: Décroissant
818 label_descending: Décroissant
819 label_date_from_to: Du %{start} au %{end}
819 label_date_from_to: Du %{start} au %{end}
820 label_wiki_content_added: Page wiki ajoutée
820 label_wiki_content_added: Page wiki ajoutée
821 label_wiki_content_updated: Page wiki mise à jour
821 label_wiki_content_updated: Page wiki mise à jour
822 label_group_plural: Groupes
822 label_group_plural: Groupes
823 label_group: Groupe
823 label_group: Groupe
824 label_group_new: Nouveau groupe
824 label_group_new: Nouveau groupe
825 label_time_entry_plural: Temps passé
825 label_time_entry_plural: Temps passé
826 label_version_sharing_none: Non partagé
826 label_version_sharing_none: Non partagé
827 label_version_sharing_descendants: Avec les sous-projets
827 label_version_sharing_descendants: Avec les sous-projets
828 label_version_sharing_hierarchy: Avec toute la hiérarchie
828 label_version_sharing_hierarchy: Avec toute la hiérarchie
829 label_version_sharing_tree: Avec tout l'arbre
829 label_version_sharing_tree: Avec tout l'arbre
830 label_version_sharing_system: Avec tous les projets
830 label_version_sharing_system: Avec tous les projets
831 label_copy_source: Source
831 label_copy_source: Source
832 label_copy_target: Cible
832 label_copy_target: Cible
833 label_copy_same_as_target: Comme la cible
833 label_copy_same_as_target: Comme la cible
834 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
834 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
835 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
835 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
836 label_api_access_key: Clé d'accès API
836 label_api_access_key: Clé d'accès API
837 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
837 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
838 label_feeds_access_key: Clé d'accès Atom
838 label_feeds_access_key: Clé d'accès Atom
839 label_missing_api_access_key: Clé d'accès API manquante
839 label_missing_api_access_key: Clé d'accès API manquante
840 label_missing_feeds_access_key: Clé d'accès Atom manquante
840 label_missing_feeds_access_key: Clé d'accès Atom manquante
841 label_close_versions: Fermer les versions terminées
841 label_close_versions: Fermer les versions terminées
842 label_revision_id: Révision %{value}
842 label_revision_id: Révision %{value}
843 label_profile: Profil
843 label_profile: Profil
844 label_subtask_plural: Sous-tâches
844 label_subtask_plural: Sous-tâches
845 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
845 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
846 label_principal_search: "Rechercher un utilisateur ou un groupe :"
846 label_principal_search: "Rechercher un utilisateur ou un groupe :"
847 label_user_search: "Rechercher un utilisateur :"
847 label_user_search: "Rechercher un utilisateur :"
848 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
848 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
849 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
849 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
850 label_issues_visibility_all: Toutes les demandes
850 label_issues_visibility_all: Toutes les demandes
851 label_issues_visibility_public: Toutes les demandes non privées
851 label_issues_visibility_public: Toutes les demandes non privées
852 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
852 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
853 label_export_options: Options d'exportation %{export_format}
853 label_export_options: Options d'exportation %{export_format}
854 label_copy_attachments: Copier les fichiers
854 label_copy_attachments: Copier les fichiers
855 label_copy_subtasks: Copier les sous-tâches
855 label_copy_subtasks: Copier les sous-tâches
856 label_item_position: "%{position} sur %{count}"
856 label_item_position: "%{position} sur %{count}"
857 label_completed_versions: Versions passées
857 label_completed_versions: Versions passées
858 label_session_expiration: Expiration des sessions
858 label_session_expiration: Expiration des sessions
859 label_show_closed_projects: Voir les projets fermés
859 label_show_closed_projects: Voir les projets fermés
860 label_status_transitions: Changements de statut
860 label_status_transitions: Changements de statut
861 label_fields_permissions: Permissions sur les champs
861 label_fields_permissions: Permissions sur les champs
862 label_readonly: Lecture
862 label_readonly: Lecture
863 label_required: Obligatoire
863 label_required: Obligatoire
864 label_attribute_of_project: "%{name} du projet"
864 label_attribute_of_project: "%{name} du projet"
865 label_attribute_of_issue: "%{name} de la demande"
865 label_attribute_of_issue: "%{name} de la demande"
866 label_attribute_of_author: "%{name} de l'auteur"
866 label_attribute_of_author: "%{name} de l'auteur"
867 label_attribute_of_assigned_to: "%{name} de l'assigné"
867 label_attribute_of_assigned_to: "%{name} de l'assigné"
868 label_attribute_of_user: "%{name} de l'utilisateur"
868 label_attribute_of_user: "%{name} de l'utilisateur"
869 label_attribute_of_fixed_version: "%{name} de la version cible"
869 label_attribute_of_fixed_version: "%{name} de la version cible"
870 label_cross_project_descendants: Avec les sous-projets
870 label_cross_project_descendants: Avec les sous-projets
871 label_cross_project_tree: Avec tout l'arbre
871 label_cross_project_tree: Avec tout l'arbre
872 label_cross_project_hierarchy: Avec toute la hiérarchie
872 label_cross_project_hierarchy: Avec toute la hiérarchie
873 label_cross_project_system: Avec tous les projets
873 label_cross_project_system: Avec tous les projets
874 label_gantt_progress_line: Ligne de progression
874 label_gantt_progress_line: Ligne de progression
875 label_visibility_private: par moi uniquement
876 label_visibility_roles: par ces roles uniquement
877 label_visibility_public: par tout le monde
875
878
876 button_login: Connexion
879 button_login: Connexion
877 button_submit: Soumettre
880 button_submit: Soumettre
878 button_save: Sauvegarder
881 button_save: Sauvegarder
879 button_check_all: Tout cocher
882 button_check_all: Tout cocher
880 button_uncheck_all: Tout décocher
883 button_uncheck_all: Tout décocher
881 button_collapse_all: Plier tout
884 button_collapse_all: Plier tout
882 button_expand_all: Déplier tout
885 button_expand_all: Déplier tout
883 button_delete: Supprimer
886 button_delete: Supprimer
884 button_create: Créer
887 button_create: Créer
885 button_create_and_continue: Créer et continuer
888 button_create_and_continue: Créer et continuer
886 button_test: Tester
889 button_test: Tester
887 button_edit: Modifier
890 button_edit: Modifier
888 button_add: Ajouter
891 button_add: Ajouter
889 button_change: Changer
892 button_change: Changer
890 button_apply: Appliquer
893 button_apply: Appliquer
891 button_clear: Effacer
894 button_clear: Effacer
892 button_lock: Verrouiller
895 button_lock: Verrouiller
893 button_unlock: Déverrouiller
896 button_unlock: Déverrouiller
894 button_download: Télécharger
897 button_download: Télécharger
895 button_list: Lister
898 button_list: Lister
896 button_view: Voir
899 button_view: Voir
897 button_move: Déplacer
900 button_move: Déplacer
898 button_move_and_follow: Déplacer et suivre
901 button_move_and_follow: Déplacer et suivre
899 button_back: Retour
902 button_back: Retour
900 button_cancel: Annuler
903 button_cancel: Annuler
901 button_activate: Activer
904 button_activate: Activer
902 button_sort: Trier
905 button_sort: Trier
903 button_log_time: Saisir temps
906 button_log_time: Saisir temps
904 button_rollback: Revenir à cette version
907 button_rollback: Revenir à cette version
905 button_watch: Surveiller
908 button_watch: Surveiller
906 button_unwatch: Ne plus surveiller
909 button_unwatch: Ne plus surveiller
907 button_reply: Répondre
910 button_reply: Répondre
908 button_archive: Archiver
911 button_archive: Archiver
909 button_unarchive: Désarchiver
912 button_unarchive: Désarchiver
910 button_reset: Réinitialiser
913 button_reset: Réinitialiser
911 button_rename: Renommer
914 button_rename: Renommer
912 button_change_password: Changer de mot de passe
915 button_change_password: Changer de mot de passe
913 button_copy: Copier
916 button_copy: Copier
914 button_copy_and_follow: Copier et suivre
917 button_copy_and_follow: Copier et suivre
915 button_annotate: Annoter
918 button_annotate: Annoter
916 button_update: Mettre à jour
919 button_update: Mettre à jour
917 button_configure: Configurer
920 button_configure: Configurer
918 button_quote: Citer
921 button_quote: Citer
919 button_duplicate: Dupliquer
922 button_duplicate: Dupliquer
920 button_show: Afficher
923 button_show: Afficher
921 button_hide: Cacher
924 button_hide: Cacher
922 button_edit_section: Modifier cette section
925 button_edit_section: Modifier cette section
923 button_export: Exporter
926 button_export: Exporter
924 button_delete_my_account: Supprimer mon compte
927 button_delete_my_account: Supprimer mon compte
925 button_close: Fermer
928 button_close: Fermer
926 button_reopen: Réouvrir
929 button_reopen: Réouvrir
927
930
928 status_active: actif
931 status_active: actif
929 status_registered: enregistré
932 status_registered: enregistré
930 status_locked: verrouillé
933 status_locked: verrouillé
931
934
932 project_status_active: actif
935 project_status_active: actif
933 project_status_closed: fermé
936 project_status_closed: fermé
934 project_status_archived: archivé
937 project_status_archived: archivé
935
938
936 version_status_open: ouvert
939 version_status_open: ouvert
937 version_status_locked: verrouillé
940 version_status_locked: verrouillé
938 version_status_closed: fermé
941 version_status_closed: fermé
939
942
940 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
943 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
941 text_regexp_info: ex. ^[A-Z0-9]+$
944 text_regexp_info: ex. ^[A-Z0-9]+$
942 text_min_max_length_info: 0 pour aucune restriction
945 text_min_max_length_info: 0 pour aucune restriction
943 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
946 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
944 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
947 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
945 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
948 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
946 text_are_you_sure: Êtes-vous sûr ?
949 text_are_you_sure: Êtes-vous sûr ?
947 text_tip_issue_begin_day: tâche commençant ce jour
950 text_tip_issue_begin_day: tâche commençant ce jour
948 text_tip_issue_end_day: tâche finissant ce jour
951 text_tip_issue_end_day: tâche finissant ce jour
949 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
952 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
950 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et underscore sont autorisés, doit commencer par une minuscule.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
953 text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et underscore sont autorisés, doit commencer par une minuscule.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
951 text_caracters_maximum: "%{count} caractères maximum."
954 text_caracters_maximum: "%{count} caractères maximum."
952 text_caracters_minimum: "%{count} caractères minimum."
955 text_caracters_minimum: "%{count} caractères minimum."
953 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
956 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
954 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
957 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
955 text_unallowed_characters: Caractères non autorisés
958 text_unallowed_characters: Caractères non autorisés
956 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
959 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
957 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
960 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
958 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
961 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
959 text_issue_added: "La demande %{id} a été soumise par %{author}."
962 text_issue_added: "La demande %{id} a été soumise par %{author}."
960 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
963 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
961 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
964 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
962 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
965 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
963 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
966 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
964 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
967 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
965 text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)."
968 text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)."
966 text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
969 text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
967 text_load_default_configuration: Charger le paramétrage par défaut
970 text_load_default_configuration: Charger le paramétrage par défaut
968 text_status_changed_by_changeset: "Appliqué par commit %{value}."
971 text_status_changed_by_changeset: "Appliqué par commit %{value}."
969 text_time_logged_by_changeset: "Appliqué par commit %{value}"
972 text_time_logged_by_changeset: "Appliqué par commit %{value}"
970 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
973 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
971 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
974 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
972 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
975 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
973 text_default_administrator_account_changed: Compte administrateur par défaut changé
976 text_default_administrator_account_changed: Compte administrateur par défaut changé
974 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
977 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
975 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
978 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
976 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
979 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
977 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
980 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
978 text_destroy_time_entries: Supprimer les heures
981 text_destroy_time_entries: Supprimer les heures
979 text_assign_time_entries_to_project: Reporter les heures sur le projet
982 text_assign_time_entries_to_project: Reporter les heures sur le projet
980 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
983 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
981 text_user_wrote: "%{value} a écrit :"
984 text_user_wrote: "%{value} a écrit :"
982 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
985 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
983 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
986 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
984 text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/configuration.yml et redémarrez l'application pour les activer."
987 text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/configuration.yml et redémarrez l'application pour les activer."
985 text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés."
988 text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés."
986 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
989 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
987 text_custom_field_possible_values_info: 'Une ligne par valeur'
990 text_custom_field_possible_values_info: 'Une ligne par valeur'
988 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
991 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
989 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
992 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
990 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
993 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
991 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
994 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
992 text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
995 text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
993 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
996 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
994 text_issue_conflict_resolution_overwrite: "Appliquer quand même ma mise à jour (les notes précédentes seront conservées mais des changements pourront être écrasés)"
997 text_issue_conflict_resolution_overwrite: "Appliquer quand même ma mise à jour (les notes précédentes seront conservées mais des changements pourront être écrasés)"
995 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
998 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
996 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
999 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
997 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
1000 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
998 text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre."
1001 text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre."
999 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1002 text_project_closed: Ce projet est fermé et accessible en lecture seule.
1000 text_turning_multiple_off: "Si vous désactivez les valeurs multiples, les valeurs multiples seront supprimées pour n'en conserver qu'une par objet."
1003 text_turning_multiple_off: "Si vous désactivez les valeurs multiples, les valeurs multiples seront supprimées pour n'en conserver qu'une par objet."
1001
1004
1002 default_role_manager: "Manager "
1005 default_role_manager: "Manager "
1003 default_role_developer: "Développeur "
1006 default_role_developer: "Développeur "
1004 default_role_reporter: "Rapporteur "
1007 default_role_reporter: "Rapporteur "
1005 default_tracker_bug: Anomalie
1008 default_tracker_bug: Anomalie
1006 default_tracker_feature: Evolution
1009 default_tracker_feature: Evolution
1007 default_tracker_support: Assistance
1010 default_tracker_support: Assistance
1008 default_issue_status_new: Nouveau
1011 default_issue_status_new: Nouveau
1009 default_issue_status_in_progress: En cours
1012 default_issue_status_in_progress: En cours
1010 default_issue_status_resolved: Résolu
1013 default_issue_status_resolved: Résolu
1011 default_issue_status_feedback: Commentaire
1014 default_issue_status_feedback: Commentaire
1012 default_issue_status_closed: Fermé
1015 default_issue_status_closed: Fermé
1013 default_issue_status_rejected: Rejeté
1016 default_issue_status_rejected: Rejeté
1014 default_doc_category_user: Documentation utilisateur
1017 default_doc_category_user: Documentation utilisateur
1015 default_doc_category_tech: Documentation technique
1018 default_doc_category_tech: Documentation technique
1016 default_priority_low: Bas
1019 default_priority_low: Bas
1017 default_priority_normal: Normal
1020 default_priority_normal: Normal
1018 default_priority_high: Haut
1021 default_priority_high: Haut
1019 default_priority_urgent: Urgent
1022 default_priority_urgent: Urgent
1020 default_priority_immediate: Immédiat
1023 default_priority_immediate: Immédiat
1021 default_activity_design: Conception
1024 default_activity_design: Conception
1022 default_activity_development: Développement
1025 default_activity_development: Développement
1023
1026
1024 enumeration_issue_priorities: Priorités des demandes
1027 enumeration_issue_priorities: Priorités des demandes
1025 enumeration_doc_categories: Catégories des documents
1028 enumeration_doc_categories: Catégories des documents
1026 enumeration_activities: Activités (suivi du temps)
1029 enumeration_activities: Activités (suivi du temps)
1027 label_greater_or_equal: ">="
1030 label_greater_or_equal: ">="
1028 label_less_or_equal: "<="
1031 label_less_or_equal: "<="
1029 label_between: entre
1032 label_between: entre
1030 label_view_all_revisions: Voir toutes les révisions
1033 label_view_all_revisions: Voir toutes les révisions
1031 label_tag: Tag
1034 label_tag: Tag
1032 label_branch: Branche
1035 label_branch: Branche
1033 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
1036 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
1034 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
1037 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
1035 text_journal_changed: "%{label} changé de %{old} à %{new}"
1038 text_journal_changed: "%{label} changé de %{old} à %{new}"
1036 text_journal_changed_no_detail: "%{label} mis à jour"
1039 text_journal_changed_no_detail: "%{label} mis à jour"
1037 text_journal_set_to: "%{label} mis à %{value}"
1040 text_journal_set_to: "%{label} mis à %{value}"
1038 text_journal_deleted: "%{label} %{old} supprimé"
1041 text_journal_deleted: "%{label} %{old} supprimé"
1039 text_journal_added: "%{label} %{value} ajouté"
1042 text_journal_added: "%{label} %{value} ajouté"
1040 enumeration_system_activity: Activité système
1043 enumeration_system_activity: Activité système
1041 label_board_sticky: Sticky
1044 label_board_sticky: Sticky
1042 label_board_locked: Verrouillé
1045 label_board_locked: Verrouillé
1043 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
1046 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
1044 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
1047 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
1045 error_unable_to_connect: Connexion impossible (%{value})
1048 error_unable_to_connect: Connexion impossible (%{value})
1046 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
1049 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
1047 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
1050 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
1048 field_principal: Principal
1051 field_principal: Principal
1049 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
1052 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
1050 text_zoom_out: Zoom arrière
1053 text_zoom_out: Zoom arrière
1051 text_zoom_in: Zoom avant
1054 text_zoom_in: Zoom avant
1052 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
1055 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
1053 label_overall_spent_time: Temps passé global
1056 label_overall_spent_time: Temps passé global
1054 field_time_entries: Temps passé
1057 field_time_entries: Temps passé
1055 project_module_gantt: Gantt
1058 project_module_gantt: Gantt
1056 project_module_calendar: Calendrier
1059 project_module_calendar: Calendrier
1057 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
1060 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
1058 field_text: Champ texte
1061 field_text: Champ texte
1059 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
1062 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
1060 setting_default_notification_option: Option de notification par défaut
1063 setting_default_notification_option: Option de notification par défaut
1061 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
1064 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
1062 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
1065 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
1063 label_user_mail_option_none: Aucune notification
1066 label_user_mail_option_none: Aucune notification
1064 field_member_of_group: Groupe de l'assigné
1067 field_member_of_group: Groupe de l'assigné
1065 field_assigned_to_role: Rôle de l'assigné
1068 field_assigned_to_role: Rôle de l'assigné
1066 setting_emails_header: En-tête des emails
1069 setting_emails_header: En-tête des emails
1067 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
1070 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
1068 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1071 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1069 field_scm_path_encoding: Encodage des chemins
1072 field_scm_path_encoding: Encodage des chemins
1070 text_scm_path_encoding_note: "Défaut : UTF-8"
1073 text_scm_path_encoding_note: "Défaut : UTF-8"
1071 field_path_to_repository: Chemin du dépôt
1074 field_path_to_repository: Chemin du dépôt
1072 field_root_directory: Répertoire racine
1075 field_root_directory: Répertoire racine
1073 field_cvs_module: Module
1076 field_cvs_module: Module
1074 field_cvsroot: CVSROOT
1077 field_cvsroot: CVSROOT
1075 text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1078 text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1076 text_scm_command: Commande
1079 text_scm_command: Commande
1077 text_scm_command_version: Version
1080 text_scm_command_version: Version
1078 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
1081 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
1079 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1082 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1080 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1083 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1081 label_diff: diff
1084 label_diff: diff
1082 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1085 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1083 description_query_sort_criteria_direction: Ordre de tri
1086 description_query_sort_criteria_direction: Ordre de tri
1084 description_project_scope: Périmètre de recherche
1087 description_project_scope: Périmètre de recherche
1085 description_filter: Filtre
1088 description_filter: Filtre
1086 description_user_mail_notification: Option de notification
1089 description_user_mail_notification: Option de notification
1087 description_date_from: Date de début
1090 description_date_from: Date de début
1088 description_message_content: Contenu du message
1091 description_message_content: Contenu du message
1089 description_available_columns: Colonnes disponibles
1092 description_available_columns: Colonnes disponibles
1090 description_all_columns: Toutes les colonnes
1093 description_all_columns: Toutes les colonnes
1091 description_date_range_interval: Choisir une période
1094 description_date_range_interval: Choisir une période
1092 description_issue_category_reassign: Choisir une catégorie
1095 description_issue_category_reassign: Choisir une catégorie
1093 description_search: Champ de recherche
1096 description_search: Champ de recherche
1094 description_notes: Notes
1097 description_notes: Notes
1095 description_date_range_list: Choisir une période prédéfinie
1098 description_date_range_list: Choisir une période prédéfinie
1096 description_choose_project: Projets
1099 description_choose_project: Projets
1097 description_date_to: Date de fin
1100 description_date_to: Date de fin
1098 description_query_sort_criteria_attribute: Critère de tri
1101 description_query_sort_criteria_attribute: Critère de tri
1099 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1102 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1100 description_selected_columns: Colonnes sélectionnées
1103 description_selected_columns: Colonnes sélectionnées
1101 label_parent_revision: Parent
1104 label_parent_revision: Parent
1102 label_child_revision: Enfant
1105 label_child_revision: Enfant
1103 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
1106 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
1104 setting_repositories_encodings: Encodages des fichiers et des dépôts
1107 setting_repositories_encodings: Encodages des fichiers et des dépôts
1105 label_search_for_watchers: Rechercher des observateurs
1108 label_search_for_watchers: Rechercher des observateurs
1106 text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et underscore sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
1109 text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et underscore sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
@@ -1,1154 +1,1156
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3
3
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 #content h1, h2, h3, h4 {color: #555;}
5 #content h1, h2, h3, h4 {color: #555;}
6 h2, .wiki h1 {font-size: 20px;}
6 h2, .wiki h1 {font-size: 20px;}
7 h3, .wiki h2 {font-size: 16px;}
7 h3, .wiki h2 {font-size: 16px;}
8 h4, .wiki h3 {font-size: 13px;}
8 h4, .wiki h3 {font-size: 13px;}
9 h4 {border-bottom: 1px dotted #bbb;}
9 h4 {border-bottom: 1px dotted #bbb;}
10
10
11 /***** Layout *****/
11 /***** Layout *****/
12 #wrapper {background: white;}
12 #wrapper {background: white;}
13
13
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 #top-menu ul {margin: 0; padding: 0;}
15 #top-menu ul {margin: 0; padding: 0;}
16 #top-menu li {
16 #top-menu li {
17 float:left;
17 float:left;
18 list-style-type:none;
18 list-style-type:none;
19 margin: 0px 0px 0px 0px;
19 margin: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
21 white-space:nowrap;
21 white-space:nowrap;
22 }
22 }
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25
25
26 #account {float:right;}
26 #account {float:right;}
27
27
28 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
28 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
29 #header a {color:#f8f8f8;}
29 #header a {color:#f8f8f8;}
30 #header h1 a.ancestor { font-size: 80%; }
30 #header h1 a.ancestor { font-size: 80%; }
31 #quick-search {float:right;}
31 #quick-search {float:right;}
32
32
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
34 #main-menu ul {margin: 0; padding: 0;}
34 #main-menu ul {margin: 0; padding: 0;}
35 #main-menu li {
35 #main-menu li {
36 float:left;
36 float:left;
37 list-style-type:none;
37 list-style-type:none;
38 margin: 0px 2px 0px 0px;
38 margin: 0px 2px 0px 0px;
39 padding: 0px 0px 0px 0px;
39 padding: 0px 0px 0px 0px;
40 white-space:nowrap;
40 white-space:nowrap;
41 }
41 }
42 #main-menu li a {
42 #main-menu li a {
43 display: block;
43 display: block;
44 color: #fff;
44 color: #fff;
45 text-decoration: none;
45 text-decoration: none;
46 font-weight: bold;
46 font-weight: bold;
47 margin: 0;
47 margin: 0;
48 padding: 4px 10px 4px 10px;
48 padding: 4px 10px 4px 10px;
49 }
49 }
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
52
52
53 #admin-menu ul {margin: 0; padding: 0;}
53 #admin-menu ul {margin: 0; padding: 0;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
55
55
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
70
70
71 #main {background-color:#EEEEEE;}
71 #main {background-color:#EEEEEE;}
72
72
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
74 * html #sidebar{ width: 22%; }
74 * html #sidebar{ width: 22%; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
78 #sidebar .contextual { margin-right: 1em; }
78 #sidebar .contextual { margin-right: 1em; }
79 #sidebar ul {margin: 0; padding: 0;}
79 #sidebar ul {margin: 0; padding: 0;}
80 #sidebar ul li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
80 #sidebar ul li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
81
81
82 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
82 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
83 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
83 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
84 html>body #content { min-height: 600px; }
84 html>body #content { min-height: 600px; }
85 * html body #content { height: 600px; } /* IE */
85 * html body #content { height: 600px; } /* IE */
86
86
87 #main.nosidebar #sidebar{ display: none; }
87 #main.nosidebar #sidebar{ display: none; }
88 #main.nosidebar #content{ width: auto; border-right: 0; }
88 #main.nosidebar #content{ width: auto; border-right: 0; }
89
89
90 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
90 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
91
91
92 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
92 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
93 #login-form table td {padding: 6px;}
93 #login-form table td {padding: 6px;}
94 #login-form label {font-weight: bold;}
94 #login-form label {font-weight: bold;}
95 #login-form input#username, #login-form input#password { width: 300px; }
95 #login-form input#username, #login-form input#password { width: 300px; }
96
96
97 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
97 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
98 div.modal h3.title {display:none;}
98 div.modal h3.title {display:none;}
99 div.modal p.buttons {text-align:right; margin-bottom:0;}
99 div.modal p.buttons {text-align:right; margin-bottom:0;}
100
100
101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
102
102
103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
104
104
105 /***** Links *****/
105 /***** Links *****/
106 a, a:link, a:visited{ color: #169; text-decoration: none; }
106 a, a:link, a:visited{ color: #169; text-decoration: none; }
107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
108 a img{ border: 0; }
108 a img{ border: 0; }
109
109
110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
111 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
111 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
112 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
112 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
113
113
114 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
114 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
115 #sidebar a.selected:hover {text-decoration:none;}
115 #sidebar a.selected:hover {text-decoration:none;}
116 #admin-menu a {line-height:1.7em;}
116 #admin-menu a {line-height:1.7em;}
117 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
117 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
118
118
119 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
119 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
120 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
120 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
121
121
122 a#toggle-completed-versions {color:#999;}
122 a#toggle-completed-versions {color:#999;}
123 /***** Tables *****/
123 /***** Tables *****/
124 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
124 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
125 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
125 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
126 table.list td { vertical-align: top; padding-right:10px; }
126 table.list td { vertical-align: top; padding-right:10px; }
127 table.list td.id { width: 2%; text-align: center;}
127 table.list td.id { width: 2%; text-align: center;}
128 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
128 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
129 table.list td.checkbox input {padding:0px;}
129 table.list td.checkbox input {padding:0px;}
130 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
130 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
131 table.list td.buttons a { padding-right: 0.6em; }
131 table.list td.buttons a { padding-right: 0.6em; }
132 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
132 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
133
133
134 tr.project td.name a { white-space:nowrap; }
134 tr.project td.name a { white-space:nowrap; }
135 tr.project.closed, tr.project.archived { color: #aaa; }
135 tr.project.closed, tr.project.archived { color: #aaa; }
136 tr.project.closed a, tr.project.archived a { color: #aaa; }
136 tr.project.closed a, tr.project.archived a { color: #aaa; }
137
137
138 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
138 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
139 tr.project.idnt-1 td.name {padding-left: 0.5em;}
139 tr.project.idnt-1 td.name {padding-left: 0.5em;}
140 tr.project.idnt-2 td.name {padding-left: 2em;}
140 tr.project.idnt-2 td.name {padding-left: 2em;}
141 tr.project.idnt-3 td.name {padding-left: 3.5em;}
141 tr.project.idnt-3 td.name {padding-left: 3.5em;}
142 tr.project.idnt-4 td.name {padding-left: 5em;}
142 tr.project.idnt-4 td.name {padding-left: 5em;}
143 tr.project.idnt-5 td.name {padding-left: 6.5em;}
143 tr.project.idnt-5 td.name {padding-left: 6.5em;}
144 tr.project.idnt-6 td.name {padding-left: 8em;}
144 tr.project.idnt-6 td.name {padding-left: 8em;}
145 tr.project.idnt-7 td.name {padding-left: 9.5em;}
145 tr.project.idnt-7 td.name {padding-left: 9.5em;}
146 tr.project.idnt-8 td.name {padding-left: 11em;}
146 tr.project.idnt-8 td.name {padding-left: 11em;}
147 tr.project.idnt-9 td.name {padding-left: 12.5em;}
147 tr.project.idnt-9 td.name {padding-left: 12.5em;}
148
148
149 tr.issue { text-align: center; white-space: nowrap; }
149 tr.issue { text-align: center; white-space: nowrap; }
150 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
150 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
151 tr.issue td.subject, tr.issue td.relations { text-align: left; }
151 tr.issue td.subject, tr.issue td.relations { text-align: left; }
152 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
152 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
153 tr.issue td.relations span {white-space: nowrap;}
153 tr.issue td.relations span {white-space: nowrap;}
154 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
154 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
155 table.issues td.description pre {white-space:normal;}
155 table.issues td.description pre {white-space:normal;}
156
156
157 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
157 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
158 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
158 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
159 tr.issue.idnt-2 td.subject {padding-left: 2em;}
159 tr.issue.idnt-2 td.subject {padding-left: 2em;}
160 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
160 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
161 tr.issue.idnt-4 td.subject {padding-left: 5em;}
161 tr.issue.idnt-4 td.subject {padding-left: 5em;}
162 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
162 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
163 tr.issue.idnt-6 td.subject {padding-left: 8em;}
163 tr.issue.idnt-6 td.subject {padding-left: 8em;}
164 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
164 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
165 tr.issue.idnt-8 td.subject {padding-left: 11em;}
165 tr.issue.idnt-8 td.subject {padding-left: 11em;}
166 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
166 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
167
167
168 tr.entry { border: 1px solid #f8f8f8; }
168 tr.entry { border: 1px solid #f8f8f8; }
169 tr.entry td { white-space: nowrap; }
169 tr.entry td { white-space: nowrap; }
170 tr.entry td.filename { width: 30%; }
170 tr.entry td.filename { width: 30%; }
171 tr.entry td.filename_no_report { width: 70%; }
171 tr.entry td.filename_no_report { width: 70%; }
172 tr.entry td.size { text-align: right; font-size: 90%; }
172 tr.entry td.size { text-align: right; font-size: 90%; }
173 tr.entry td.revision, tr.entry td.author { text-align: center; }
173 tr.entry td.revision, tr.entry td.author { text-align: center; }
174 tr.entry td.age { text-align: right; }
174 tr.entry td.age { text-align: right; }
175 tr.entry.file td.filename a { margin-left: 16px; }
175 tr.entry.file td.filename a { margin-left: 16px; }
176 tr.entry.file td.filename_no_report a { margin-left: 16px; }
176 tr.entry.file td.filename_no_report a { margin-left: 16px; }
177
177
178 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
178 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
179 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
179 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
180
180
181 tr.changeset { height: 20px }
181 tr.changeset { height: 20px }
182 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
182 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
183 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
183 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
184 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
184 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
185 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
185 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
186
186
187 table.files tr.file td { text-align: center; }
187 table.files tr.file td { text-align: center; }
188 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
188 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
189 table.files tr.file td.digest { font-size: 80%; }
189 table.files tr.file td.digest { font-size: 80%; }
190
190
191 table.members td.roles, table.memberships td.roles { width: 45%; }
191 table.members td.roles, table.memberships td.roles { width: 45%; }
192
192
193 tr.message { height: 2.6em; }
193 tr.message { height: 2.6em; }
194 tr.message td.subject { padding-left: 20px; }
194 tr.message td.subject { padding-left: 20px; }
195 tr.message td.created_on { white-space: nowrap; }
195 tr.message td.created_on { white-space: nowrap; }
196 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
196 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
197 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
197 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
198 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
198 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
199
199
200 tr.version.closed, tr.version.closed a { color: #999; }
200 tr.version.closed, tr.version.closed a { color: #999; }
201 tr.version td.name { padding-left: 20px; }
201 tr.version td.name { padding-left: 20px; }
202 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
202 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
203 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
203 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
204
204
205 tr.user td { width:13%; }
205 tr.user td { width:13%; }
206 tr.user td.email { width:18%; }
206 tr.user td.email { width:18%; }
207 tr.user td { white-space: nowrap; }
207 tr.user td { white-space: nowrap; }
208 tr.user.locked, tr.user.registered { color: #aaa; }
208 tr.user.locked, tr.user.registered { color: #aaa; }
209 tr.user.locked a, tr.user.registered a { color: #aaa; }
209 tr.user.locked a, tr.user.registered a { color: #aaa; }
210
210
211 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
211 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
212
212
213 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
213 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
214
214
215 tr.time-entry { text-align: center; white-space: nowrap; }
215 tr.time-entry { text-align: center; white-space: nowrap; }
216 tr.time-entry td.issue, tr.time-entry td.comments { text-align: left; white-space: normal; }
216 tr.time-entry td.issue, tr.time-entry td.comments { text-align: left; white-space: normal; }
217 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
217 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
218 td.hours .hours-dec { font-size: 0.9em; }
218 td.hours .hours-dec { font-size: 0.9em; }
219
219
220 table.plugins td { vertical-align: middle; }
220 table.plugins td { vertical-align: middle; }
221 table.plugins td.configure { text-align: right; padding-right: 1em; }
221 table.plugins td.configure { text-align: right; padding-right: 1em; }
222 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
222 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
223 table.plugins span.description { display: block; font-size: 0.9em; }
223 table.plugins span.description { display: block; font-size: 0.9em; }
224 table.plugins span.url { display: block; font-size: 0.9em; }
224 table.plugins span.url { display: block; font-size: 0.9em; }
225
225
226 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
226 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
227 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
227 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
228 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
228 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
229 tr.group:hover a.toggle-all { display:inline;}
229 tr.group:hover a.toggle-all { display:inline;}
230 a.toggle-all:hover {text-decoration:none;}
230 a.toggle-all:hover {text-decoration:none;}
231
231
232 table.list tbody tr:hover { background-color:#ffffdd; }
232 table.list tbody tr:hover { background-color:#ffffdd; }
233 table.list tbody tr.group:hover { background-color:inherit; }
233 table.list tbody tr.group:hover { background-color:inherit; }
234 table td {padding:2px;}
234 table td {padding:2px;}
235 table p {margin:0;}
235 table p {margin:0;}
236 .odd {background-color:#f6f7f8;}
236 .odd {background-color:#f6f7f8;}
237 .even {background-color: #fff;}
237 .even {background-color: #fff;}
238
238
239 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
239 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
240 a.sort.asc { background-image: url(../images/sort_asc.png); }
240 a.sort.asc { background-image: url(../images/sort_asc.png); }
241 a.sort.desc { background-image: url(../images/sort_desc.png); }
241 a.sort.desc { background-image: url(../images/sort_desc.png); }
242
242
243 table.attributes { width: 100% }
243 table.attributes { width: 100% }
244 table.attributes th { vertical-align: top; text-align: left; }
244 table.attributes th { vertical-align: top; text-align: left; }
245 table.attributes td { vertical-align: top; }
245 table.attributes td { vertical-align: top; }
246
246
247 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
247 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
248 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
248 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
249 table.boards td.last-message {font-size:80%;}
249 table.boards td.last-message {font-size:80%;}
250
250
251 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
251 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
252
252
253 table.query-columns {
253 table.query-columns {
254 border-collapse: collapse;
254 border-collapse: collapse;
255 border: 0;
255 border: 0;
256 }
256 }
257
257
258 table.query-columns td.buttons {
258 table.query-columns td.buttons {
259 vertical-align: middle;
259 vertical-align: middle;
260 text-align: center;
260 text-align: center;
261 }
261 }
262
262
263 td.center {text-align:center;}
263 td.center {text-align:center;}
264
264
265 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
265 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
266
266
267 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
267 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
268 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
268 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
269 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
269 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
270 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
270 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
271
271
272 #watchers select {width: 95%; display: block;}
272 #watchers select {width: 95%; display: block;}
273 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
273 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
274 #watchers a.delete:hover {opacity: 1;}
274 #watchers a.delete:hover {opacity: 1;}
275 #watchers img.gravatar {margin: 0 4px 2px 0;}
275 #watchers img.gravatar {margin: 0 4px 2px 0;}
276
276
277 span#watchers_inputs {overflow:auto; display:block;}
277 span#watchers_inputs {overflow:auto; display:block;}
278 span.search_for_watchers {display:block;}
278 span.search_for_watchers {display:block;}
279 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
279 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
280 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
280 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
281
281
282
282
283 .highlight { background-color: #FCFD8D;}
283 .highlight { background-color: #FCFD8D;}
284 .highlight.token-1 { background-color: #faa;}
284 .highlight.token-1 { background-color: #faa;}
285 .highlight.token-2 { background-color: #afa;}
285 .highlight.token-2 { background-color: #afa;}
286 .highlight.token-3 { background-color: #aaf;}
286 .highlight.token-3 { background-color: #aaf;}
287
287
288 .box{
288 .box{
289 padding:6px;
289 padding:6px;
290 margin-bottom: 10px;
290 margin-bottom: 10px;
291 background-color:#f6f6f6;
291 background-color:#f6f6f6;
292 color:#505050;
292 color:#505050;
293 line-height:1.5em;
293 line-height:1.5em;
294 border: 1px solid #e4e4e4;
294 border: 1px solid #e4e4e4;
295 }
295 }
296
296
297 div.square {
297 div.square {
298 border: 1px solid #999;
298 border: 1px solid #999;
299 float: left;
299 float: left;
300 margin: .3em .4em 0 .4em;
300 margin: .3em .4em 0 .4em;
301 overflow: hidden;
301 overflow: hidden;
302 width: .6em; height: .6em;
302 width: .6em; height: .6em;
303 }
303 }
304 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
304 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
305 .contextual input, .contextual select {font-size:0.9em;}
305 .contextual input, .contextual select {font-size:0.9em;}
306 .message .contextual { margin-top: 0; }
306 .message .contextual { margin-top: 0; }
307
307
308 .splitcontent {overflow:auto;}
308 .splitcontent {overflow:auto;}
309 .splitcontentleft{float:left; width:49%;}
309 .splitcontentleft{float:left; width:49%;}
310 .splitcontentright{float:right; width:49%;}
310 .splitcontentright{float:right; width:49%;}
311 form {display: inline;}
311 form {display: inline;}
312 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
312 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
313 fieldset {border: 1px solid #e4e4e4; margin:0;}
313 fieldset {border: 1px solid #e4e4e4; margin:0;}
314 legend {color: #484848;}
314 legend {color: #484848;}
315 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
315 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
316 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
316 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
317 blockquote blockquote { margin-left: 0;}
317 blockquote blockquote { margin-left: 0;}
318 acronym { border-bottom: 1px dotted; cursor: help; }
318 acronym { border-bottom: 1px dotted; cursor: help; }
319 textarea.wiki-edit {width:99%; resize:vertical;}
319 textarea.wiki-edit {width:99%; resize:vertical;}
320 li p {margin-top: 0;}
320 li p {margin-top: 0;}
321 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
321 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
322 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
322 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
323 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
323 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
324 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
324 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
325
325
326 div.issue div.subject div div { padding-left: 16px; }
326 div.issue div.subject div div { padding-left: 16px; }
327 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
327 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
328 div.issue div.subject>div>p { margin-top: 0.5em; }
328 div.issue div.subject>div>p { margin-top: 0.5em; }
329 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
329 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
330 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
330 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
331 div.issue .next-prev-links {color:#999;}
331 div.issue .next-prev-links {color:#999;}
332 div.issue table.attributes th {width:22%;}
332 div.issue table.attributes th {width:22%;}
333 div.issue table.attributes td {width:28%;}
333 div.issue table.attributes td {width:28%;}
334
334
335 #issue_tree table.issues, #relations table.issues { border: 0; }
335 #issue_tree table.issues, #relations table.issues { border: 0; }
336 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
336 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
337 #relations td.buttons {padding:0;}
337 #relations td.buttons {padding:0;}
338
338
339 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
339 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
340 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
340 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
341 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
341 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
342
342
343 fieldset#date-range p { margin: 2px 0 2px 0; }
343 fieldset#date-range p { margin: 2px 0 2px 0; }
344 fieldset#filters table { border-collapse: collapse; }
344 fieldset#filters table { border-collapse: collapse; }
345 fieldset#filters table td { padding: 0; vertical-align: middle; }
345 fieldset#filters table td { padding: 0; vertical-align: middle; }
346 fieldset#filters tr.filter { height: 2.1em; }
346 fieldset#filters tr.filter { height: 2.1em; }
347 fieldset#filters td.field { width:230px; }
347 fieldset#filters td.field { width:230px; }
348 fieldset#filters td.operator { width:180px; }
348 fieldset#filters td.operator { width:180px; }
349 fieldset#filters td.operator select {max-width:170px;}
349 fieldset#filters td.operator select {max-width:170px;}
350 fieldset#filters td.values { white-space:nowrap; }
350 fieldset#filters td.values { white-space:nowrap; }
351 fieldset#filters td.values select {min-width:130px;}
351 fieldset#filters td.values select {min-width:130px;}
352 fieldset#filters td.values input {height:1em;}
352 fieldset#filters td.values input {height:1em;}
353 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
353 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
354
354
355 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
355 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
356 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
356 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
357
357
358 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
358 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
359 div#issue-changesets div.changeset { padding: 4px;}
359 div#issue-changesets div.changeset { padding: 4px;}
360 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
360 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
361 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
361 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
362
362
363 .journal ul.details img {margin:0 0 -3px 4px;}
363 .journal ul.details img {margin:0 0 -3px 4px;}
364 div.journal {overflow:auto;}
364 div.journal {overflow:auto;}
365 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
365 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
366
366
367 div#activity dl, #search-results { margin-left: 2em; }
367 div#activity dl, #search-results { margin-left: 2em; }
368 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
368 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
369 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
369 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
370 div#activity dt.me .time { border-bottom: 1px solid #999; }
370 div#activity dt.me .time { border-bottom: 1px solid #999; }
371 div#activity dt .time { color: #777; font-size: 80%; }
371 div#activity dt .time { color: #777; font-size: 80%; }
372 div#activity dd .description, #search-results dd .description { font-style: italic; }
372 div#activity dd .description, #search-results dd .description { font-style: italic; }
373 div#activity span.project:after, #search-results span.project:after { content: " -"; }
373 div#activity span.project:after, #search-results span.project:after { content: " -"; }
374 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
374 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
375 div#activity dt.grouped {margin-left:5em;}
375 div#activity dt.grouped {margin-left:5em;}
376 div#activity dd.grouped {margin-left:9em;}
376 div#activity dd.grouped {margin-left:9em;}
377
377
378 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
378 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
379
379
380 div#search-results-counts {float:right;}
380 div#search-results-counts {float:right;}
381 div#search-results-counts ul { margin-top: 0.5em; }
381 div#search-results-counts ul { margin-top: 0.5em; }
382 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
382 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
383
383
384 dt.issue { background-image: url(../images/ticket.png); }
384 dt.issue { background-image: url(../images/ticket.png); }
385 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
385 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
386 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
386 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
387 dt.issue-note { background-image: url(../images/ticket_note.png); }
387 dt.issue-note { background-image: url(../images/ticket_note.png); }
388 dt.changeset { background-image: url(../images/changeset.png); }
388 dt.changeset { background-image: url(../images/changeset.png); }
389 dt.news { background-image: url(../images/news.png); }
389 dt.news { background-image: url(../images/news.png); }
390 dt.message { background-image: url(../images/message.png); }
390 dt.message { background-image: url(../images/message.png); }
391 dt.reply { background-image: url(../images/comments.png); }
391 dt.reply { background-image: url(../images/comments.png); }
392 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
392 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
393 dt.attachment { background-image: url(../images/attachment.png); }
393 dt.attachment { background-image: url(../images/attachment.png); }
394 dt.document { background-image: url(../images/document.png); }
394 dt.document { background-image: url(../images/document.png); }
395 dt.project { background-image: url(../images/projects.png); }
395 dt.project { background-image: url(../images/projects.png); }
396 dt.time-entry { background-image: url(../images/time.png); }
396 dt.time-entry { background-image: url(../images/time.png); }
397
397
398 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
398 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
399
399
400 div#roadmap .related-issues { margin-bottom: 1em; }
400 div#roadmap .related-issues { margin-bottom: 1em; }
401 div#roadmap .related-issues td.checkbox { display: none; }
401 div#roadmap .related-issues td.checkbox { display: none; }
402 div#roadmap .wiki h1:first-child { display: none; }
402 div#roadmap .wiki h1:first-child { display: none; }
403 div#roadmap .wiki h1 { font-size: 120%; }
403 div#roadmap .wiki h1 { font-size: 120%; }
404 div#roadmap .wiki h2 { font-size: 110%; }
404 div#roadmap .wiki h2 { font-size: 110%; }
405 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
405 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
406
406
407 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
407 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
408 div#version-summary fieldset { margin-bottom: 1em; }
408 div#version-summary fieldset { margin-bottom: 1em; }
409 div#version-summary fieldset.time-tracking table { width:100%; }
409 div#version-summary fieldset.time-tracking table { width:100%; }
410 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
410 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
411
411
412 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
412 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
413 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
413 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
414 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
414 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
415 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
415 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
416 table#time-report .hours-dec { font-size: 0.9em; }
416 table#time-report .hours-dec { font-size: 0.9em; }
417
417
418 div.wiki-page .contextual a {opacity: 0.4}
418 div.wiki-page .contextual a {opacity: 0.4}
419 div.wiki-page .contextual a:hover {opacity: 1}
419 div.wiki-page .contextual a:hover {opacity: 1}
420
420
421 form .attributes select { width: 60%; }
421 form .attributes select { width: 60%; }
422 input#issue_subject { width: 99%; }
422 input#issue_subject { width: 99%; }
423 select#issue_done_ratio { width: 95px; }
423 select#issue_done_ratio { width: 95px; }
424
424
425 ul.projects {margin:0; padding-left:1em;}
425 ul.projects {margin:0; padding-left:1em;}
426 ul.projects ul {padding-left:1.6em;}
426 ul.projects ul {padding-left:1.6em;}
427 ul.projects.root {margin:0; padding:0;}
427 ul.projects.root {margin:0; padding:0;}
428 ul.projects li {list-style-type:none;}
428 ul.projects li {list-style-type:none;}
429
429
430 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
430 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
431 #projects-index ul.projects li.root {margin-bottom: 1em;}
431 #projects-index ul.projects li.root {margin-bottom: 1em;}
432 #projects-index ul.projects li.child {margin-top: 1em;}
432 #projects-index ul.projects li.child {margin-top: 1em;}
433 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
433 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
434 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
434 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
435
435
436 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
436 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
437
437
438 #related-issues li img {vertical-align:middle;}
438 #related-issues li img {vertical-align:middle;}
439
439
440 ul.properties {padding:0; font-size: 0.9em; color: #777;}
440 ul.properties {padding:0; font-size: 0.9em; color: #777;}
441 ul.properties li {list-style-type:none;}
441 ul.properties li {list-style-type:none;}
442 ul.properties li span {font-style:italic;}
442 ul.properties li span {font-style:italic;}
443
443
444 .total-hours { font-size: 110%; font-weight: bold; }
444 .total-hours { font-size: 110%; font-weight: bold; }
445 .total-hours span.hours-int { font-size: 120%; }
445 .total-hours span.hours-int { font-size: 120%; }
446
446
447 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
447 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
448 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
448 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
449
449
450 #workflow_copy_form select { width: 200px; }
450 #workflow_copy_form select { width: 200px; }
451 table.transitions td.enabled {background: #bfb;}
451 table.transitions td.enabled {background: #bfb;}
452 table.fields_permissions select {font-size:90%}
452 table.fields_permissions select {font-size:90%}
453 table.fields_permissions td.readonly {background:#ddd;}
453 table.fields_permissions td.readonly {background:#ddd;}
454 table.fields_permissions td.required {background:#d88;}
454 table.fields_permissions td.required {background:#d88;}
455
455
456 textarea#custom_field_possible_values {width: 99%}
456 textarea#custom_field_possible_values {width: 99%}
457 textarea#custom_field_default_value {width: 99%}
457 textarea#custom_field_default_value {width: 99%}
458
458
459 input#content_comments {width: 99%}
459 input#content_comments {width: 99%}
460
460
461 p.pagination {margin-top:8px; font-size: 90%}
461 p.pagination {margin-top:8px; font-size: 90%}
462
462
463 /***** Tabular forms ******/
463 /***** Tabular forms ******/
464 .tabular p{
464 .tabular p{
465 margin: 0;
465 margin: 0;
466 padding: 3px 0 3px 0;
466 padding: 3px 0 3px 0;
467 padding-left: 180px; /* width of left column containing the label elements */
467 padding-left: 180px; /* width of left column containing the label elements */
468 min-height: 1.8em;
468 min-height: 1.8em;
469 clear:left;
469 clear:left;
470 }
470 }
471
471
472 html>body .tabular p {overflow:hidden;}
472 html>body .tabular p {overflow:hidden;}
473
473
474 .tabular label{
474 .tabular label{
475 font-weight: bold;
475 font-weight: bold;
476 float: left;
476 float: left;
477 text-align: right;
477 text-align: right;
478 /* width of left column */
478 /* width of left column */
479 margin-left: -180px;
479 margin-left: -180px;
480 /* width of labels. Should be smaller than left column to create some right margin */
480 /* width of labels. Should be smaller than left column to create some right margin */
481 width: 175px;
481 width: 175px;
482 }
482 }
483
483
484 .tabular label.floating{
484 .tabular label.floating{
485 font-weight: normal;
485 font-weight: normal;
486 margin-left: 0px;
486 margin-left: 0px;
487 text-align: left;
487 text-align: left;
488 width: 270px;
488 width: 270px;
489 }
489 }
490
490
491 .tabular label.block{
491 .tabular label.block{
492 font-weight: normal;
492 font-weight: normal;
493 margin-left: 0px !important;
493 margin-left: 0px !important;
494 text-align: left;
494 text-align: left;
495 float: none;
495 float: none;
496 display: block;
496 display: block;
497 width: auto;
497 width: auto;
498 }
498 }
499
499
500 .tabular label.inline{
500 .tabular label.inline{
501 font-weight: normal;
501 font-weight: normal;
502 float:none;
502 float:none;
503 margin-left: 5px !important;
503 margin-left: 5px !important;
504 width: auto;
504 width: auto;
505 }
505 }
506
506
507 label.no-css {
507 label.no-css {
508 font-weight: inherit;
508 font-weight: inherit;
509 float:none;
509 float:none;
510 text-align:left;
510 text-align:left;
511 margin-left:0px;
511 margin-left:0px;
512 width:auto;
512 width:auto;
513 }
513 }
514 input#time_entry_comments { width: 90%;}
514 input#time_entry_comments { width: 90%;}
515
515
516 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
516 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
517
517
518 .tabular.settings p{ padding-left: 300px; }
518 .tabular.settings p{ padding-left: 300px; }
519 .tabular.settings label{ margin-left: -300px; width: 295px; }
519 .tabular.settings label{ margin-left: -300px; width: 295px; }
520 .tabular.settings textarea { width: 99%; }
520 .tabular.settings textarea { width: 99%; }
521
521
522 .settings.enabled_scm table {width:100%}
522 .settings.enabled_scm table {width:100%}
523 .settings.enabled_scm td.scm_name{ font-weight: bold; }
523 .settings.enabled_scm td.scm_name{ font-weight: bold; }
524
524
525 fieldset.settings label { display: block; }
525 fieldset.settings label { display: block; }
526 fieldset#notified_events .parent { padding-left: 20px; }
526 fieldset#notified_events .parent { padding-left: 20px; }
527
527
528 span.required {color: #bb0000;}
528 span.required {color: #bb0000;}
529 .summary {font-style: italic;}
529 .summary {font-style: italic;}
530
530
531 #attachments_fields input.description {margin-left:4px; width:340px;}
531 #attachments_fields input.description {margin-left:4px; width:340px;}
532 #attachments_fields span {display:block; white-space:nowrap;}
532 #attachments_fields span {display:block; white-space:nowrap;}
533 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
533 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
534 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
534 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
535 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
535 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
536 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
536 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
537 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
537 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
538 a.remove-upload:hover {text-decoration:none !important;}
538 a.remove-upload:hover {text-decoration:none !important;}
539
539
540 div.fileover { background-color: lavender; }
540 div.fileover { background-color: lavender; }
541
541
542 div.attachments { margin-top: 12px; }
542 div.attachments { margin-top: 12px; }
543 div.attachments p { margin:4px 0 2px 0; }
543 div.attachments p { margin:4px 0 2px 0; }
544 div.attachments img { vertical-align: middle; }
544 div.attachments img { vertical-align: middle; }
545 div.attachments span.author { font-size: 0.9em; color: #888; }
545 div.attachments span.author { font-size: 0.9em; color: #888; }
546
546
547 div.thumbnails {margin-top:0.6em;}
547 div.thumbnails {margin-top:0.6em;}
548 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
548 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
549 div.thumbnails img {margin: 3px;}
549 div.thumbnails img {margin: 3px;}
550
550
551 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
551 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
552 .other-formats span + span:before { content: "| "; }
552 .other-formats span + span:before { content: "| "; }
553
553
554 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
554 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
555
555
556 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
556 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
557 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
557 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
558
558
559 textarea.text_cf {width:90%;}
559 textarea.text_cf {width:90%;}
560
560
561 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
561 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
562
562
563 #tab-content-members .splitcontentleft, #tab-content-memberships .splitcontentleft, #tab-content-users .splitcontentleft {width: 64%;}
563 #tab-content-members .splitcontentleft, #tab-content-memberships .splitcontentleft, #tab-content-users .splitcontentleft {width: 64%;}
564 #tab-content-members .splitcontentright, #tab-content-memberships .splitcontentright, #tab-content-users .splitcontentright {width: 34%;}
564 #tab-content-members .splitcontentright, #tab-content-memberships .splitcontentright, #tab-content-users .splitcontentright {width: 34%;}
565 #tab-content-members fieldset, #tab-content-memberships fieldset, #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
565 #tab-content-members fieldset, #tab-content-memberships fieldset, #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
566 #tab-content-members fieldset legend, #tab-content-memberships fieldset legend, #tab-content-users fieldset legend {font-weight: bold;}
566 #tab-content-members fieldset legend, #tab-content-memberships fieldset legend, #tab-content-users fieldset legend {font-weight: bold;}
567 #tab-content-members fieldset label, #tab-content-memberships fieldset label, #tab-content-users fieldset label {display: block;}
567 #tab-content-members fieldset label, #tab-content-memberships fieldset label, #tab-content-users fieldset label {display: block;}
568 #tab-content-members #principals, #tab-content-users #principals {max-height: 400px; overflow: auto;}
568 #tab-content-members #principals, #tab-content-users #principals {max-height: 400px; overflow: auto;}
569
569
570 #tab-content-memberships .splitcontentright select {width:90%}
570 #tab-content-memberships .splitcontentright select {width:90%}
571
571
572 #users_for_watcher {height: 200px; overflow:auto;}
572 #users_for_watcher {height: 200px; overflow:auto;}
573 #users_for_watcher label {display: block;}
573 #users_for_watcher label {display: block;}
574
574
575 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
575 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
576
576
577 input#principal_search, input#user_search {width:90%}
577 input#principal_search, input#user_search {width:90%}
578
578
579 input.autocomplete {
579 input.autocomplete {
580 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
580 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
581 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
581 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
582 }
582 }
583 input.autocomplete.ajax-loading {
583 input.autocomplete.ajax-loading {
584 background-image: url(../images/loading.gif);
584 background-image: url(../images/loading.gif);
585 }
585 }
586
586
587 .role-visibility {padding-left:2em;}
588
587 /***** Flash & error messages ****/
589 /***** Flash & error messages ****/
588 #errorExplanation, div.flash, .nodata, .warning, .conflict {
590 #errorExplanation, div.flash, .nodata, .warning, .conflict {
589 padding: 4px 4px 4px 30px;
591 padding: 4px 4px 4px 30px;
590 margin-bottom: 12px;
592 margin-bottom: 12px;
591 font-size: 1.1em;
593 font-size: 1.1em;
592 border: 2px solid;
594 border: 2px solid;
593 }
595 }
594
596
595 div.flash {margin-top: 8px;}
597 div.flash {margin-top: 8px;}
596
598
597 div.flash.error, #errorExplanation {
599 div.flash.error, #errorExplanation {
598 background: url(../images/exclamation.png) 8px 50% no-repeat;
600 background: url(../images/exclamation.png) 8px 50% no-repeat;
599 background-color: #ffe3e3;
601 background-color: #ffe3e3;
600 border-color: #dd0000;
602 border-color: #dd0000;
601 color: #880000;
603 color: #880000;
602 }
604 }
603
605
604 div.flash.notice {
606 div.flash.notice {
605 background: url(../images/true.png) 8px 5px no-repeat;
607 background: url(../images/true.png) 8px 5px no-repeat;
606 background-color: #dfffdf;
608 background-color: #dfffdf;
607 border-color: #9fcf9f;
609 border-color: #9fcf9f;
608 color: #005f00;
610 color: #005f00;
609 }
611 }
610
612
611 div.flash.warning, .conflict {
613 div.flash.warning, .conflict {
612 background: url(../images/warning.png) 8px 5px no-repeat;
614 background: url(../images/warning.png) 8px 5px no-repeat;
613 background-color: #FFEBC1;
615 background-color: #FFEBC1;
614 border-color: #FDBF3B;
616 border-color: #FDBF3B;
615 color: #A6750C;
617 color: #A6750C;
616 text-align: left;
618 text-align: left;
617 }
619 }
618
620
619 .nodata, .warning {
621 .nodata, .warning {
620 text-align: center;
622 text-align: center;
621 background-color: #FFEBC1;
623 background-color: #FFEBC1;
622 border-color: #FDBF3B;
624 border-color: #FDBF3B;
623 color: #A6750C;
625 color: #A6750C;
624 }
626 }
625
627
626 #errorExplanation ul { font-size: 0.9em;}
628 #errorExplanation ul { font-size: 0.9em;}
627 #errorExplanation h2, #errorExplanation p { display: none; }
629 #errorExplanation h2, #errorExplanation p { display: none; }
628
630
629 .conflict-details {font-size:80%;}
631 .conflict-details {font-size:80%;}
630
632
631 /***** Ajax indicator ******/
633 /***** Ajax indicator ******/
632 #ajax-indicator {
634 #ajax-indicator {
633 position: absolute; /* fixed not supported by IE */
635 position: absolute; /* fixed not supported by IE */
634 background-color:#eee;
636 background-color:#eee;
635 border: 1px solid #bbb;
637 border: 1px solid #bbb;
636 top:35%;
638 top:35%;
637 left:40%;
639 left:40%;
638 width:20%;
640 width:20%;
639 font-weight:bold;
641 font-weight:bold;
640 text-align:center;
642 text-align:center;
641 padding:0.6em;
643 padding:0.6em;
642 z-index:100;
644 z-index:100;
643 opacity: 0.5;
645 opacity: 0.5;
644 }
646 }
645
647
646 html>body #ajax-indicator { position: fixed; }
648 html>body #ajax-indicator { position: fixed; }
647
649
648 #ajax-indicator span {
650 #ajax-indicator span {
649 background-position: 0% 40%;
651 background-position: 0% 40%;
650 background-repeat: no-repeat;
652 background-repeat: no-repeat;
651 background-image: url(../images/loading.gif);
653 background-image: url(../images/loading.gif);
652 padding-left: 26px;
654 padding-left: 26px;
653 vertical-align: bottom;
655 vertical-align: bottom;
654 }
656 }
655
657
656 /***** Calendar *****/
658 /***** Calendar *****/
657 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
659 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
658 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
660 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
659 table.cal thead th.week-number {width: auto;}
661 table.cal thead th.week-number {width: auto;}
660 table.cal tbody tr {height: 100px;}
662 table.cal tbody tr {height: 100px;}
661 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
663 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
662 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
664 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
663 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
665 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
664 table.cal td.odd p.day-num {color: #bbb;}
666 table.cal td.odd p.day-num {color: #bbb;}
665 table.cal td.today {background:#ffffdd;}
667 table.cal td.today {background:#ffffdd;}
666 table.cal td.today p.day-num {font-weight: bold;}
668 table.cal td.today p.day-num {font-weight: bold;}
667 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
669 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
668 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
670 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
669 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
671 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
670 p.cal.legend span {display:block;}
672 p.cal.legend span {display:block;}
671
673
672 /***** Tooltips ******/
674 /***** Tooltips ******/
673 .tooltip{position:relative;z-index:24;}
675 .tooltip{position:relative;z-index:24;}
674 .tooltip:hover{z-index:25;color:#000;}
676 .tooltip:hover{z-index:25;color:#000;}
675 .tooltip span.tip{display: none; text-align:left;}
677 .tooltip span.tip{display: none; text-align:left;}
676
678
677 div.tooltip:hover span.tip{
679 div.tooltip:hover span.tip{
678 display:block;
680 display:block;
679 position:absolute;
681 position:absolute;
680 top:12px; left:24px; width:270px;
682 top:12px; left:24px; width:270px;
681 border:1px solid #555;
683 border:1px solid #555;
682 background-color:#fff;
684 background-color:#fff;
683 padding: 4px;
685 padding: 4px;
684 font-size: 0.8em;
686 font-size: 0.8em;
685 color:#505050;
687 color:#505050;
686 }
688 }
687
689
688 img.ui-datepicker-trigger {
690 img.ui-datepicker-trigger {
689 cursor: pointer;
691 cursor: pointer;
690 vertical-align: middle;
692 vertical-align: middle;
691 margin-left: 4px;
693 margin-left: 4px;
692 }
694 }
693
695
694 /***** Progress bar *****/
696 /***** Progress bar *****/
695 table.progress {
697 table.progress {
696 border-collapse: collapse;
698 border-collapse: collapse;
697 border-spacing: 0pt;
699 border-spacing: 0pt;
698 empty-cells: show;
700 empty-cells: show;
699 text-align: center;
701 text-align: center;
700 float:left;
702 float:left;
701 margin: 1px 6px 1px 0px;
703 margin: 1px 6px 1px 0px;
702 }
704 }
703
705
704 table.progress td { height: 1em; }
706 table.progress td { height: 1em; }
705 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
707 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
706 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
708 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
707 table.progress td.todo { background: #eee none repeat scroll 0%; }
709 table.progress td.todo { background: #eee none repeat scroll 0%; }
708 p.percent {font-size: 80%;}
710 p.percent {font-size: 80%;}
709 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
711 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
710
712
711 #roadmap table.progress td { height: 1.2em; }
713 #roadmap table.progress td { height: 1.2em; }
712 /***** Tabs *****/
714 /***** Tabs *****/
713 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
715 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
714 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
716 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
715 #content .tabs ul li {
717 #content .tabs ul li {
716 float:left;
718 float:left;
717 list-style-type:none;
719 list-style-type:none;
718 white-space:nowrap;
720 white-space:nowrap;
719 margin-right:4px;
721 margin-right:4px;
720 background:#fff;
722 background:#fff;
721 position:relative;
723 position:relative;
722 margin-bottom:-1px;
724 margin-bottom:-1px;
723 }
725 }
724 #content .tabs ul li a{
726 #content .tabs ul li a{
725 display:block;
727 display:block;
726 font-size: 0.9em;
728 font-size: 0.9em;
727 text-decoration:none;
729 text-decoration:none;
728 line-height:1.3em;
730 line-height:1.3em;
729 padding:4px 6px 4px 6px;
731 padding:4px 6px 4px 6px;
730 border: 1px solid #ccc;
732 border: 1px solid #ccc;
731 border-bottom: 1px solid #bbbbbb;
733 border-bottom: 1px solid #bbbbbb;
732 background-color: #f6f6f6;
734 background-color: #f6f6f6;
733 color:#999;
735 color:#999;
734 font-weight:bold;
736 font-weight:bold;
735 border-top-left-radius:3px;
737 border-top-left-radius:3px;
736 border-top-right-radius:3px;
738 border-top-right-radius:3px;
737 }
739 }
738
740
739 #content .tabs ul li a:hover {
741 #content .tabs ul li a:hover {
740 background-color: #ffffdd;
742 background-color: #ffffdd;
741 text-decoration:none;
743 text-decoration:none;
742 }
744 }
743
745
744 #content .tabs ul li a.selected {
746 #content .tabs ul li a.selected {
745 background-color: #fff;
747 background-color: #fff;
746 border: 1px solid #bbbbbb;
748 border: 1px solid #bbbbbb;
747 border-bottom: 1px solid #fff;
749 border-bottom: 1px solid #fff;
748 color:#444;
750 color:#444;
749 }
751 }
750
752
751 #content .tabs ul li a.selected:hover {background-color: #fff;}
753 #content .tabs ul li a.selected:hover {background-color: #fff;}
752
754
753 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
755 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
754
756
755 button.tab-left, button.tab-right {
757 button.tab-left, button.tab-right {
756 font-size: 0.9em;
758 font-size: 0.9em;
757 cursor: pointer;
759 cursor: pointer;
758 height:24px;
760 height:24px;
759 border: 1px solid #ccc;
761 border: 1px solid #ccc;
760 border-bottom: 1px solid #bbbbbb;
762 border-bottom: 1px solid #bbbbbb;
761 position:absolute;
763 position:absolute;
762 padding:4px;
764 padding:4px;
763 width: 20px;
765 width: 20px;
764 bottom: -1px;
766 bottom: -1px;
765 }
767 }
766
768
767 button.tab-left {
769 button.tab-left {
768 right: 20px;
770 right: 20px;
769 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
771 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
770 border-top-left-radius:3px;
772 border-top-left-radius:3px;
771 }
773 }
772
774
773 button.tab-right {
775 button.tab-right {
774 right: 0;
776 right: 0;
775 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
777 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
776 border-top-right-radius:3px;
778 border-top-right-radius:3px;
777 }
779 }
778
780
779 /***** Diff *****/
781 /***** Diff *****/
780 .diff_out { background: #fcc; }
782 .diff_out { background: #fcc; }
781 .diff_out span { background: #faa; }
783 .diff_out span { background: #faa; }
782 .diff_in { background: #cfc; }
784 .diff_in { background: #cfc; }
783 .diff_in span { background: #afa; }
785 .diff_in span { background: #afa; }
784
786
785 .text-diff {
787 .text-diff {
786 padding: 1em;
788 padding: 1em;
787 background-color:#f6f6f6;
789 background-color:#f6f6f6;
788 color:#505050;
790 color:#505050;
789 border: 1px solid #e4e4e4;
791 border: 1px solid #e4e4e4;
790 }
792 }
791
793
792 /***** Wiki *****/
794 /***** Wiki *****/
793 div.wiki table {
795 div.wiki table {
794 border-collapse: collapse;
796 border-collapse: collapse;
795 margin-bottom: 1em;
797 margin-bottom: 1em;
796 }
798 }
797
799
798 div.wiki table, div.wiki td, div.wiki th {
800 div.wiki table, div.wiki td, div.wiki th {
799 border: 1px solid #bbb;
801 border: 1px solid #bbb;
800 padding: 4px;
802 padding: 4px;
801 }
803 }
802
804
803 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
805 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
804
806
805 div.wiki .external {
807 div.wiki .external {
806 background-position: 0% 60%;
808 background-position: 0% 60%;
807 background-repeat: no-repeat;
809 background-repeat: no-repeat;
808 padding-left: 12px;
810 padding-left: 12px;
809 background-image: url(../images/external.png);
811 background-image: url(../images/external.png);
810 }
812 }
811
813
812 div.wiki a.new {color: #b73535;}
814 div.wiki a.new {color: #b73535;}
813
815
814 div.wiki ul, div.wiki ol {margin-bottom:1em;}
816 div.wiki ul, div.wiki ol {margin-bottom:1em;}
815
817
816 div.wiki pre {
818 div.wiki pre {
817 margin: 1em 1em 1em 1.6em;
819 margin: 1em 1em 1em 1.6em;
818 padding: 8px;
820 padding: 8px;
819 background-color: #fafafa;
821 background-color: #fafafa;
820 border: 1px solid #e2e2e2;
822 border: 1px solid #e2e2e2;
821 width:auto;
823 width:auto;
822 overflow-x: auto;
824 overflow-x: auto;
823 overflow-y: hidden;
825 overflow-y: hidden;
824 }
826 }
825
827
826 div.wiki ul.toc {
828 div.wiki ul.toc {
827 background-color: #ffffdd;
829 background-color: #ffffdd;
828 border: 1px solid #e4e4e4;
830 border: 1px solid #e4e4e4;
829 padding: 4px;
831 padding: 4px;
830 line-height: 1.2em;
832 line-height: 1.2em;
831 margin-bottom: 12px;
833 margin-bottom: 12px;
832 margin-right: 12px;
834 margin-right: 12px;
833 margin-left: 0;
835 margin-left: 0;
834 display: table
836 display: table
835 }
837 }
836 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
838 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
837
839
838 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
840 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
839 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
841 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
840 div.wiki ul.toc ul { margin: 0; padding: 0; }
842 div.wiki ul.toc ul { margin: 0; padding: 0; }
841 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
843 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
842 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
844 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
843 div.wiki ul.toc a {
845 div.wiki ul.toc a {
844 font-size: 0.9em;
846 font-size: 0.9em;
845 font-weight: normal;
847 font-weight: normal;
846 text-decoration: none;
848 text-decoration: none;
847 color: #606060;
849 color: #606060;
848 }
850 }
849 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
851 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
850
852
851 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
853 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
852 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
854 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
853 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
855 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
854
856
855 div.wiki img { vertical-align: middle; }
857 div.wiki img { vertical-align: middle; }
856
858
857 /***** My page layout *****/
859 /***** My page layout *****/
858 .block-receiver {
860 .block-receiver {
859 border:1px dashed #c0c0c0;
861 border:1px dashed #c0c0c0;
860 margin-bottom: 20px;
862 margin-bottom: 20px;
861 padding: 15px 0 15px 0;
863 padding: 15px 0 15px 0;
862 }
864 }
863
865
864 .mypage-box {
866 .mypage-box {
865 margin:0 0 20px 0;
867 margin:0 0 20px 0;
866 color:#505050;
868 color:#505050;
867 line-height:1.5em;
869 line-height:1.5em;
868 }
870 }
869
871
870 .handle {cursor: move;}
872 .handle {cursor: move;}
871
873
872 a.close-icon {
874 a.close-icon {
873 display:block;
875 display:block;
874 margin-top:3px;
876 margin-top:3px;
875 overflow:hidden;
877 overflow:hidden;
876 width:12px;
878 width:12px;
877 height:12px;
879 height:12px;
878 background-repeat: no-repeat;
880 background-repeat: no-repeat;
879 cursor:pointer;
881 cursor:pointer;
880 background-image:url('../images/close.png');
882 background-image:url('../images/close.png');
881 }
883 }
882 a.close-icon:hover {background-image:url('../images/close_hl.png');}
884 a.close-icon:hover {background-image:url('../images/close_hl.png');}
883
885
884 /***** Gantt chart *****/
886 /***** Gantt chart *****/
885 .gantt_hdr {
887 .gantt_hdr {
886 position:absolute;
888 position:absolute;
887 top:0;
889 top:0;
888 height:16px;
890 height:16px;
889 border-top: 1px solid #c0c0c0;
891 border-top: 1px solid #c0c0c0;
890 border-bottom: 1px solid #c0c0c0;
892 border-bottom: 1px solid #c0c0c0;
891 border-right: 1px solid #c0c0c0;
893 border-right: 1px solid #c0c0c0;
892 text-align: center;
894 text-align: center;
893 overflow: hidden;
895 overflow: hidden;
894 }
896 }
895
897
896 .gantt_hdr.nwday {background-color:#f1f1f1;}
898 .gantt_hdr.nwday {background-color:#f1f1f1;}
897
899
898 .gantt_subjects { font-size: 0.8em; }
900 .gantt_subjects { font-size: 0.8em; }
899 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
901 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
900
902
901 .task {
903 .task {
902 position: absolute;
904 position: absolute;
903 height:8px;
905 height:8px;
904 font-size:0.8em;
906 font-size:0.8em;
905 color:#888;
907 color:#888;
906 padding:0;
908 padding:0;
907 margin:0;
909 margin:0;
908 line-height:16px;
910 line-height:16px;
909 white-space:nowrap;
911 white-space:nowrap;
910 }
912 }
911
913
912 .task.label {width:100%;}
914 .task.label {width:100%;}
913 .task.label.project, .task.label.version { font-weight: bold; }
915 .task.label.project, .task.label.version { font-weight: bold; }
914
916
915 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
917 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
916 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
918 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
917 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
919 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
918
920
919 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
921 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
920 .task_late.parent, .task_done.parent { height: 3px;}
922 .task_late.parent, .task_done.parent { height: 3px;}
921 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
923 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
922 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
924 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
923
925
924 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
926 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
925 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
927 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
926 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
928 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
927 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
929 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
928
930
929 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
931 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
930 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
932 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
931 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
933 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
932 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
934 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
933
935
934 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
936 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
935 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
937 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
936
938
937 /***** Icons *****/
939 /***** Icons *****/
938 .icon {
940 .icon {
939 background-position: 0% 50%;
941 background-position: 0% 50%;
940 background-repeat: no-repeat;
942 background-repeat: no-repeat;
941 padding-left: 20px;
943 padding-left: 20px;
942 padding-top: 2px;
944 padding-top: 2px;
943 padding-bottom: 3px;
945 padding-bottom: 3px;
944 }
946 }
945
947
946 .icon-add { background-image: url(../images/add.png); }
948 .icon-add { background-image: url(../images/add.png); }
947 .icon-edit { background-image: url(../images/edit.png); }
949 .icon-edit { background-image: url(../images/edit.png); }
948 .icon-copy { background-image: url(../images/copy.png); }
950 .icon-copy { background-image: url(../images/copy.png); }
949 .icon-duplicate { background-image: url(../images/duplicate.png); }
951 .icon-duplicate { background-image: url(../images/duplicate.png); }
950 .icon-del { background-image: url(../images/delete.png); }
952 .icon-del { background-image: url(../images/delete.png); }
951 .icon-move { background-image: url(../images/move.png); }
953 .icon-move { background-image: url(../images/move.png); }
952 .icon-save { background-image: url(../images/save.png); }
954 .icon-save { background-image: url(../images/save.png); }
953 .icon-cancel { background-image: url(../images/cancel.png); }
955 .icon-cancel { background-image: url(../images/cancel.png); }
954 .icon-multiple { background-image: url(../images/table_multiple.png); }
956 .icon-multiple { background-image: url(../images/table_multiple.png); }
955 .icon-folder { background-image: url(../images/folder.png); }
957 .icon-folder { background-image: url(../images/folder.png); }
956 .open .icon-folder { background-image: url(../images/folder_open.png); }
958 .open .icon-folder { background-image: url(../images/folder_open.png); }
957 .icon-package { background-image: url(../images/package.png); }
959 .icon-package { background-image: url(../images/package.png); }
958 .icon-user { background-image: url(../images/user.png); }
960 .icon-user { background-image: url(../images/user.png); }
959 .icon-projects { background-image: url(../images/projects.png); }
961 .icon-projects { background-image: url(../images/projects.png); }
960 .icon-help { background-image: url(../images/help.png); }
962 .icon-help { background-image: url(../images/help.png); }
961 .icon-attachment { background-image: url(../images/attachment.png); }
963 .icon-attachment { background-image: url(../images/attachment.png); }
962 .icon-history { background-image: url(../images/history.png); }
964 .icon-history { background-image: url(../images/history.png); }
963 .icon-time { background-image: url(../images/time.png); }
965 .icon-time { background-image: url(../images/time.png); }
964 .icon-time-add { background-image: url(../images/time_add.png); }
966 .icon-time-add { background-image: url(../images/time_add.png); }
965 .icon-stats { background-image: url(../images/stats.png); }
967 .icon-stats { background-image: url(../images/stats.png); }
966 .icon-warning { background-image: url(../images/warning.png); }
968 .icon-warning { background-image: url(../images/warning.png); }
967 .icon-fav { background-image: url(../images/fav.png); }
969 .icon-fav { background-image: url(../images/fav.png); }
968 .icon-fav-off { background-image: url(../images/fav_off.png); }
970 .icon-fav-off { background-image: url(../images/fav_off.png); }
969 .icon-reload { background-image: url(../images/reload.png); }
971 .icon-reload { background-image: url(../images/reload.png); }
970 .icon-lock { background-image: url(../images/locked.png); }
972 .icon-lock { background-image: url(../images/locked.png); }
971 .icon-unlock { background-image: url(../images/unlock.png); }
973 .icon-unlock { background-image: url(../images/unlock.png); }
972 .icon-checked { background-image: url(../images/true.png); }
974 .icon-checked { background-image: url(../images/true.png); }
973 .icon-details { background-image: url(../images/zoom_in.png); }
975 .icon-details { background-image: url(../images/zoom_in.png); }
974 .icon-report { background-image: url(../images/report.png); }
976 .icon-report { background-image: url(../images/report.png); }
975 .icon-comment { background-image: url(../images/comment.png); }
977 .icon-comment { background-image: url(../images/comment.png); }
976 .icon-summary { background-image: url(../images/lightning.png); }
978 .icon-summary { background-image: url(../images/lightning.png); }
977 .icon-server-authentication { background-image: url(../images/server_key.png); }
979 .icon-server-authentication { background-image: url(../images/server_key.png); }
978 .icon-issue { background-image: url(../images/ticket.png); }
980 .icon-issue { background-image: url(../images/ticket.png); }
979 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
981 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
980 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
982 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
981 .icon-passwd { background-image: url(../images/textfield_key.png); }
983 .icon-passwd { background-image: url(../images/textfield_key.png); }
982 .icon-test { background-image: url(../images/bullet_go.png); }
984 .icon-test { background-image: url(../images/bullet_go.png); }
983
985
984 .icon-file { background-image: url(../images/files/default.png); }
986 .icon-file { background-image: url(../images/files/default.png); }
985 .icon-file.text-plain { background-image: url(../images/files/text.png); }
987 .icon-file.text-plain { background-image: url(../images/files/text.png); }
986 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
988 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
987 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
989 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
988 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
990 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
989 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
991 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
990 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
992 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
991 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
993 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
992 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
994 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
993 .icon-file.text-css { background-image: url(../images/files/css.png); }
995 .icon-file.text-css { background-image: url(../images/files/css.png); }
994 .icon-file.text-html { background-image: url(../images/files/html.png); }
996 .icon-file.text-html { background-image: url(../images/files/html.png); }
995 .icon-file.image-gif { background-image: url(../images/files/image.png); }
997 .icon-file.image-gif { background-image: url(../images/files/image.png); }
996 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
998 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
997 .icon-file.image-png { background-image: url(../images/files/image.png); }
999 .icon-file.image-png { background-image: url(../images/files/image.png); }
998 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1000 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
999 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1001 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1000 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1002 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1001 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1003 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1002
1004
1003 img.gravatar {
1005 img.gravatar {
1004 padding: 2px;
1006 padding: 2px;
1005 border: solid 1px #d5d5d5;
1007 border: solid 1px #d5d5d5;
1006 background: #fff;
1008 background: #fff;
1007 vertical-align: middle;
1009 vertical-align: middle;
1008 }
1010 }
1009
1011
1010 div.issue img.gravatar {
1012 div.issue img.gravatar {
1011 float: left;
1013 float: left;
1012 margin: 0 6px 0 0;
1014 margin: 0 6px 0 0;
1013 padding: 5px;
1015 padding: 5px;
1014 }
1016 }
1015
1017
1016 div.issue table img.gravatar {
1018 div.issue table img.gravatar {
1017 height: 14px;
1019 height: 14px;
1018 width: 14px;
1020 width: 14px;
1019 padding: 2px;
1021 padding: 2px;
1020 float: left;
1022 float: left;
1021 margin: 0 0.5em 0 0;
1023 margin: 0 0.5em 0 0;
1022 }
1024 }
1023
1025
1024 h2 img.gravatar {margin: -2px 4px -4px 0;}
1026 h2 img.gravatar {margin: -2px 4px -4px 0;}
1025 h3 img.gravatar {margin: -4px 4px -4px 0;}
1027 h3 img.gravatar {margin: -4px 4px -4px 0;}
1026 h4 img.gravatar {margin: -6px 4px -4px 0;}
1028 h4 img.gravatar {margin: -6px 4px -4px 0;}
1027 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1029 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1028 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1030 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1029 /* Used on 12px Gravatar img tags without the icon background */
1031 /* Used on 12px Gravatar img tags without the icon background */
1030 .icon-gravatar {float: left; margin-right: 4px;}
1032 .icon-gravatar {float: left; margin-right: 4px;}
1031
1033
1032 #activity dt, .journal {clear: left;}
1034 #activity dt, .journal {clear: left;}
1033
1035
1034 .journal-link {float: right;}
1036 .journal-link {float: right;}
1035
1037
1036 h2 img { vertical-align:middle; }
1038 h2 img { vertical-align:middle; }
1037
1039
1038 .hascontextmenu { cursor: context-menu; }
1040 .hascontextmenu { cursor: context-menu; }
1039
1041
1040 /* Custom JQuery styles */
1042 /* Custom JQuery styles */
1041 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1043 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1042
1044
1043
1045
1044 /************* CodeRay styles *************/
1046 /************* CodeRay styles *************/
1045 .syntaxhl div {display: inline;}
1047 .syntaxhl div {display: inline;}
1046 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1048 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1047 .syntaxhl .code pre { overflow: auto }
1049 .syntaxhl .code pre { overflow: auto }
1048 .syntaxhl .debug { color: white !important; background: blue !important; }
1050 .syntaxhl .debug { color: white !important; background: blue !important; }
1049
1051
1050 .syntaxhl .annotation { color:#007 }
1052 .syntaxhl .annotation { color:#007 }
1051 .syntaxhl .attribute-name { color:#b48 }
1053 .syntaxhl .attribute-name { color:#b48 }
1052 .syntaxhl .attribute-value { color:#700 }
1054 .syntaxhl .attribute-value { color:#700 }
1053 .syntaxhl .binary { color:#509 }
1055 .syntaxhl .binary { color:#509 }
1054 .syntaxhl .char .content { color:#D20 }
1056 .syntaxhl .char .content { color:#D20 }
1055 .syntaxhl .char .delimiter { color:#710 }
1057 .syntaxhl .char .delimiter { color:#710 }
1056 .syntaxhl .char { color:#D20 }
1058 .syntaxhl .char { color:#D20 }
1057 .syntaxhl .class { color:#258; font-weight:bold }
1059 .syntaxhl .class { color:#258; font-weight:bold }
1058 .syntaxhl .class-variable { color:#369 }
1060 .syntaxhl .class-variable { color:#369 }
1059 .syntaxhl .color { color:#0A0 }
1061 .syntaxhl .color { color:#0A0 }
1060 .syntaxhl .comment { color:#385 }
1062 .syntaxhl .comment { color:#385 }
1061 .syntaxhl .comment .char { color:#385 }
1063 .syntaxhl .comment .char { color:#385 }
1062 .syntaxhl .comment .delimiter { color:#385 }
1064 .syntaxhl .comment .delimiter { color:#385 }
1063 .syntaxhl .complex { color:#A08 }
1065 .syntaxhl .complex { color:#A08 }
1064 .syntaxhl .constant { color:#258; font-weight:bold }
1066 .syntaxhl .constant { color:#258; font-weight:bold }
1065 .syntaxhl .decorator { color:#B0B }
1067 .syntaxhl .decorator { color:#B0B }
1066 .syntaxhl .definition { color:#099; font-weight:bold }
1068 .syntaxhl .definition { color:#099; font-weight:bold }
1067 .syntaxhl .delimiter { color:black }
1069 .syntaxhl .delimiter { color:black }
1068 .syntaxhl .directive { color:#088; font-weight:bold }
1070 .syntaxhl .directive { color:#088; font-weight:bold }
1069 .syntaxhl .doc { color:#970 }
1071 .syntaxhl .doc { color:#970 }
1070 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1072 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1071 .syntaxhl .doctype { color:#34b }
1073 .syntaxhl .doctype { color:#34b }
1072 .syntaxhl .entity { color:#800; font-weight:bold }
1074 .syntaxhl .entity { color:#800; font-weight:bold }
1073 .syntaxhl .error { color:#F00; background-color:#FAA }
1075 .syntaxhl .error { color:#F00; background-color:#FAA }
1074 .syntaxhl .escape { color:#666 }
1076 .syntaxhl .escape { color:#666 }
1075 .syntaxhl .exception { color:#C00; font-weight:bold }
1077 .syntaxhl .exception { color:#C00; font-weight:bold }
1076 .syntaxhl .float { color:#06D }
1078 .syntaxhl .float { color:#06D }
1077 .syntaxhl .function { color:#06B; font-weight:bold }
1079 .syntaxhl .function { color:#06B; font-weight:bold }
1078 .syntaxhl .global-variable { color:#d70 }
1080 .syntaxhl .global-variable { color:#d70 }
1079 .syntaxhl .hex { color:#02b }
1081 .syntaxhl .hex { color:#02b }
1080 .syntaxhl .imaginary { color:#f00 }
1082 .syntaxhl .imaginary { color:#f00 }
1081 .syntaxhl .include { color:#B44; font-weight:bold }
1083 .syntaxhl .include { color:#B44; font-weight:bold }
1082 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1084 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1083 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1085 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1084 .syntaxhl .instance-variable { color:#33B }
1086 .syntaxhl .instance-variable { color:#33B }
1085 .syntaxhl .integer { color:#06D }
1087 .syntaxhl .integer { color:#06D }
1086 .syntaxhl .key .char { color: #60f }
1088 .syntaxhl .key .char { color: #60f }
1087 .syntaxhl .key .delimiter { color: #404 }
1089 .syntaxhl .key .delimiter { color: #404 }
1088 .syntaxhl .key { color: #606 }
1090 .syntaxhl .key { color: #606 }
1089 .syntaxhl .keyword { color:#939; font-weight:bold }
1091 .syntaxhl .keyword { color:#939; font-weight:bold }
1090 .syntaxhl .label { color:#970; font-weight:bold }
1092 .syntaxhl .label { color:#970; font-weight:bold }
1091 .syntaxhl .local-variable { color:#963 }
1093 .syntaxhl .local-variable { color:#963 }
1092 .syntaxhl .namespace { color:#707; font-weight:bold }
1094 .syntaxhl .namespace { color:#707; font-weight:bold }
1093 .syntaxhl .octal { color:#40E }
1095 .syntaxhl .octal { color:#40E }
1094 .syntaxhl .operator { }
1096 .syntaxhl .operator { }
1095 .syntaxhl .predefined { color:#369; font-weight:bold }
1097 .syntaxhl .predefined { color:#369; font-weight:bold }
1096 .syntaxhl .predefined-constant { color:#069 }
1098 .syntaxhl .predefined-constant { color:#069 }
1097 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1099 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1098 .syntaxhl .preprocessor { color:#579 }
1100 .syntaxhl .preprocessor { color:#579 }
1099 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1101 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1100 .syntaxhl .regexp .content { color:#808 }
1102 .syntaxhl .regexp .content { color:#808 }
1101 .syntaxhl .regexp .delimiter { color:#404 }
1103 .syntaxhl .regexp .delimiter { color:#404 }
1102 .syntaxhl .regexp .modifier { color:#C2C }
1104 .syntaxhl .regexp .modifier { color:#C2C }
1103 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1105 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1104 .syntaxhl .reserved { color:#080; font-weight:bold }
1106 .syntaxhl .reserved { color:#080; font-weight:bold }
1105 .syntaxhl .shell .content { color:#2B2 }
1107 .syntaxhl .shell .content { color:#2B2 }
1106 .syntaxhl .shell .delimiter { color:#161 }
1108 .syntaxhl .shell .delimiter { color:#161 }
1107 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1109 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1108 .syntaxhl .string .char { color: #46a }
1110 .syntaxhl .string .char { color: #46a }
1109 .syntaxhl .string .content { color: #46a }
1111 .syntaxhl .string .content { color: #46a }
1110 .syntaxhl .string .delimiter { color: #46a }
1112 .syntaxhl .string .delimiter { color: #46a }
1111 .syntaxhl .string .modifier { color: #46a }
1113 .syntaxhl .string .modifier { color: #46a }
1112 .syntaxhl .symbol .content { color:#d33 }
1114 .syntaxhl .symbol .content { color:#d33 }
1113 .syntaxhl .symbol .delimiter { color:#d33 }
1115 .syntaxhl .symbol .delimiter { color:#d33 }
1114 .syntaxhl .symbol { color:#d33 }
1116 .syntaxhl .symbol { color:#d33 }
1115 .syntaxhl .tag { color:#070 }
1117 .syntaxhl .tag { color:#070 }
1116 .syntaxhl .type { color:#339; font-weight:bold }
1118 .syntaxhl .type { color:#339; font-weight:bold }
1117 .syntaxhl .value { color: #088; }
1119 .syntaxhl .value { color: #088; }
1118 .syntaxhl .variable { color:#037 }
1120 .syntaxhl .variable { color:#037 }
1119
1121
1120 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1122 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1121 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1123 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1122 .syntaxhl .change { color: #bbf; background: #007; }
1124 .syntaxhl .change { color: #bbf; background: #007; }
1123 .syntaxhl .head { color: #f8f; background: #505 }
1125 .syntaxhl .head { color: #f8f; background: #505 }
1124 .syntaxhl .head .filename { color: white; }
1126 .syntaxhl .head .filename { color: white; }
1125
1127
1126 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1128 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1127 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1129 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1128
1130
1129 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1131 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1130 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1132 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1131 .syntaxhl .change .change { color: #88f }
1133 .syntaxhl .change .change { color: #88f }
1132 .syntaxhl .head .head { color: #f4f }
1134 .syntaxhl .head .head { color: #f4f }
1133
1135
1134 /***** Media print specific styles *****/
1136 /***** Media print specific styles *****/
1135 @media print {
1137 @media print {
1136 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1138 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1137 #main { background: #fff; }
1139 #main { background: #fff; }
1138 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1140 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1139 #wiki_add_attachment { display:none; }
1141 #wiki_add_attachment { display:none; }
1140 .hide-when-print { display: none; }
1142 .hide-when-print { display: none; }
1141 .autoscroll {overflow-x: visible;}
1143 .autoscroll {overflow-x: visible;}
1142 table.list {margin-top:0.5em;}
1144 table.list {margin-top:0.5em;}
1143 table.list th, table.list td {border: 1px solid #aaa;}
1145 table.list th, table.list td {border: 1px solid #aaa;}
1144 }
1146 }
1145
1147
1146 /* Accessibility specific styles */
1148 /* Accessibility specific styles */
1147 .hidden-for-sighted {
1149 .hidden-for-sighted {
1148 position:absolute;
1150 position:absolute;
1149 left:-10000px;
1151 left:-10000px;
1150 top:auto;
1152 top:auto;
1151 width:1px;
1153 width:1px;
1152 height:1px;
1154 height:1px;
1153 overflow:hidden;
1155 overflow:hidden;
1154 }
1156 }
@@ -1,165 +1,165
1 ---
1 ---
2 queries_001:
2 queries_001:
3 id: 1
3 id: 1
4 type: IssueQuery
4 type: IssueQuery
5 project_id: 1
5 project_id: 1
6 is_public: true
6 visibility: 2
7 name: Multiple custom fields query
7 name: Multiple custom fields query
8 filters: |
8 filters: |
9 ---
9 ---
10 cf_1:
10 cf_1:
11 :values:
11 :values:
12 - MySQL
12 - MySQL
13 :operator: "="
13 :operator: "="
14 status_id:
14 status_id:
15 :values:
15 :values:
16 - "1"
16 - "1"
17 :operator: o
17 :operator: o
18 cf_2:
18 cf_2:
19 :values:
19 :values:
20 - "125"
20 - "125"
21 :operator: "="
21 :operator: "="
22
22
23 user_id: 1
23 user_id: 1
24 column_names:
24 column_names:
25 queries_002:
25 queries_002:
26 id: 2
26 id: 2
27 type: IssueQuery
27 type: IssueQuery
28 project_id: 1
28 project_id: 1
29 is_public: false
29 visibility: 0
30 name: Private query for cookbook
30 name: Private query for cookbook
31 filters: |
31 filters: |
32 ---
32 ---
33 tracker_id:
33 tracker_id:
34 :values:
34 :values:
35 - "3"
35 - "3"
36 :operator: "="
36 :operator: "="
37 status_id:
37 status_id:
38 :values:
38 :values:
39 - "1"
39 - "1"
40 :operator: o
40 :operator: o
41
41
42 user_id: 3
42 user_id: 3
43 column_names:
43 column_names:
44 queries_003:
44 queries_003:
45 id: 3
45 id: 3
46 type: IssueQuery
46 type: IssueQuery
47 project_id:
47 project_id:
48 is_public: false
48 visibility: 0
49 name: Private query for all projects
49 name: Private query for all projects
50 filters: |
50 filters: |
51 ---
51 ---
52 tracker_id:
52 tracker_id:
53 :values:
53 :values:
54 - "3"
54 - "3"
55 :operator: "="
55 :operator: "="
56
56
57 user_id: 3
57 user_id: 3
58 column_names:
58 column_names:
59 queries_004:
59 queries_004:
60 id: 4
60 id: 4
61 type: IssueQuery
61 type: IssueQuery
62 project_id:
62 project_id:
63 is_public: true
63 visibility: 2
64 name: Public query for all projects
64 name: Public query for all projects
65 filters: |
65 filters: |
66 ---
66 ---
67 tracker_id:
67 tracker_id:
68 :values:
68 :values:
69 - "3"
69 - "3"
70 :operator: "="
70 :operator: "="
71
71
72 user_id: 2
72 user_id: 2
73 column_names:
73 column_names:
74 queries_005:
74 queries_005:
75 id: 5
75 id: 5
76 type: IssueQuery
76 type: IssueQuery
77 project_id:
77 project_id:
78 is_public: true
78 visibility: 2
79 name: Open issues by priority and tracker
79 name: Open issues by priority and tracker
80 filters: |
80 filters: |
81 ---
81 ---
82 status_id:
82 status_id:
83 :values:
83 :values:
84 - "1"
84 - "1"
85 :operator: o
85 :operator: o
86
86
87 user_id: 1
87 user_id: 1
88 column_names:
88 column_names:
89 sort_criteria: |
89 sort_criteria: |
90 ---
90 ---
91 - - priority
91 - - priority
92 - desc
92 - desc
93 - - tracker
93 - - tracker
94 - asc
94 - asc
95 queries_006:
95 queries_006:
96 id: 6
96 id: 6
97 type: IssueQuery
97 type: IssueQuery
98 project_id:
98 project_id:
99 is_public: true
99 visibility: 2
100 name: Open issues grouped by tracker
100 name: Open issues grouped by tracker
101 filters: |
101 filters: |
102 ---
102 ---
103 status_id:
103 status_id:
104 :values:
104 :values:
105 - "1"
105 - "1"
106 :operator: o
106 :operator: o
107
107
108 user_id: 1
108 user_id: 1
109 column_names:
109 column_names:
110 group_by: tracker
110 group_by: tracker
111 sort_criteria: |
111 sort_criteria: |
112 ---
112 ---
113 - - priority
113 - - priority
114 - desc
114 - desc
115 queries_007:
115 queries_007:
116 id: 7
116 id: 7
117 type: IssueQuery
117 type: IssueQuery
118 project_id: 2
118 project_id: 2
119 is_public: true
119 visibility: 2
120 name: Public query for project 2
120 name: Public query for project 2
121 filters: |
121 filters: |
122 ---
122 ---
123 tracker_id:
123 tracker_id:
124 :values:
124 :values:
125 - "3"
125 - "3"
126 :operator: "="
126 :operator: "="
127
127
128 user_id: 2
128 user_id: 2
129 column_names:
129 column_names:
130 queries_008:
130 queries_008:
131 id: 8
131 id: 8
132 type: IssueQuery
132 type: IssueQuery
133 project_id: 2
133 project_id: 2
134 is_public: false
134 visibility: 0
135 name: Private query for project 2
135 name: Private query for project 2
136 filters: |
136 filters: |
137 ---
137 ---
138 tracker_id:
138 tracker_id:
139 :values:
139 :values:
140 - "3"
140 - "3"
141 :operator: "="
141 :operator: "="
142
142
143 user_id: 2
143 user_id: 2
144 column_names:
144 column_names:
145 queries_009:
145 queries_009:
146 id: 9
146 id: 9
147 type: IssueQuery
147 type: IssueQuery
148 project_id:
148 project_id:
149 is_public: true
149 visibility: 2
150 name: Open issues grouped by list custom field
150 name: Open issues grouped by list custom field
151 filters: |
151 filters: |
152 ---
152 ---
153 status_id:
153 status_id:
154 :values:
154 :values:
155 - "1"
155 - "1"
156 :operator: o
156 :operator: o
157
157
158 user_id: 1
158 user_id: 1
159 column_names:
159 column_names:
160 group_by: cf_1
160 group_by: cf_1
161 sort_criteria: |
161 sort_criteria: |
162 ---
162 ---
163 - - priority
163 - - priority
164 - desc
164 - desc
165
165
@@ -1,84 +1,84
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class CalendarsControllerTest < ActionController::TestCase
20 class CalendarsControllerTest < ActionController::TestCase
21 fixtures :projects,
21 fixtures :projects,
22 :trackers,
22 :trackers,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules
27 :enabled_modules
28
28
29 def test_show
29 def test_show
30 get :show, :project_id => 1
30 get :show, :project_id => 1
31 assert_response :success
31 assert_response :success
32 assert_template 'calendar'
32 assert_template 'calendar'
33 assert_not_nil assigns(:calendar)
33 assert_not_nil assigns(:calendar)
34 end
34 end
35
35
36 def test_show_should_run_custom_queries
36 def test_show_should_run_custom_queries
37 @query = IssueQuery.create!(:name => 'Calendar', :is_public => true)
37 @query = IssueQuery.create!(:name => 'Calendar', :visibility => IssueQuery::VISIBILITY_PUBLIC)
38
38
39 get :show, :query_id => @query.id
39 get :show, :query_id => @query.id
40 assert_response :success
40 assert_response :success
41 end
41 end
42
42
43 def test_cross_project_calendar
43 def test_cross_project_calendar
44 get :show
44 get :show
45 assert_response :success
45 assert_response :success
46 assert_template 'calendar'
46 assert_template 'calendar'
47 assert_not_nil assigns(:calendar)
47 assert_not_nil assigns(:calendar)
48 end
48 end
49
49
50 def test_week_number_calculation
50 def test_week_number_calculation
51 Setting.start_of_week = 7
51 Setting.start_of_week = 7
52
52
53 get :show, :month => '1', :year => '2010'
53 get :show, :month => '1', :year => '2010'
54 assert_response :success
54 assert_response :success
55
55
56 assert_select 'tr' do
56 assert_select 'tr' do
57 assert_select 'td.week-number', :text => '53'
57 assert_select 'td.week-number', :text => '53'
58 assert_select 'td.odd', :text => '27'
58 assert_select 'td.odd', :text => '27'
59 assert_select 'td.even', :text => '2'
59 assert_select 'td.even', :text => '2'
60 end
60 end
61
61
62 assert_select 'tr' do
62 assert_select 'tr' do
63 assert_select 'td.week-number', :text => '1'
63 assert_select 'td.week-number', :text => '1'
64 assert_select 'td.odd', :text => '3'
64 assert_select 'td.odd', :text => '3'
65 assert_select 'td.even', :text => '9'
65 assert_select 'td.even', :text => '9'
66 end
66 end
67
67
68 Setting.start_of_week = 1
68 Setting.start_of_week = 1
69 get :show, :month => '1', :year => '2010'
69 get :show, :month => '1', :year => '2010'
70 assert_response :success
70 assert_response :success
71
71
72 assert_select 'tr' do
72 assert_select 'tr' do
73 assert_select 'td.week-number', :text => '53'
73 assert_select 'td.week-number', :text => '53'
74 assert_select 'td.even', :text => '28'
74 assert_select 'td.even', :text => '28'
75 assert_select 'td.even', :text => '3'
75 assert_select 'td.even', :text => '3'
76 end
76 end
77
77
78 assert_select 'tr' do
78 assert_select 'tr' do
79 assert_select 'td.week-number', :text => '1'
79 assert_select 'td.week-number', :text => '1'
80 assert_select 'td.even', :text => '4'
80 assert_select 'td.even', :text => '4'
81 assert_select 'td.even', :text => '10'
81 assert_select 'td.even', :text => '10'
82 end
82 end
83 end
83 end
84 end
84 end
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,290 +1,280
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueriesControllerTest < ActionController::TestCase
20 class QueriesControllerTest < ActionController::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
22
22
23 def setup
23 def setup
24 User.current = nil
24 User.current = nil
25 end
25 end
26
26
27 def test_index
27 def test_index
28 get :index
28 get :index
29 # HTML response not implemented
29 # HTML response not implemented
30 assert_response 406
30 assert_response 406
31 end
31 end
32
32
33 def test_new_project_query
33 def test_new_project_query
34 @request.session[:user_id] = 2
34 @request.session[:user_id] = 2
35 get :new, :project_id => 1
35 get :new, :project_id => 1
36 assert_response :success
36 assert_response :success
37 assert_template 'new'
37 assert_template 'new'
38 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
38 assert_select 'input[name=?][value=0][checked=checked]', 'query[visibility]'
39 :name => 'query[is_public]',
40 :checked => nil }
41 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
39 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
42 :name => 'query_is_for_all',
40 :name => 'query_is_for_all',
43 :checked => nil,
41 :checked => nil,
44 :disabled => nil }
42 :disabled => nil }
45 assert_select 'select[name=?]', 'c[]' do
43 assert_select 'select[name=?]', 'c[]' do
46 assert_select 'option[value=tracker]'
44 assert_select 'option[value=tracker]'
47 assert_select 'option[value=subject]'
45 assert_select 'option[value=subject]'
48 end
46 end
49 end
47 end
50
48
51 def test_new_global_query
49 def test_new_global_query
52 @request.session[:user_id] = 2
50 @request.session[:user_id] = 2
53 get :new
51 get :new
54 assert_response :success
52 assert_response :success
55 assert_template 'new'
53 assert_template 'new'
56 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
54 assert_select 'input[name=?]', 'query[visibility]', 0
57 :name => 'query[is_public]' }
58 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
55 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
59 :name => 'query_is_for_all',
56 :name => 'query_is_for_all',
60 :checked => 'checked',
57 :checked => 'checked',
61 :disabled => nil }
58 :disabled => nil }
62 end
59 end
63
60
64 def test_new_on_invalid_project
61 def test_new_on_invalid_project
65 @request.session[:user_id] = 2
62 @request.session[:user_id] = 2
66 get :new, :project_id => 'invalid'
63 get :new, :project_id => 'invalid'
67 assert_response 404
64 assert_response 404
68 end
65 end
69
66
70 def test_create_project_public_query
67 def test_create_project_public_query
71 @request.session[:user_id] = 2
68 @request.session[:user_id] = 2
72 post :create,
69 post :create,
73 :project_id => 'ecookbook',
70 :project_id => 'ecookbook',
74 :default_columns => '1',
71 :default_columns => '1',
75 :f => ["status_id", "assigned_to_id"],
72 :f => ["status_id", "assigned_to_id"],
76 :op => {"assigned_to_id" => "=", "status_id" => "o"},
73 :op => {"assigned_to_id" => "=", "status_id" => "o"},
77 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
74 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
78 :query => {"name" => "test_new_project_public_query", "is_public" => "1"}
75 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
79
76
80 q = Query.find_by_name('test_new_project_public_query')
77 q = Query.find_by_name('test_new_project_public_query')
81 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
78 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
82 assert q.is_public?
79 assert q.is_public?
83 assert q.has_default_columns?
80 assert q.has_default_columns?
84 assert q.valid?
81 assert q.valid?
85 end
82 end
86
83
87 def test_create_project_private_query
84 def test_create_project_private_query
88 @request.session[:user_id] = 3
85 @request.session[:user_id] = 3
89 post :create,
86 post :create,
90 :project_id => 'ecookbook',
87 :project_id => 'ecookbook',
91 :default_columns => '1',
88 :default_columns => '1',
92 :fields => ["status_id", "assigned_to_id"],
89 :fields => ["status_id", "assigned_to_id"],
93 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
90 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
94 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
91 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
95 :query => {"name" => "test_new_project_private_query", "is_public" => "1"}
92 :query => {"name" => "test_new_project_private_query", "visibility" => "2"}
96
93
97 q = Query.find_by_name('test_new_project_private_query')
94 q = Query.find_by_name('test_new_project_private_query')
98 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
95 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
99 assert !q.is_public?
96 assert !q.is_public?
100 assert q.has_default_columns?
97 assert q.has_default_columns?
101 assert q.valid?
98 assert q.valid?
102 end
99 end
103
100
104 def test_create_global_private_query_with_custom_columns
101 def test_create_global_private_query_with_custom_columns
105 @request.session[:user_id] = 3
102 @request.session[:user_id] = 3
106 post :create,
103 post :create,
107 :fields => ["status_id", "assigned_to_id"],
104 :fields => ["status_id", "assigned_to_id"],
108 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
105 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
109 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
106 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
110 :query => {"name" => "test_new_global_private_query", "is_public" => "1"},
107 :query => {"name" => "test_new_global_private_query", "visibility" => "2"},
111 :c => ["", "tracker", "subject", "priority", "category"]
108 :c => ["", "tracker", "subject", "priority", "category"]
112
109
113 q = Query.find_by_name('test_new_global_private_query')
110 q = Query.find_by_name('test_new_global_private_query')
114 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
111 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
115 assert !q.is_public?
112 assert !q.is_public?
116 assert !q.has_default_columns?
113 assert !q.has_default_columns?
117 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
114 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
118 assert q.valid?
115 assert q.valid?
119 end
116 end
120
117
121 def test_create_global_query_with_custom_filters
118 def test_create_global_query_with_custom_filters
122 @request.session[:user_id] = 3
119 @request.session[:user_id] = 3
123 post :create,
120 post :create,
124 :fields => ["assigned_to_id"],
121 :fields => ["assigned_to_id"],
125 :operators => {"assigned_to_id" => "="},
122 :operators => {"assigned_to_id" => "="},
126 :values => { "assigned_to_id" => ["me"]},
123 :values => { "assigned_to_id" => ["me"]},
127 :query => {"name" => "test_new_global_query"}
124 :query => {"name" => "test_new_global_query"}
128
125
129 q = Query.find_by_name('test_new_global_query')
126 q = Query.find_by_name('test_new_global_query')
130 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
127 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
131 assert !q.has_filter?(:status_id)
128 assert !q.has_filter?(:status_id)
132 assert_equal ['assigned_to_id'], q.filters.keys
129 assert_equal ['assigned_to_id'], q.filters.keys
133 assert q.valid?
130 assert q.valid?
134 end
131 end
135
132
136 def test_create_with_sort
133 def test_create_with_sort
137 @request.session[:user_id] = 1
134 @request.session[:user_id] = 1
138 post :create,
135 post :create,
139 :default_columns => '1',
136 :default_columns => '1',
140 :operators => {"status_id" => "o"},
137 :operators => {"status_id" => "o"},
141 :values => {"status_id" => ["1"]},
138 :values => {"status_id" => ["1"]},
142 :query => {:name => "test_new_with_sort",
139 :query => {:name => "test_new_with_sort",
143 :is_public => "1",
140 :visibility => "2",
144 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
141 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
145
142
146 query = Query.find_by_name("test_new_with_sort")
143 query = Query.find_by_name("test_new_with_sort")
147 assert_not_nil query
144 assert_not_nil query
148 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
145 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
149 end
146 end
150
147
151 def test_create_with_failure
148 def test_create_with_failure
152 @request.session[:user_id] = 2
149 @request.session[:user_id] = 2
153 assert_no_difference '::Query.count' do
150 assert_no_difference '::Query.count' do
154 post :create, :project_id => 'ecookbook', :query => {:name => ''}
151 post :create, :project_id => 'ecookbook', :query => {:name => ''}
155 end
152 end
156 assert_response :success
153 assert_response :success
157 assert_template 'new'
154 assert_template 'new'
158 assert_select 'input[name=?]', 'query[name]'
155 assert_select 'input[name=?]', 'query[name]'
159 end
156 end
160
157
161 def test_edit_global_public_query
158 def test_edit_global_public_query
162 @request.session[:user_id] = 1
159 @request.session[:user_id] = 1
163 get :edit, :id => 4
160 get :edit, :id => 4
164 assert_response :success
161 assert_response :success
165 assert_template 'edit'
162 assert_template 'edit'
166 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
163 assert_select 'input[name=?][value=2][checked=checked]', 'query[visibility]'
167 :name => 'query[is_public]',
168 :checked => 'checked' }
169 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
164 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
170 :name => 'query_is_for_all',
165 :name => 'query_is_for_all',
171 :checked => 'checked',
166 :checked => 'checked',
172 :disabled => 'disabled' }
167 :disabled => 'disabled' }
173 end
168 end
174
169
175 def test_edit_global_private_query
170 def test_edit_global_private_query
176 @request.session[:user_id] = 3
171 @request.session[:user_id] = 3
177 get :edit, :id => 3
172 get :edit, :id => 3
178 assert_response :success
173 assert_response :success
179 assert_template 'edit'
174 assert_template 'edit'
180 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
175 assert_select 'input[name=?]', 'query[visibility]', 0
181 :name => 'query[is_public]' }
182 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
176 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
183 :name => 'query_is_for_all',
177 :name => 'query_is_for_all',
184 :checked => 'checked',
178 :checked => 'checked',
185 :disabled => 'disabled' }
179 :disabled => 'disabled' }
186 end
180 end
187
181
188 def test_edit_project_private_query
182 def test_edit_project_private_query
189 @request.session[:user_id] = 3
183 @request.session[:user_id] = 3
190 get :edit, :id => 2
184 get :edit, :id => 2
191 assert_response :success
185 assert_response :success
192 assert_template 'edit'
186 assert_template 'edit'
193 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
187 assert_select 'input[name=?]', 'query[visibility]', 0
194 :name => 'query[is_public]' }
195 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
188 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
196 :name => 'query_is_for_all',
189 :name => 'query_is_for_all',
197 :checked => nil,
190 :checked => nil,
198 :disabled => nil }
191 :disabled => nil }
199 end
192 end
200
193
201 def test_edit_project_public_query
194 def test_edit_project_public_query
202 @request.session[:user_id] = 2
195 @request.session[:user_id] = 2
203 get :edit, :id => 1
196 get :edit, :id => 1
204 assert_response :success
197 assert_response :success
205 assert_template 'edit'
198 assert_template 'edit'
206 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
199 assert_select 'input[name=?][value=2][checked=checked]', 'query[visibility]'
207 :name => 'query[is_public]',
208 :checked => 'checked'
209 }
210 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
200 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
211 :name => 'query_is_for_all',
201 :name => 'query_is_for_all',
212 :checked => nil,
202 :checked => nil,
213 :disabled => 'disabled' }
203 :disabled => 'disabled' }
214 end
204 end
215
205
216 def test_edit_sort_criteria
206 def test_edit_sort_criteria
217 @request.session[:user_id] = 1
207 @request.session[:user_id] = 1
218 get :edit, :id => 5
208 get :edit, :id => 5
219 assert_response :success
209 assert_response :success
220 assert_template 'edit'
210 assert_template 'edit'
221 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
211 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
222 :child => { :tag => 'option', :attributes => { :value => 'priority',
212 :child => { :tag => 'option', :attributes => { :value => 'priority',
223 :selected => 'selected' } }
213 :selected => 'selected' } }
224 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
214 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
225 :child => { :tag => 'option', :attributes => { :value => 'desc',
215 :child => { :tag => 'option', :attributes => { :value => 'desc',
226 :selected => 'selected' } }
216 :selected => 'selected' } }
227 end
217 end
228
218
229 def test_edit_invalid_query
219 def test_edit_invalid_query
230 @request.session[:user_id] = 2
220 @request.session[:user_id] = 2
231 get :edit, :id => 99
221 get :edit, :id => 99
232 assert_response 404
222 assert_response 404
233 end
223 end
234
224
235 def test_udpate_global_private_query
225 def test_udpate_global_private_query
236 @request.session[:user_id] = 3
226 @request.session[:user_id] = 3
237 put :update,
227 put :update,
238 :id => 3,
228 :id => 3,
239 :default_columns => '1',
229 :default_columns => '1',
240 :fields => ["status_id", "assigned_to_id"],
230 :fields => ["status_id", "assigned_to_id"],
241 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
231 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
242 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
232 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
243 :query => {"name" => "test_edit_global_private_query", "is_public" => "1"}
233 :query => {"name" => "test_edit_global_private_query", "visibility" => "2"}
244
234
245 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
235 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
246 q = Query.find_by_name('test_edit_global_private_query')
236 q = Query.find_by_name('test_edit_global_private_query')
247 assert !q.is_public?
237 assert !q.is_public?
248 assert q.has_default_columns?
238 assert q.has_default_columns?
249 assert q.valid?
239 assert q.valid?
250 end
240 end
251
241
252 def test_update_global_public_query
242 def test_update_global_public_query
253 @request.session[:user_id] = 1
243 @request.session[:user_id] = 1
254 put :update,
244 put :update,
255 :id => 4,
245 :id => 4,
256 :default_columns => '1',
246 :default_columns => '1',
257 :fields => ["status_id", "assigned_to_id"],
247 :fields => ["status_id", "assigned_to_id"],
258 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
248 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
259 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
249 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
260 :query => {"name" => "test_edit_global_public_query", "is_public" => "1"}
250 :query => {"name" => "test_edit_global_public_query", "visibility" => "2"}
261
251
262 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
252 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
263 q = Query.find_by_name('test_edit_global_public_query')
253 q = Query.find_by_name('test_edit_global_public_query')
264 assert q.is_public?
254 assert q.is_public?
265 assert q.has_default_columns?
255 assert q.has_default_columns?
266 assert q.valid?
256 assert q.valid?
267 end
257 end
268
258
269 def test_update_with_failure
259 def test_update_with_failure
270 @request.session[:user_id] = 1
260 @request.session[:user_id] = 1
271 put :update, :id => 4, :query => {:name => ''}
261 put :update, :id => 4, :query => {:name => ''}
272 assert_response :success
262 assert_response :success
273 assert_template 'edit'
263 assert_template 'edit'
274 end
264 end
275
265
276 def test_destroy
266 def test_destroy
277 @request.session[:user_id] = 2
267 @request.session[:user_id] = 2
278 delete :destroy, :id => 1
268 delete :destroy, :id => 1
279 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
269 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
280 assert_nil Query.find_by_id(1)
270 assert_nil Query.find_by_id(1)
281 end
271 end
282
272
283 def test_backslash_should_be_escaped_in_filters
273 def test_backslash_should_be_escaped_in_filters
284 @request.session[:user_id] = 2
274 @request.session[:user_id] = 2
285 get :new, :subject => 'foo/bar'
275 get :new, :subject => 'foo/bar'
286 assert_response :success
276 assert_response :success
287 assert_template 'new'
277 assert_template 'new'
288 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
278 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
289 end
279 end
290 end
280 end
@@ -1,1275 +1,1341
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :projects, :enabled_modules, :users, :members,
23 fixtures :projects, :enabled_modules, :users, :members,
24 :member_roles, :roles, :trackers, :issue_statuses,
24 :member_roles, :roles, :trackers, :issue_statuses,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :watchers, :custom_fields, :custom_values, :versions,
26 :watchers, :custom_fields, :custom_values, :versions,
27 :queries,
27 :queries,
28 :projects_trackers,
28 :projects_trackers,
29 :custom_fields_trackers
29 :custom_fields_trackers
30
30
31 def test_query_with_roles_visibility_should_validate_roles
32 set_language_if_valid 'en'
33 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
34 assert !query.save
35 assert_include "Roles can't be blank", query.errors.full_messages
36 query.role_ids = [1, 2]
37 assert query.save
38 end
39
40 def test_changing_roles_visibility_should_clear_roles
41 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
42 assert_equal 2, query.roles.count
43
44 query.visibility = IssueQuery::VISIBILITY_PUBLIC
45 query.save!
46 assert_equal 0, query.roles.count
47 end
48
31 def test_available_filters_should_be_ordered
49 def test_available_filters_should_be_ordered
32 set_language_if_valid 'en'
50 set_language_if_valid 'en'
33 query = IssueQuery.new
51 query = IssueQuery.new
34 assert_equal 0, query.available_filters.keys.index('status_id')
52 assert_equal 0, query.available_filters.keys.index('status_id')
35 expected_order = [
53 expected_order = [
36 "Status",
54 "Status",
37 "Project",
55 "Project",
38 "Tracker",
56 "Tracker",
39 "Priority"
57 "Priority"
40 ]
58 ]
41 assert_equal expected_order,
59 assert_equal expected_order,
42 (query.available_filters.values.map{|v| v[:name]} & expected_order)
60 (query.available_filters.values.map{|v| v[:name]} & expected_order)
43 end
61 end
44
62
45 def test_available_filters_with_custom_fields_should_be_ordered
63 def test_available_filters_with_custom_fields_should_be_ordered
46 set_language_if_valid 'en'
64 set_language_if_valid 'en'
47 UserCustomField.create!(
65 UserCustomField.create!(
48 :name => 'order test', :field_format => 'string',
66 :name => 'order test', :field_format => 'string',
49 :is_for_all => true, :is_filter => true
67 :is_for_all => true, :is_filter => true
50 )
68 )
51 query = IssueQuery.new
69 query = IssueQuery.new
52 expected_order = [
70 expected_order = [
53 "Searchable field",
71 "Searchable field",
54 "Database",
72 "Database",
55 "Project's Development status",
73 "Project's Development status",
56 "Author's order test",
74 "Author's order test",
57 "Assignee's order test"
75 "Assignee's order test"
58 ]
76 ]
59 assert_equal expected_order,
77 assert_equal expected_order,
60 (query.available_filters.values.map{|v| v[:name]} & expected_order)
78 (query.available_filters.values.map{|v| v[:name]} & expected_order)
61 end
79 end
62
80
63 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
81 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
64 query = IssueQuery.new(:project => nil, :name => '_')
82 query = IssueQuery.new(:project => nil, :name => '_')
65 assert query.available_filters.has_key?('cf_1')
83 assert query.available_filters.has_key?('cf_1')
66 assert !query.available_filters.has_key?('cf_3')
84 assert !query.available_filters.has_key?('cf_3')
67 end
85 end
68
86
69 def test_system_shared_versions_should_be_available_in_global_queries
87 def test_system_shared_versions_should_be_available_in_global_queries
70 Version.find(2).update_attribute :sharing, 'system'
88 Version.find(2).update_attribute :sharing, 'system'
71 query = IssueQuery.new(:project => nil, :name => '_')
89 query = IssueQuery.new(:project => nil, :name => '_')
72 assert query.available_filters.has_key?('fixed_version_id')
90 assert query.available_filters.has_key?('fixed_version_id')
73 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
91 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
74 end
92 end
75
93
76 def test_project_filter_in_global_queries
94 def test_project_filter_in_global_queries
77 query = IssueQuery.new(:project => nil, :name => '_')
95 query = IssueQuery.new(:project => nil, :name => '_')
78 project_filter = query.available_filters["project_id"]
96 project_filter = query.available_filters["project_id"]
79 assert_not_nil project_filter
97 assert_not_nil project_filter
80 project_ids = project_filter[:values].map{|p| p[1]}
98 project_ids = project_filter[:values].map{|p| p[1]}
81 assert project_ids.include?("1") #public project
99 assert project_ids.include?("1") #public project
82 assert !project_ids.include?("2") #private project user cannot see
100 assert !project_ids.include?("2") #private project user cannot see
83 end
101 end
84
102
85 def find_issues_with_query(query)
103 def find_issues_with_query(query)
86 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
104 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
87 query.statement
105 query.statement
88 ).all
106 ).all
89 end
107 end
90
108
91 def assert_find_issues_with_query_is_successful(query)
109 def assert_find_issues_with_query_is_successful(query)
92 assert_nothing_raised do
110 assert_nothing_raised do
93 find_issues_with_query(query)
111 find_issues_with_query(query)
94 end
112 end
95 end
113 end
96
114
97 def assert_query_statement_includes(query, condition)
115 def assert_query_statement_includes(query, condition)
98 assert_include condition, query.statement
116 assert_include condition, query.statement
99 end
117 end
100
118
101 def assert_query_result(expected, query)
119 def assert_query_result(expected, query)
102 assert_nothing_raised do
120 assert_nothing_raised do
103 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
121 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
104 assert_equal expected.size, query.issue_count
122 assert_equal expected.size, query.issue_count
105 end
123 end
106 end
124 end
107
125
108 def test_query_should_allow_shared_versions_for_a_project_query
126 def test_query_should_allow_shared_versions_for_a_project_query
109 subproject_version = Version.find(4)
127 subproject_version = Version.find(4)
110 query = IssueQuery.new(:project => Project.find(1), :name => '_')
128 query = IssueQuery.new(:project => Project.find(1), :name => '_')
111 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
129 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
112
130
113 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
114 end
132 end
115
133
116 def test_query_with_multiple_custom_fields
134 def test_query_with_multiple_custom_fields
117 query = IssueQuery.find(1)
135 query = IssueQuery.find(1)
118 assert query.valid?
136 assert query.valid?
119 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
137 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
120 issues = find_issues_with_query(query)
138 issues = find_issues_with_query(query)
121 assert_equal 1, issues.length
139 assert_equal 1, issues.length
122 assert_equal Issue.find(3), issues.first
140 assert_equal Issue.find(3), issues.first
123 end
141 end
124
142
125 def test_operator_none
143 def test_operator_none
126 query = IssueQuery.new(:project => Project.find(1), :name => '_')
144 query = IssueQuery.new(:project => Project.find(1), :name => '_')
127 query.add_filter('fixed_version_id', '!*', [''])
145 query.add_filter('fixed_version_id', '!*', [''])
128 query.add_filter('cf_1', '!*', [''])
146 query.add_filter('cf_1', '!*', [''])
129 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
147 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
130 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
148 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
131 find_issues_with_query(query)
149 find_issues_with_query(query)
132 end
150 end
133
151
134 def test_operator_none_for_integer
152 def test_operator_none_for_integer
135 query = IssueQuery.new(:project => Project.find(1), :name => '_')
153 query = IssueQuery.new(:project => Project.find(1), :name => '_')
136 query.add_filter('estimated_hours', '!*', [''])
154 query.add_filter('estimated_hours', '!*', [''])
137 issues = find_issues_with_query(query)
155 issues = find_issues_with_query(query)
138 assert !issues.empty?
156 assert !issues.empty?
139 assert issues.all? {|i| !i.estimated_hours}
157 assert issues.all? {|i| !i.estimated_hours}
140 end
158 end
141
159
142 def test_operator_none_for_date
160 def test_operator_none_for_date
143 query = IssueQuery.new(:project => Project.find(1), :name => '_')
161 query = IssueQuery.new(:project => Project.find(1), :name => '_')
144 query.add_filter('start_date', '!*', [''])
162 query.add_filter('start_date', '!*', [''])
145 issues = find_issues_with_query(query)
163 issues = find_issues_with_query(query)
146 assert !issues.empty?
164 assert !issues.empty?
147 assert issues.all? {|i| i.start_date.nil?}
165 assert issues.all? {|i| i.start_date.nil?}
148 end
166 end
149
167
150 def test_operator_none_for_string_custom_field
168 def test_operator_none_for_string_custom_field
151 query = IssueQuery.new(:project => Project.find(1), :name => '_')
169 query = IssueQuery.new(:project => Project.find(1), :name => '_')
152 query.add_filter('cf_2', '!*', [''])
170 query.add_filter('cf_2', '!*', [''])
153 assert query.has_filter?('cf_2')
171 assert query.has_filter?('cf_2')
154 issues = find_issues_with_query(query)
172 issues = find_issues_with_query(query)
155 assert !issues.empty?
173 assert !issues.empty?
156 assert issues.all? {|i| i.custom_field_value(2).blank?}
174 assert issues.all? {|i| i.custom_field_value(2).blank?}
157 end
175 end
158
176
159 def test_operator_all
177 def test_operator_all
160 query = IssueQuery.new(:project => Project.find(1), :name => '_')
178 query = IssueQuery.new(:project => Project.find(1), :name => '_')
161 query.add_filter('fixed_version_id', '*', [''])
179 query.add_filter('fixed_version_id', '*', [''])
162 query.add_filter('cf_1', '*', [''])
180 query.add_filter('cf_1', '*', [''])
163 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
181 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
164 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
182 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
165 find_issues_with_query(query)
183 find_issues_with_query(query)
166 end
184 end
167
185
168 def test_operator_all_for_date
186 def test_operator_all_for_date
169 query = IssueQuery.new(:project => Project.find(1), :name => '_')
187 query = IssueQuery.new(:project => Project.find(1), :name => '_')
170 query.add_filter('start_date', '*', [''])
188 query.add_filter('start_date', '*', [''])
171 issues = find_issues_with_query(query)
189 issues = find_issues_with_query(query)
172 assert !issues.empty?
190 assert !issues.empty?
173 assert issues.all? {|i| i.start_date.present?}
191 assert issues.all? {|i| i.start_date.present?}
174 end
192 end
175
193
176 def test_operator_all_for_string_custom_field
194 def test_operator_all_for_string_custom_field
177 query = IssueQuery.new(:project => Project.find(1), :name => '_')
195 query = IssueQuery.new(:project => Project.find(1), :name => '_')
178 query.add_filter('cf_2', '*', [''])
196 query.add_filter('cf_2', '*', [''])
179 assert query.has_filter?('cf_2')
197 assert query.has_filter?('cf_2')
180 issues = find_issues_with_query(query)
198 issues = find_issues_with_query(query)
181 assert !issues.empty?
199 assert !issues.empty?
182 assert issues.all? {|i| i.custom_field_value(2).present?}
200 assert issues.all? {|i| i.custom_field_value(2).present?}
183 end
201 end
184
202
185 def test_numeric_filter_should_not_accept_non_numeric_values
203 def test_numeric_filter_should_not_accept_non_numeric_values
186 query = IssueQuery.new(:name => '_')
204 query = IssueQuery.new(:name => '_')
187 query.add_filter('estimated_hours', '=', ['a'])
205 query.add_filter('estimated_hours', '=', ['a'])
188
206
189 assert query.has_filter?('estimated_hours')
207 assert query.has_filter?('estimated_hours')
190 assert !query.valid?
208 assert !query.valid?
191 end
209 end
192
210
193 def test_operator_is_on_float
211 def test_operator_is_on_float
194 Issue.update_all("estimated_hours = 171.2", "id=2")
212 Issue.update_all("estimated_hours = 171.2", "id=2")
195
213
196 query = IssueQuery.new(:name => '_')
214 query = IssueQuery.new(:name => '_')
197 query.add_filter('estimated_hours', '=', ['171.20'])
215 query.add_filter('estimated_hours', '=', ['171.20'])
198 issues = find_issues_with_query(query)
216 issues = find_issues_with_query(query)
199 assert_equal 1, issues.size
217 assert_equal 1, issues.size
200 assert_equal 2, issues.first.id
218 assert_equal 2, issues.first.id
201 end
219 end
202
220
203 def test_operator_is_on_integer_custom_field
221 def test_operator_is_on_integer_custom_field
204 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
222 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
205 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
223 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
206 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
224 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
207 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
225 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
208
226
209 query = IssueQuery.new(:name => '_')
227 query = IssueQuery.new(:name => '_')
210 query.add_filter("cf_#{f.id}", '=', ['12'])
228 query.add_filter("cf_#{f.id}", '=', ['12'])
211 issues = find_issues_with_query(query)
229 issues = find_issues_with_query(query)
212 assert_equal 1, issues.size
230 assert_equal 1, issues.size
213 assert_equal 2, issues.first.id
231 assert_equal 2, issues.first.id
214 end
232 end
215
233
216 def test_operator_is_on_integer_custom_field_should_accept_negative_value
234 def test_operator_is_on_integer_custom_field_should_accept_negative_value
217 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
235 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
218 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
236 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
219 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
237 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
220 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
238 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
221
239
222 query = IssueQuery.new(:name => '_')
240 query = IssueQuery.new(:name => '_')
223 query.add_filter("cf_#{f.id}", '=', ['-12'])
241 query.add_filter("cf_#{f.id}", '=', ['-12'])
224 assert query.valid?
242 assert query.valid?
225 issues = find_issues_with_query(query)
243 issues = find_issues_with_query(query)
226 assert_equal 1, issues.size
244 assert_equal 1, issues.size
227 assert_equal 2, issues.first.id
245 assert_equal 2, issues.first.id
228 end
246 end
229
247
230 def test_operator_is_on_float_custom_field
248 def test_operator_is_on_float_custom_field
231 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
249 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
232 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
250 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
233 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
234 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
252 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
235
253
236 query = IssueQuery.new(:name => '_')
254 query = IssueQuery.new(:name => '_')
237 query.add_filter("cf_#{f.id}", '=', ['12.7'])
255 query.add_filter("cf_#{f.id}", '=', ['12.7'])
238 issues = find_issues_with_query(query)
256 issues = find_issues_with_query(query)
239 assert_equal 1, issues.size
257 assert_equal 1, issues.size
240 assert_equal 2, issues.first.id
258 assert_equal 2, issues.first.id
241 end
259 end
242
260
243 def test_operator_is_on_float_custom_field_should_accept_negative_value
261 def test_operator_is_on_float_custom_field_should_accept_negative_value
244 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
262 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
245 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
263 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
246 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
264 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
247 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
265 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
248
266
249 query = IssueQuery.new(:name => '_')
267 query = IssueQuery.new(:name => '_')
250 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
268 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
251 assert query.valid?
269 assert query.valid?
252 issues = find_issues_with_query(query)
270 issues = find_issues_with_query(query)
253 assert_equal 1, issues.size
271 assert_equal 1, issues.size
254 assert_equal 2, issues.first.id
272 assert_equal 2, issues.first.id
255 end
273 end
256
274
257 def test_operator_is_on_multi_list_custom_field
275 def test_operator_is_on_multi_list_custom_field
258 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
276 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
259 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
277 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
260 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
261 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
262 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
263
281
264 query = IssueQuery.new(:name => '_')
282 query = IssueQuery.new(:name => '_')
265 query.add_filter("cf_#{f.id}", '=', ['value1'])
283 query.add_filter("cf_#{f.id}", '=', ['value1'])
266 issues = find_issues_with_query(query)
284 issues = find_issues_with_query(query)
267 assert_equal [1, 3], issues.map(&:id).sort
285 assert_equal [1, 3], issues.map(&:id).sort
268
286
269 query = IssueQuery.new(:name => '_')
287 query = IssueQuery.new(:name => '_')
270 query.add_filter("cf_#{f.id}", '=', ['value2'])
288 query.add_filter("cf_#{f.id}", '=', ['value2'])
271 issues = find_issues_with_query(query)
289 issues = find_issues_with_query(query)
272 assert_equal [1], issues.map(&:id).sort
290 assert_equal [1], issues.map(&:id).sort
273 end
291 end
274
292
275 def test_operator_is_not_on_multi_list_custom_field
293 def test_operator_is_not_on_multi_list_custom_field
276 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
294 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
277 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
295 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
296 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
297 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
298 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
281
299
282 query = IssueQuery.new(:name => '_')
300 query = IssueQuery.new(:name => '_')
283 query.add_filter("cf_#{f.id}", '!', ['value1'])
301 query.add_filter("cf_#{f.id}", '!', ['value1'])
284 issues = find_issues_with_query(query)
302 issues = find_issues_with_query(query)
285 assert !issues.map(&:id).include?(1)
303 assert !issues.map(&:id).include?(1)
286 assert !issues.map(&:id).include?(3)
304 assert !issues.map(&:id).include?(3)
287
305
288 query = IssueQuery.new(:name => '_')
306 query = IssueQuery.new(:name => '_')
289 query.add_filter("cf_#{f.id}", '!', ['value2'])
307 query.add_filter("cf_#{f.id}", '!', ['value2'])
290 issues = find_issues_with_query(query)
308 issues = find_issues_with_query(query)
291 assert !issues.map(&:id).include?(1)
309 assert !issues.map(&:id).include?(1)
292 assert issues.map(&:id).include?(3)
310 assert issues.map(&:id).include?(3)
293 end
311 end
294
312
295 def test_operator_is_on_is_private_field
313 def test_operator_is_on_is_private_field
296 # is_private filter only available for those who can set issues private
314 # is_private filter only available for those who can set issues private
297 User.current = User.find(2)
315 User.current = User.find(2)
298
316
299 query = IssueQuery.new(:name => '_')
317 query = IssueQuery.new(:name => '_')
300 assert query.available_filters.key?('is_private')
318 assert query.available_filters.key?('is_private')
301
319
302 query.add_filter("is_private", '=', ['1'])
320 query.add_filter("is_private", '=', ['1'])
303 issues = find_issues_with_query(query)
321 issues = find_issues_with_query(query)
304 assert issues.any?
322 assert issues.any?
305 assert_nil issues.detect {|issue| !issue.is_private?}
323 assert_nil issues.detect {|issue| !issue.is_private?}
306 ensure
324 ensure
307 User.current = nil
325 User.current = nil
308 end
326 end
309
327
310 def test_operator_is_not_on_is_private_field
328 def test_operator_is_not_on_is_private_field
311 # is_private filter only available for those who can set issues private
329 # is_private filter only available for those who can set issues private
312 User.current = User.find(2)
330 User.current = User.find(2)
313
331
314 query = IssueQuery.new(:name => '_')
332 query = IssueQuery.new(:name => '_')
315 assert query.available_filters.key?('is_private')
333 assert query.available_filters.key?('is_private')
316
334
317 query.add_filter("is_private", '!', ['1'])
335 query.add_filter("is_private", '!', ['1'])
318 issues = find_issues_with_query(query)
336 issues = find_issues_with_query(query)
319 assert issues.any?
337 assert issues.any?
320 assert_nil issues.detect {|issue| issue.is_private?}
338 assert_nil issues.detect {|issue| issue.is_private?}
321 ensure
339 ensure
322 User.current = nil
340 User.current = nil
323 end
341 end
324
342
325 def test_operator_greater_than
343 def test_operator_greater_than
326 query = IssueQuery.new(:project => Project.find(1), :name => '_')
344 query = IssueQuery.new(:project => Project.find(1), :name => '_')
327 query.add_filter('done_ratio', '>=', ['40'])
345 query.add_filter('done_ratio', '>=', ['40'])
328 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
346 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
329 find_issues_with_query(query)
347 find_issues_with_query(query)
330 end
348 end
331
349
332 def test_operator_greater_than_a_float
350 def test_operator_greater_than_a_float
333 query = IssueQuery.new(:project => Project.find(1), :name => '_')
351 query = IssueQuery.new(:project => Project.find(1), :name => '_')
334 query.add_filter('estimated_hours', '>=', ['40.5'])
352 query.add_filter('estimated_hours', '>=', ['40.5'])
335 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
353 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
336 find_issues_with_query(query)
354 find_issues_with_query(query)
337 end
355 end
338
356
339 def test_operator_greater_than_on_int_custom_field
357 def test_operator_greater_than_on_int_custom_field
340 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
358 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
341 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
359 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
342 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
360 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
343 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
361 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
344
362
345 query = IssueQuery.new(:project => Project.find(1), :name => '_')
363 query = IssueQuery.new(:project => Project.find(1), :name => '_')
346 query.add_filter("cf_#{f.id}", '>=', ['8'])
364 query.add_filter("cf_#{f.id}", '>=', ['8'])
347 issues = find_issues_with_query(query)
365 issues = find_issues_with_query(query)
348 assert_equal 1, issues.size
366 assert_equal 1, issues.size
349 assert_equal 2, issues.first.id
367 assert_equal 2, issues.first.id
350 end
368 end
351
369
352 def test_operator_lesser_than
370 def test_operator_lesser_than
353 query = IssueQuery.new(:project => Project.find(1), :name => '_')
371 query = IssueQuery.new(:project => Project.find(1), :name => '_')
354 query.add_filter('done_ratio', '<=', ['30'])
372 query.add_filter('done_ratio', '<=', ['30'])
355 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
373 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
356 find_issues_with_query(query)
374 find_issues_with_query(query)
357 end
375 end
358
376
359 def test_operator_lesser_than_on_custom_field
377 def test_operator_lesser_than_on_custom_field
360 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
378 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
361 query = IssueQuery.new(:project => Project.find(1), :name => '_')
379 query = IssueQuery.new(:project => Project.find(1), :name => '_')
362 query.add_filter("cf_#{f.id}", '<=', ['30'])
380 query.add_filter("cf_#{f.id}", '<=', ['30'])
363 assert_match /CAST.+ <= 30\.0/, query.statement
381 assert_match /CAST.+ <= 30\.0/, query.statement
364 find_issues_with_query(query)
382 find_issues_with_query(query)
365 end
383 end
366
384
367 def test_operator_lesser_than_on_date_custom_field
385 def test_operator_lesser_than_on_date_custom_field
368 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true)
386 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true)
369 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
387 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
370 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
388 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
371 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
389 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
372
390
373 query = IssueQuery.new(:project => Project.find(1), :name => '_')
391 query = IssueQuery.new(:project => Project.find(1), :name => '_')
374 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
392 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
375 issue_ids = find_issues_with_query(query).map(&:id)
393 issue_ids = find_issues_with_query(query).map(&:id)
376 assert_include 1, issue_ids
394 assert_include 1, issue_ids
377 assert_not_include 2, issue_ids
395 assert_not_include 2, issue_ids
378 assert_not_include 3, issue_ids
396 assert_not_include 3, issue_ids
379 end
397 end
380
398
381 def test_operator_between
399 def test_operator_between
382 query = IssueQuery.new(:project => Project.find(1), :name => '_')
400 query = IssueQuery.new(:project => Project.find(1), :name => '_')
383 query.add_filter('done_ratio', '><', ['30', '40'])
401 query.add_filter('done_ratio', '><', ['30', '40'])
384 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
402 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
385 find_issues_with_query(query)
403 find_issues_with_query(query)
386 end
404 end
387
405
388 def test_operator_between_on_custom_field
406 def test_operator_between_on_custom_field
389 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
407 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
390 query = IssueQuery.new(:project => Project.find(1), :name => '_')
408 query = IssueQuery.new(:project => Project.find(1), :name => '_')
391 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
409 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
392 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
410 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
393 find_issues_with_query(query)
411 find_issues_with_query(query)
394 end
412 end
395
413
396 def test_date_filter_should_not_accept_non_date_values
414 def test_date_filter_should_not_accept_non_date_values
397 query = IssueQuery.new(:name => '_')
415 query = IssueQuery.new(:name => '_')
398 query.add_filter('created_on', '=', ['a'])
416 query.add_filter('created_on', '=', ['a'])
399
417
400 assert query.has_filter?('created_on')
418 assert query.has_filter?('created_on')
401 assert !query.valid?
419 assert !query.valid?
402 end
420 end
403
421
404 def test_date_filter_should_not_accept_invalid_date_values
422 def test_date_filter_should_not_accept_invalid_date_values
405 query = IssueQuery.new(:name => '_')
423 query = IssueQuery.new(:name => '_')
406 query.add_filter('created_on', '=', ['2011-01-34'])
424 query.add_filter('created_on', '=', ['2011-01-34'])
407
425
408 assert query.has_filter?('created_on')
426 assert query.has_filter?('created_on')
409 assert !query.valid?
427 assert !query.valid?
410 end
428 end
411
429
412 def test_relative_date_filter_should_not_accept_non_integer_values
430 def test_relative_date_filter_should_not_accept_non_integer_values
413 query = IssueQuery.new(:name => '_')
431 query = IssueQuery.new(:name => '_')
414 query.add_filter('created_on', '>t-', ['a'])
432 query.add_filter('created_on', '>t-', ['a'])
415
433
416 assert query.has_filter?('created_on')
434 assert query.has_filter?('created_on')
417 assert !query.valid?
435 assert !query.valid?
418 end
436 end
419
437
420 def test_operator_date_equals
438 def test_operator_date_equals
421 query = IssueQuery.new(:name => '_')
439 query = IssueQuery.new(:name => '_')
422 query.add_filter('due_date', '=', ['2011-07-10'])
440 query.add_filter('due_date', '=', ['2011-07-10'])
423 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
441 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
424 find_issues_with_query(query)
442 find_issues_with_query(query)
425 end
443 end
426
444
427 def test_operator_date_lesser_than
445 def test_operator_date_lesser_than
428 query = IssueQuery.new(:name => '_')
446 query = IssueQuery.new(:name => '_')
429 query.add_filter('due_date', '<=', ['2011-07-10'])
447 query.add_filter('due_date', '<=', ['2011-07-10'])
430 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
448 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
431 find_issues_with_query(query)
449 find_issues_with_query(query)
432 end
450 end
433
451
434 def test_operator_date_greater_than
452 def test_operator_date_greater_than
435 query = IssueQuery.new(:name => '_')
453 query = IssueQuery.new(:name => '_')
436 query.add_filter('due_date', '>=', ['2011-07-10'])
454 query.add_filter('due_date', '>=', ['2011-07-10'])
437 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
455 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
438 find_issues_with_query(query)
456 find_issues_with_query(query)
439 end
457 end
440
458
441 def test_operator_date_between
459 def test_operator_date_between
442 query = IssueQuery.new(:name => '_')
460 query = IssueQuery.new(:name => '_')
443 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
461 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
444 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
462 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
445 find_issues_with_query(query)
463 find_issues_with_query(query)
446 end
464 end
447
465
448 def test_operator_in_more_than
466 def test_operator_in_more_than
449 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
467 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
450 query = IssueQuery.new(:project => Project.find(1), :name => '_')
468 query = IssueQuery.new(:project => Project.find(1), :name => '_')
451 query.add_filter('due_date', '>t+', ['15'])
469 query.add_filter('due_date', '>t+', ['15'])
452 issues = find_issues_with_query(query)
470 issues = find_issues_with_query(query)
453 assert !issues.empty?
471 assert !issues.empty?
454 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
472 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
455 end
473 end
456
474
457 def test_operator_in_less_than
475 def test_operator_in_less_than
458 query = IssueQuery.new(:project => Project.find(1), :name => '_')
476 query = IssueQuery.new(:project => Project.find(1), :name => '_')
459 query.add_filter('due_date', '<t+', ['15'])
477 query.add_filter('due_date', '<t+', ['15'])
460 issues = find_issues_with_query(query)
478 issues = find_issues_with_query(query)
461 assert !issues.empty?
479 assert !issues.empty?
462 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
480 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
463 end
481 end
464
482
465 def test_operator_in_the_next_days
483 def test_operator_in_the_next_days
466 query = IssueQuery.new(:project => Project.find(1), :name => '_')
484 query = IssueQuery.new(:project => Project.find(1), :name => '_')
467 query.add_filter('due_date', '><t+', ['15'])
485 query.add_filter('due_date', '><t+', ['15'])
468 issues = find_issues_with_query(query)
486 issues = find_issues_with_query(query)
469 assert !issues.empty?
487 assert !issues.empty?
470 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
488 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
471 end
489 end
472
490
473 def test_operator_less_than_ago
491 def test_operator_less_than_ago
474 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
492 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
475 query = IssueQuery.new(:project => Project.find(1), :name => '_')
493 query = IssueQuery.new(:project => Project.find(1), :name => '_')
476 query.add_filter('due_date', '>t-', ['3'])
494 query.add_filter('due_date', '>t-', ['3'])
477 issues = find_issues_with_query(query)
495 issues = find_issues_with_query(query)
478 assert !issues.empty?
496 assert !issues.empty?
479 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
497 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
480 end
498 end
481
499
482 def test_operator_in_the_past_days
500 def test_operator_in_the_past_days
483 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
501 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
484 query = IssueQuery.new(:project => Project.find(1), :name => '_')
502 query = IssueQuery.new(:project => Project.find(1), :name => '_')
485 query.add_filter('due_date', '><t-', ['3'])
503 query.add_filter('due_date', '><t-', ['3'])
486 issues = find_issues_with_query(query)
504 issues = find_issues_with_query(query)
487 assert !issues.empty?
505 assert !issues.empty?
488 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
506 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
489 end
507 end
490
508
491 def test_operator_more_than_ago
509 def test_operator_more_than_ago
492 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
510 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
493 query = IssueQuery.new(:project => Project.find(1), :name => '_')
511 query = IssueQuery.new(:project => Project.find(1), :name => '_')
494 query.add_filter('due_date', '<t-', ['10'])
512 query.add_filter('due_date', '<t-', ['10'])
495 assert query.statement.include?("#{Issue.table_name}.due_date <=")
513 assert query.statement.include?("#{Issue.table_name}.due_date <=")
496 issues = find_issues_with_query(query)
514 issues = find_issues_with_query(query)
497 assert !issues.empty?
515 assert !issues.empty?
498 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
516 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
499 end
517 end
500
518
501 def test_operator_in
519 def test_operator_in
502 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
520 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
503 query = IssueQuery.new(:project => Project.find(1), :name => '_')
521 query = IssueQuery.new(:project => Project.find(1), :name => '_')
504 query.add_filter('due_date', 't+', ['2'])
522 query.add_filter('due_date', 't+', ['2'])
505 issues = find_issues_with_query(query)
523 issues = find_issues_with_query(query)
506 assert !issues.empty?
524 assert !issues.empty?
507 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
525 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
508 end
526 end
509
527
510 def test_operator_ago
528 def test_operator_ago
511 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
529 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
512 query = IssueQuery.new(:project => Project.find(1), :name => '_')
530 query = IssueQuery.new(:project => Project.find(1), :name => '_')
513 query.add_filter('due_date', 't-', ['3'])
531 query.add_filter('due_date', 't-', ['3'])
514 issues = find_issues_with_query(query)
532 issues = find_issues_with_query(query)
515 assert !issues.empty?
533 assert !issues.empty?
516 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
534 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
517 end
535 end
518
536
519 def test_operator_today
537 def test_operator_today
520 query = IssueQuery.new(:project => Project.find(1), :name => '_')
538 query = IssueQuery.new(:project => Project.find(1), :name => '_')
521 query.add_filter('due_date', 't', [''])
539 query.add_filter('due_date', 't', [''])
522 issues = find_issues_with_query(query)
540 issues = find_issues_with_query(query)
523 assert !issues.empty?
541 assert !issues.empty?
524 issues.each {|issue| assert_equal Date.today, issue.due_date}
542 issues.each {|issue| assert_equal Date.today, issue.due_date}
525 end
543 end
526
544
527 def test_operator_this_week_on_date
545 def test_operator_this_week_on_date
528 query = IssueQuery.new(:project => Project.find(1), :name => '_')
546 query = IssueQuery.new(:project => Project.find(1), :name => '_')
529 query.add_filter('due_date', 'w', [''])
547 query.add_filter('due_date', 'w', [''])
530 find_issues_with_query(query)
548 find_issues_with_query(query)
531 end
549 end
532
550
533 def test_operator_this_week_on_datetime
551 def test_operator_this_week_on_datetime
534 query = IssueQuery.new(:project => Project.find(1), :name => '_')
552 query = IssueQuery.new(:project => Project.find(1), :name => '_')
535 query.add_filter('created_on', 'w', [''])
553 query.add_filter('created_on', 'w', [''])
536 find_issues_with_query(query)
554 find_issues_with_query(query)
537 end
555 end
538
556
539 def test_operator_contains
557 def test_operator_contains
540 query = IssueQuery.new(:project => Project.find(1), :name => '_')
558 query = IssueQuery.new(:project => Project.find(1), :name => '_')
541 query.add_filter('subject', '~', ['uNable'])
559 query.add_filter('subject', '~', ['uNable'])
542 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
560 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
543 result = find_issues_with_query(query)
561 result = find_issues_with_query(query)
544 assert result.empty?
562 assert result.empty?
545 result.each {|issue| assert issue.subject.downcase.include?('unable') }
563 result.each {|issue| assert issue.subject.downcase.include?('unable') }
546 end
564 end
547
565
548 def test_range_for_this_week_with_week_starting_on_monday
566 def test_range_for_this_week_with_week_starting_on_monday
549 I18n.locale = :fr
567 I18n.locale = :fr
550 assert_equal '1', I18n.t(:general_first_day_of_week)
568 assert_equal '1', I18n.t(:general_first_day_of_week)
551
569
552 Date.stubs(:today).returns(Date.parse('2011-04-29'))
570 Date.stubs(:today).returns(Date.parse('2011-04-29'))
553
571
554 query = IssueQuery.new(:project => Project.find(1), :name => '_')
572 query = IssueQuery.new(:project => Project.find(1), :name => '_')
555 query.add_filter('due_date', 'w', [''])
573 query.add_filter('due_date', 'w', [''])
556 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
574 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
557 I18n.locale = :en
575 I18n.locale = :en
558 end
576 end
559
577
560 def test_range_for_this_week_with_week_starting_on_sunday
578 def test_range_for_this_week_with_week_starting_on_sunday
561 I18n.locale = :en
579 I18n.locale = :en
562 assert_equal '7', I18n.t(:general_first_day_of_week)
580 assert_equal '7', I18n.t(:general_first_day_of_week)
563
581
564 Date.stubs(:today).returns(Date.parse('2011-04-29'))
582 Date.stubs(:today).returns(Date.parse('2011-04-29'))
565
583
566 query = IssueQuery.new(:project => Project.find(1), :name => '_')
584 query = IssueQuery.new(:project => Project.find(1), :name => '_')
567 query.add_filter('due_date', 'w', [''])
585 query.add_filter('due_date', 'w', [''])
568 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
586 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
569 end
587 end
570
588
571 def test_operator_does_not_contains
589 def test_operator_does_not_contains
572 query = IssueQuery.new(:project => Project.find(1), :name => '_')
590 query = IssueQuery.new(:project => Project.find(1), :name => '_')
573 query.add_filter('subject', '!~', ['uNable'])
591 query.add_filter('subject', '!~', ['uNable'])
574 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
592 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
575 find_issues_with_query(query)
593 find_issues_with_query(query)
576 end
594 end
577
595
578 def test_filter_assigned_to_me
596 def test_filter_assigned_to_me
579 user = User.find(2)
597 user = User.find(2)
580 group = Group.find(10)
598 group = Group.find(10)
581 User.current = user
599 User.current = user
582 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
600 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
583 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
601 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
584 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
602 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
585 group.users << user
603 group.users << user
586
604
587 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
605 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
588 result = query.issues
606 result = query.issues
589 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
607 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
590
608
591 assert result.include?(i1)
609 assert result.include?(i1)
592 assert result.include?(i2)
610 assert result.include?(i2)
593 assert !result.include?(i3)
611 assert !result.include?(i3)
594 end
612 end
595
613
596 def test_user_custom_field_filtered_on_me
614 def test_user_custom_field_filtered_on_me
597 User.current = User.find(2)
615 User.current = User.find(2)
598 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
616 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
599 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
617 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
600 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
618 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
601
619
602 query = IssueQuery.new(:name => '_', :project => Project.find(1))
620 query = IssueQuery.new(:name => '_', :project => Project.find(1))
603 filter = query.available_filters["cf_#{cf.id}"]
621 filter = query.available_filters["cf_#{cf.id}"]
604 assert_not_nil filter
622 assert_not_nil filter
605 assert_include 'me', filter[:values].map{|v| v[1]}
623 assert_include 'me', filter[:values].map{|v| v[1]}
606
624
607 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
625 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
608 result = query.issues
626 result = query.issues
609 assert_equal 1, result.size
627 assert_equal 1, result.size
610 assert_equal issue1, result.first
628 assert_equal issue1, result.first
611 end
629 end
612
630
613 def test_filter_my_projects
631 def test_filter_my_projects
614 User.current = User.find(2)
632 User.current = User.find(2)
615 query = IssueQuery.new(:name => '_')
633 query = IssueQuery.new(:name => '_')
616 filter = query.available_filters['project_id']
634 filter = query.available_filters['project_id']
617 assert_not_nil filter
635 assert_not_nil filter
618 assert_include 'mine', filter[:values].map{|v| v[1]}
636 assert_include 'mine', filter[:values].map{|v| v[1]}
619
637
620 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
638 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
621 result = query.issues
639 result = query.issues
622 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
640 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
623 end
641 end
624
642
625 def test_filter_watched_issues
643 def test_filter_watched_issues
626 User.current = User.find(1)
644 User.current = User.find(1)
627 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
645 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
628 result = find_issues_with_query(query)
646 result = find_issues_with_query(query)
629 assert_not_nil result
647 assert_not_nil result
630 assert !result.empty?
648 assert !result.empty?
631 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
649 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
632 User.current = nil
650 User.current = nil
633 end
651 end
634
652
635 def test_filter_unwatched_issues
653 def test_filter_unwatched_issues
636 User.current = User.find(1)
654 User.current = User.find(1)
637 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
655 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
638 result = find_issues_with_query(query)
656 result = find_issues_with_query(query)
639 assert_not_nil result
657 assert_not_nil result
640 assert !result.empty?
658 assert !result.empty?
641 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
659 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
642 User.current = nil
660 User.current = nil
643 end
661 end
644
662
645 def test_filter_on_project_custom_field
663 def test_filter_on_project_custom_field
646 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
664 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
647 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
665 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
648 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
666 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
649
667
650 query = IssueQuery.new(:name => '_')
668 query = IssueQuery.new(:name => '_')
651 filter_name = "project.cf_#{field.id}"
669 filter_name = "project.cf_#{field.id}"
652 assert_include filter_name, query.available_filters.keys
670 assert_include filter_name, query.available_filters.keys
653 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
671 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
654 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
672 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
655 end
673 end
656
674
657 def test_filter_on_author_custom_field
675 def test_filter_on_author_custom_field
658 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
676 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
659 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
677 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
660
678
661 query = IssueQuery.new(:name => '_')
679 query = IssueQuery.new(:name => '_')
662 filter_name = "author.cf_#{field.id}"
680 filter_name = "author.cf_#{field.id}"
663 assert_include filter_name, query.available_filters.keys
681 assert_include filter_name, query.available_filters.keys
664 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
682 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
665 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
683 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
666 end
684 end
667
685
668 def test_filter_on_assigned_to_custom_field
686 def test_filter_on_assigned_to_custom_field
669 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
687 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
670 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
688 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
671
689
672 query = IssueQuery.new(:name => '_')
690 query = IssueQuery.new(:name => '_')
673 filter_name = "assigned_to.cf_#{field.id}"
691 filter_name = "assigned_to.cf_#{field.id}"
674 assert_include filter_name, query.available_filters.keys
692 assert_include filter_name, query.available_filters.keys
675 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
693 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
676 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
694 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
677 end
695 end
678
696
679 def test_filter_on_fixed_version_custom_field
697 def test_filter_on_fixed_version_custom_field
680 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
698 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
681 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
699 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
682
700
683 query = IssueQuery.new(:name => '_')
701 query = IssueQuery.new(:name => '_')
684 filter_name = "fixed_version.cf_#{field.id}"
702 filter_name = "fixed_version.cf_#{field.id}"
685 assert_include filter_name, query.available_filters.keys
703 assert_include filter_name, query.available_filters.keys
686 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
704 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
687 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
705 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
688 end
706 end
689
707
690 def test_filter_on_relations_with_a_specific_issue
708 def test_filter_on_relations_with_a_specific_issue
691 IssueRelation.delete_all
709 IssueRelation.delete_all
692 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
710 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
693 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
711 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
694
712
695 query = IssueQuery.new(:name => '_')
713 query = IssueQuery.new(:name => '_')
696 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
714 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
697 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
715 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
698
716
699 query = IssueQuery.new(:name => '_')
717 query = IssueQuery.new(:name => '_')
700 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
718 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
701 assert_equal [1], find_issues_with_query(query).map(&:id).sort
719 assert_equal [1], find_issues_with_query(query).map(&:id).sort
702 end
720 end
703
721
704 def test_filter_on_relations_with_any_issues_in_a_project
722 def test_filter_on_relations_with_any_issues_in_a_project
705 IssueRelation.delete_all
723 IssueRelation.delete_all
706 with_settings :cross_project_issue_relations => '1' do
724 with_settings :cross_project_issue_relations => '1' do
707 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
725 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
708 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
726 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
709 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
727 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
710 end
728 end
711
729
712 query = IssueQuery.new(:name => '_')
730 query = IssueQuery.new(:name => '_')
713 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
731 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
714 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
732 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
715
733
716 query = IssueQuery.new(:name => '_')
734 query = IssueQuery.new(:name => '_')
717 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
735 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
718 assert_equal [1], find_issues_with_query(query).map(&:id).sort
736 assert_equal [1], find_issues_with_query(query).map(&:id).sort
719
737
720 query = IssueQuery.new(:name => '_')
738 query = IssueQuery.new(:name => '_')
721 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
739 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
722 assert_equal [], find_issues_with_query(query).map(&:id).sort
740 assert_equal [], find_issues_with_query(query).map(&:id).sort
723 end
741 end
724
742
725 def test_filter_on_relations_with_any_issues_not_in_a_project
743 def test_filter_on_relations_with_any_issues_not_in_a_project
726 IssueRelation.delete_all
744 IssueRelation.delete_all
727 with_settings :cross_project_issue_relations => '1' do
745 with_settings :cross_project_issue_relations => '1' do
728 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
746 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
729 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
747 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
730 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
748 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
731 end
749 end
732
750
733 query = IssueQuery.new(:name => '_')
751 query = IssueQuery.new(:name => '_')
734 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
752 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
735 assert_equal [1], find_issues_with_query(query).map(&:id).sort
753 assert_equal [1], find_issues_with_query(query).map(&:id).sort
736 end
754 end
737
755
738 def test_filter_on_relations_with_no_issues_in_a_project
756 def test_filter_on_relations_with_no_issues_in_a_project
739 IssueRelation.delete_all
757 IssueRelation.delete_all
740 with_settings :cross_project_issue_relations => '1' do
758 with_settings :cross_project_issue_relations => '1' do
741 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
759 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
742 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
760 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
743 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
761 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
744 end
762 end
745
763
746 query = IssueQuery.new(:name => '_')
764 query = IssueQuery.new(:name => '_')
747 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
765 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
748 ids = find_issues_with_query(query).map(&:id).sort
766 ids = find_issues_with_query(query).map(&:id).sort
749 assert_include 2, ids
767 assert_include 2, ids
750 assert_not_include 1, ids
768 assert_not_include 1, ids
751 assert_not_include 3, ids
769 assert_not_include 3, ids
752 end
770 end
753
771
754 def test_filter_on_relations_with_no_issues
772 def test_filter_on_relations_with_no_issues
755 IssueRelation.delete_all
773 IssueRelation.delete_all
756 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
774 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
757 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
775 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
758
776
759 query = IssueQuery.new(:name => '_')
777 query = IssueQuery.new(:name => '_')
760 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
778 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
761 ids = find_issues_with_query(query).map(&:id)
779 ids = find_issues_with_query(query).map(&:id)
762 assert_equal [], ids & [1, 2, 3]
780 assert_equal [], ids & [1, 2, 3]
763 assert_include 4, ids
781 assert_include 4, ids
764 end
782 end
765
783
766 def test_filter_on_relations_with_any_issues
784 def test_filter_on_relations_with_any_issues
767 IssueRelation.delete_all
785 IssueRelation.delete_all
768 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
786 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
769 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
787 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
770
788
771 query = IssueQuery.new(:name => '_')
789 query = IssueQuery.new(:name => '_')
772 query.filters = {"relates" => {:operator => '*', :values => ['']}}
790 query.filters = {"relates" => {:operator => '*', :values => ['']}}
773 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
791 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
774 end
792 end
775
793
776 def test_statement_should_be_nil_with_no_filters
794 def test_statement_should_be_nil_with_no_filters
777 q = IssueQuery.new(:name => '_')
795 q = IssueQuery.new(:name => '_')
778 q.filters = {}
796 q.filters = {}
779
797
780 assert q.valid?
798 assert q.valid?
781 assert_nil q.statement
799 assert_nil q.statement
782 end
800 end
783
801
784 def test_default_columns
802 def test_default_columns
785 q = IssueQuery.new
803 q = IssueQuery.new
786 assert q.columns.any?
804 assert q.columns.any?
787 assert q.inline_columns.any?
805 assert q.inline_columns.any?
788 assert q.block_columns.empty?
806 assert q.block_columns.empty?
789 end
807 end
790
808
791 def test_set_column_names
809 def test_set_column_names
792 q = IssueQuery.new
810 q = IssueQuery.new
793 q.column_names = ['tracker', :subject, '', 'unknonw_column']
811 q.column_names = ['tracker', :subject, '', 'unknonw_column']
794 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
812 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
795 end
813 end
796
814
797 def test_has_column_should_accept_a_column_name
815 def test_has_column_should_accept_a_column_name
798 q = IssueQuery.new
816 q = IssueQuery.new
799 q.column_names = ['tracker', :subject]
817 q.column_names = ['tracker', :subject]
800 assert q.has_column?(:tracker)
818 assert q.has_column?(:tracker)
801 assert !q.has_column?(:category)
819 assert !q.has_column?(:category)
802 end
820 end
803
821
804 def test_has_column_should_accept_a_column
822 def test_has_column_should_accept_a_column
805 q = IssueQuery.new
823 q = IssueQuery.new
806 q.column_names = ['tracker', :subject]
824 q.column_names = ['tracker', :subject]
807
825
808 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
826 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
809 assert_kind_of QueryColumn, tracker_column
827 assert_kind_of QueryColumn, tracker_column
810 category_column = q.available_columns.detect {|c| c.name==:category}
828 category_column = q.available_columns.detect {|c| c.name==:category}
811 assert_kind_of QueryColumn, category_column
829 assert_kind_of QueryColumn, category_column
812
830
813 assert q.has_column?(tracker_column)
831 assert q.has_column?(tracker_column)
814 assert !q.has_column?(category_column)
832 assert !q.has_column?(category_column)
815 end
833 end
816
834
817 def test_inline_and_block_columns
835 def test_inline_and_block_columns
818 q = IssueQuery.new
836 q = IssueQuery.new
819 q.column_names = ['subject', 'description', 'tracker']
837 q.column_names = ['subject', 'description', 'tracker']
820
838
821 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
839 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
822 assert_equal [:description], q.block_columns.map(&:name)
840 assert_equal [:description], q.block_columns.map(&:name)
823 end
841 end
824
842
825 def test_custom_field_columns_should_be_inline
843 def test_custom_field_columns_should_be_inline
826 q = IssueQuery.new
844 q = IssueQuery.new
827 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
845 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
828 assert columns.any?
846 assert columns.any?
829 assert_nil columns.detect {|column| !column.inline?}
847 assert_nil columns.detect {|column| !column.inline?}
830 end
848 end
831
849
832 def test_query_should_preload_spent_hours
850 def test_query_should_preload_spent_hours
833 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
851 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
834 assert q.has_column?(:spent_hours)
852 assert q.has_column?(:spent_hours)
835 issues = q.issues
853 issues = q.issues
836 assert_not_nil issues.first.instance_variable_get("@spent_hours")
854 assert_not_nil issues.first.instance_variable_get("@spent_hours")
837 end
855 end
838
856
839 def test_groupable_columns_should_include_custom_fields
857 def test_groupable_columns_should_include_custom_fields
840 q = IssueQuery.new
858 q = IssueQuery.new
841 column = q.groupable_columns.detect {|c| c.name == :cf_1}
859 column = q.groupable_columns.detect {|c| c.name == :cf_1}
842 assert_not_nil column
860 assert_not_nil column
843 assert_kind_of QueryCustomFieldColumn, column
861 assert_kind_of QueryCustomFieldColumn, column
844 end
862 end
845
863
846 def test_groupable_columns_should_not_include_multi_custom_fields
864 def test_groupable_columns_should_not_include_multi_custom_fields
847 field = CustomField.find(1)
865 field = CustomField.find(1)
848 field.update_attribute :multiple, true
866 field.update_attribute :multiple, true
849
867
850 q = IssueQuery.new
868 q = IssueQuery.new
851 column = q.groupable_columns.detect {|c| c.name == :cf_1}
869 column = q.groupable_columns.detect {|c| c.name == :cf_1}
852 assert_nil column
870 assert_nil column
853 end
871 end
854
872
855 def test_groupable_columns_should_include_user_custom_fields
873 def test_groupable_columns_should_include_user_custom_fields
856 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
874 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
857
875
858 q = IssueQuery.new
876 q = IssueQuery.new
859 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
877 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
860 end
878 end
861
879
862 def test_groupable_columns_should_include_version_custom_fields
880 def test_groupable_columns_should_include_version_custom_fields
863 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
881 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
864
882
865 q = IssueQuery.new
883 q = IssueQuery.new
866 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
884 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
867 end
885 end
868
886
869 def test_grouped_with_valid_column
887 def test_grouped_with_valid_column
870 q = IssueQuery.new(:group_by => 'status')
888 q = IssueQuery.new(:group_by => 'status')
871 assert q.grouped?
889 assert q.grouped?
872 assert_not_nil q.group_by_column
890 assert_not_nil q.group_by_column
873 assert_equal :status, q.group_by_column.name
891 assert_equal :status, q.group_by_column.name
874 assert_not_nil q.group_by_statement
892 assert_not_nil q.group_by_statement
875 assert_equal 'status', q.group_by_statement
893 assert_equal 'status', q.group_by_statement
876 end
894 end
877
895
878 def test_grouped_with_invalid_column
896 def test_grouped_with_invalid_column
879 q = IssueQuery.new(:group_by => 'foo')
897 q = IssueQuery.new(:group_by => 'foo')
880 assert !q.grouped?
898 assert !q.grouped?
881 assert_nil q.group_by_column
899 assert_nil q.group_by_column
882 assert_nil q.group_by_statement
900 assert_nil q.group_by_statement
883 end
901 end
884
902
885 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
903 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
886 with_settings :user_format => 'lastname_coma_firstname' do
904 with_settings :user_format => 'lastname_coma_firstname' do
887 q = IssueQuery.new
905 q = IssueQuery.new
888 assert q.sortable_columns.has_key?('assigned_to')
906 assert q.sortable_columns.has_key?('assigned_to')
889 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
907 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
890 end
908 end
891 end
909 end
892
910
893 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
911 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
894 with_settings :user_format => 'lastname_coma_firstname' do
912 with_settings :user_format => 'lastname_coma_firstname' do
895 q = IssueQuery.new
913 q = IssueQuery.new
896 assert q.sortable_columns.has_key?('author')
914 assert q.sortable_columns.has_key?('author')
897 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
915 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
898 end
916 end
899 end
917 end
900
918
901 def test_sortable_columns_should_include_custom_field
919 def test_sortable_columns_should_include_custom_field
902 q = IssueQuery.new
920 q = IssueQuery.new
903 assert q.sortable_columns['cf_1']
921 assert q.sortable_columns['cf_1']
904 end
922 end
905
923
906 def test_sortable_columns_should_not_include_multi_custom_field
924 def test_sortable_columns_should_not_include_multi_custom_field
907 field = CustomField.find(1)
925 field = CustomField.find(1)
908 field.update_attribute :multiple, true
926 field.update_attribute :multiple, true
909
927
910 q = IssueQuery.new
928 q = IssueQuery.new
911 assert !q.sortable_columns['cf_1']
929 assert !q.sortable_columns['cf_1']
912 end
930 end
913
931
914 def test_default_sort
932 def test_default_sort
915 q = IssueQuery.new
933 q = IssueQuery.new
916 assert_equal [], q.sort_criteria
934 assert_equal [], q.sort_criteria
917 end
935 end
918
936
919 def test_set_sort_criteria_with_hash
937 def test_set_sort_criteria_with_hash
920 q = IssueQuery.new
938 q = IssueQuery.new
921 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
939 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
922 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
940 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
923 end
941 end
924
942
925 def test_set_sort_criteria_with_array
943 def test_set_sort_criteria_with_array
926 q = IssueQuery.new
944 q = IssueQuery.new
927 q.sort_criteria = [['priority', 'desc'], 'tracker']
945 q.sort_criteria = [['priority', 'desc'], 'tracker']
928 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
946 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
929 end
947 end
930
948
931 def test_create_query_with_sort
949 def test_create_query_with_sort
932 q = IssueQuery.new(:name => 'Sorted')
950 q = IssueQuery.new(:name => 'Sorted')
933 q.sort_criteria = [['priority', 'desc'], 'tracker']
951 q.sort_criteria = [['priority', 'desc'], 'tracker']
934 assert q.save
952 assert q.save
935 q.reload
953 q.reload
936 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
954 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
937 end
955 end
938
956
939 def test_sort_by_string_custom_field_asc
957 def test_sort_by_string_custom_field_asc
940 q = IssueQuery.new
958 q = IssueQuery.new
941 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
959 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
942 assert c
960 assert c
943 assert c.sortable
961 assert c.sortable
944 issues = q.issues(:order => "#{c.sortable} ASC")
962 issues = q.issues(:order => "#{c.sortable} ASC")
945 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
963 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
946 assert !values.empty?
964 assert !values.empty?
947 assert_equal values.sort, values
965 assert_equal values.sort, values
948 end
966 end
949
967
950 def test_sort_by_string_custom_field_desc
968 def test_sort_by_string_custom_field_desc
951 q = IssueQuery.new
969 q = IssueQuery.new
952 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
970 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
953 assert c
971 assert c
954 assert c.sortable
972 assert c.sortable
955 issues = q.issues(:order => "#{c.sortable} DESC")
973 issues = q.issues(:order => "#{c.sortable} DESC")
956 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
974 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
957 assert !values.empty?
975 assert !values.empty?
958 assert_equal values.sort.reverse, values
976 assert_equal values.sort.reverse, values
959 end
977 end
960
978
961 def test_sort_by_float_custom_field_asc
979 def test_sort_by_float_custom_field_asc
962 q = IssueQuery.new
980 q = IssueQuery.new
963 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
981 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
964 assert c
982 assert c
965 assert c.sortable
983 assert c.sortable
966 issues = q.issues(:order => "#{c.sortable} ASC")
984 issues = q.issues(:order => "#{c.sortable} ASC")
967 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
985 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
968 assert !values.empty?
986 assert !values.empty?
969 assert_equal values.sort, values
987 assert_equal values.sort, values
970 end
988 end
971
989
972 def test_invalid_query_should_raise_query_statement_invalid_error
990 def test_invalid_query_should_raise_query_statement_invalid_error
973 q = IssueQuery.new
991 q = IssueQuery.new
974 assert_raise Query::StatementInvalid do
992 assert_raise Query::StatementInvalid do
975 q.issues(:conditions => "foo = 1")
993 q.issues(:conditions => "foo = 1")
976 end
994 end
977 end
995 end
978
996
979 def test_issue_count
997 def test_issue_count
980 q = IssueQuery.new(:name => '_')
998 q = IssueQuery.new(:name => '_')
981 issue_count = q.issue_count
999 issue_count = q.issue_count
982 assert_equal q.issues.size, issue_count
1000 assert_equal q.issues.size, issue_count
983 end
1001 end
984
1002
985 def test_issue_count_with_archived_issues
1003 def test_issue_count_with_archived_issues
986 p = Project.generate! do |project|
1004 p = Project.generate! do |project|
987 project.status = Project::STATUS_ARCHIVED
1005 project.status = Project::STATUS_ARCHIVED
988 end
1006 end
989 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1007 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
990 assert !i.visible?
1008 assert !i.visible?
991
1009
992 test_issue_count
1010 test_issue_count
993 end
1011 end
994
1012
995 def test_issue_count_by_association_group
1013 def test_issue_count_by_association_group
996 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1014 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
997 count_by_group = q.issue_count_by_group
1015 count_by_group = q.issue_count_by_group
998 assert_kind_of Hash, count_by_group
1016 assert_kind_of Hash, count_by_group
999 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1017 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1000 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1018 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1001 assert count_by_group.has_key?(User.find(3))
1019 assert count_by_group.has_key?(User.find(3))
1002 end
1020 end
1003
1021
1004 def test_issue_count_by_list_custom_field_group
1022 def test_issue_count_by_list_custom_field_group
1005 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1023 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1006 count_by_group = q.issue_count_by_group
1024 count_by_group = q.issue_count_by_group
1007 assert_kind_of Hash, count_by_group
1025 assert_kind_of Hash, count_by_group
1008 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1026 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1009 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1027 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1010 assert count_by_group.has_key?('MySQL')
1028 assert count_by_group.has_key?('MySQL')
1011 end
1029 end
1012
1030
1013 def test_issue_count_by_date_custom_field_group
1031 def test_issue_count_by_date_custom_field_group
1014 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1032 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1015 count_by_group = q.issue_count_by_group
1033 count_by_group = q.issue_count_by_group
1016 assert_kind_of Hash, count_by_group
1034 assert_kind_of Hash, count_by_group
1017 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1035 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1018 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1036 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1019 end
1037 end
1020
1038
1021 def test_issue_count_with_nil_group_only
1039 def test_issue_count_with_nil_group_only
1022 Issue.update_all("assigned_to_id = NULL")
1040 Issue.update_all("assigned_to_id = NULL")
1023
1041
1024 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1042 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1025 count_by_group = q.issue_count_by_group
1043 count_by_group = q.issue_count_by_group
1026 assert_kind_of Hash, count_by_group
1044 assert_kind_of Hash, count_by_group
1027 assert_equal 1, count_by_group.keys.size
1045 assert_equal 1, count_by_group.keys.size
1028 assert_nil count_by_group.keys.first
1046 assert_nil count_by_group.keys.first
1029 end
1047 end
1030
1048
1031 def test_issue_ids
1049 def test_issue_ids
1032 q = IssueQuery.new(:name => '_')
1050 q = IssueQuery.new(:name => '_')
1033 order = "issues.subject, issues.id"
1051 order = "issues.subject, issues.id"
1034 issues = q.issues(:order => order)
1052 issues = q.issues(:order => order)
1035 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1053 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1036 end
1054 end
1037
1055
1038 def test_label_for
1056 def test_label_for
1039 set_language_if_valid 'en'
1057 set_language_if_valid 'en'
1040 q = IssueQuery.new
1058 q = IssueQuery.new
1041 assert_equal 'Assignee', q.label_for('assigned_to_id')
1059 assert_equal 'Assignee', q.label_for('assigned_to_id')
1042 end
1060 end
1043
1061
1044 def test_label_for_fr
1062 def test_label_for_fr
1045 set_language_if_valid 'fr'
1063 set_language_if_valid 'fr'
1046 q = IssueQuery.new
1064 q = IssueQuery.new
1047 s = "Assign\xc3\xa9 \xc3\xa0"
1065 s = "Assign\xc3\xa9 \xc3\xa0"
1048 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
1066 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
1049 assert_equal s, q.label_for('assigned_to_id')
1067 assert_equal s, q.label_for('assigned_to_id')
1050 end
1068 end
1051
1069
1052 def test_editable_by
1070 def test_editable_by
1053 admin = User.find(1)
1071 admin = User.find(1)
1054 manager = User.find(2)
1072 manager = User.find(2)
1055 developer = User.find(3)
1073 developer = User.find(3)
1056
1074
1057 # Public query on project 1
1075 # Public query on project 1
1058 q = IssueQuery.find(1)
1076 q = IssueQuery.find(1)
1059 assert q.editable_by?(admin)
1077 assert q.editable_by?(admin)
1060 assert q.editable_by?(manager)
1078 assert q.editable_by?(manager)
1061 assert !q.editable_by?(developer)
1079 assert !q.editable_by?(developer)
1062
1080
1063 # Private query on project 1
1081 # Private query on project 1
1064 q = IssueQuery.find(2)
1082 q = IssueQuery.find(2)
1065 assert q.editable_by?(admin)
1083 assert q.editable_by?(admin)
1066 assert !q.editable_by?(manager)
1084 assert !q.editable_by?(manager)
1067 assert q.editable_by?(developer)
1085 assert q.editable_by?(developer)
1068
1086
1069 # Private query for all projects
1087 # Private query for all projects
1070 q = IssueQuery.find(3)
1088 q = IssueQuery.find(3)
1071 assert q.editable_by?(admin)
1089 assert q.editable_by?(admin)
1072 assert !q.editable_by?(manager)
1090 assert !q.editable_by?(manager)
1073 assert q.editable_by?(developer)
1091 assert q.editable_by?(developer)
1074
1092
1075 # Public query for all projects
1093 # Public query for all projects
1076 q = IssueQuery.find(4)
1094 q = IssueQuery.find(4)
1077 assert q.editable_by?(admin)
1095 assert q.editable_by?(admin)
1078 assert !q.editable_by?(manager)
1096 assert !q.editable_by?(manager)
1079 assert !q.editable_by?(developer)
1097 assert !q.editable_by?(developer)
1080 end
1098 end
1081
1099
1082 def test_visible_scope
1100 def test_visible_scope
1083 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1101 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1084
1102
1085 assert query_ids.include?(1), 'public query on public project was not visible'
1103 assert query_ids.include?(1), 'public query on public project was not visible'
1086 assert query_ids.include?(4), 'public query for all projects was not visible'
1104 assert query_ids.include?(4), 'public query for all projects was not visible'
1087 assert !query_ids.include?(2), 'private query on public project was visible'
1105 assert !query_ids.include?(2), 'private query on public project was visible'
1088 assert !query_ids.include?(3), 'private query for all projects was visible'
1106 assert !query_ids.include?(3), 'private query for all projects was visible'
1089 assert !query_ids.include?(7), 'public query on private project was visible'
1107 assert !query_ids.include?(7), 'public query on private project was visible'
1090 end
1108 end
1091
1109
1110 def test_query_with_public_visibility_should_be_visible_to_anyone
1111 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1112
1113 assert q.visible?(User.anonymous)
1114 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1115
1116 assert q.visible?(User.find(7))
1117 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1118
1119 assert q.visible?(User.find(2))
1120 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1121
1122 assert q.visible?(User.find(1))
1123 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1124 end
1125
1126 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1127 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1128
1129 assert !q.visible?(User.anonymous)
1130 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1131
1132 assert !q.visible?(User.find(7))
1133 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1134
1135 assert q.visible?(User.find(2))
1136 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1137
1138 assert q.visible?(User.find(1))
1139 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1140 end
1141
1142 def test_query_with_private_visibility_should_be_visible_to_owner
1143 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1144
1145 assert !q.visible?(User.anonymous)
1146 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1147
1148 assert q.visible?(User.find(7))
1149 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1150
1151 assert !q.visible?(User.find(2))
1152 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1153
1154 assert q.visible?(User.find(1))
1155 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1156 end
1157
1092 test "#available_filters should include users of visible projects in cross-project view" do
1158 test "#available_filters should include users of visible projects in cross-project view" do
1093 users = IssueQuery.new.available_filters["assigned_to_id"]
1159 users = IssueQuery.new.available_filters["assigned_to_id"]
1094 assert_not_nil users
1160 assert_not_nil users
1095 assert users[:values].map{|u|u[1]}.include?("3")
1161 assert users[:values].map{|u|u[1]}.include?("3")
1096 end
1162 end
1097
1163
1098 test "#available_filters should include users of subprojects" do
1164 test "#available_filters should include users of subprojects" do
1099 user1 = User.generate!
1165 user1 = User.generate!
1100 user2 = User.generate!
1166 user2 = User.generate!
1101 project = Project.find(1)
1167 project = Project.find(1)
1102 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1168 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1103
1169
1104 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1170 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1105 assert_not_nil users
1171 assert_not_nil users
1106 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1172 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1107 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1173 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1108 end
1174 end
1109
1175
1110 test "#available_filters should include visible projects in cross-project view" do
1176 test "#available_filters should include visible projects in cross-project view" do
1111 projects = IssueQuery.new.available_filters["project_id"]
1177 projects = IssueQuery.new.available_filters["project_id"]
1112 assert_not_nil projects
1178 assert_not_nil projects
1113 assert projects[:values].map{|u|u[1]}.include?("1")
1179 assert projects[:values].map{|u|u[1]}.include?("1")
1114 end
1180 end
1115
1181
1116 test "#available_filters should include 'member_of_group' filter" do
1182 test "#available_filters should include 'member_of_group' filter" do
1117 query = IssueQuery.new
1183 query = IssueQuery.new
1118 assert query.available_filters.keys.include?("member_of_group")
1184 assert query.available_filters.keys.include?("member_of_group")
1119 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1185 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1120 assert query.available_filters["member_of_group"][:values].present?
1186 assert query.available_filters["member_of_group"][:values].present?
1121 assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]},
1187 assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]},
1122 query.available_filters["member_of_group"][:values].sort
1188 query.available_filters["member_of_group"][:values].sort
1123 end
1189 end
1124
1190
1125 test "#available_filters should include 'assigned_to_role' filter" do
1191 test "#available_filters should include 'assigned_to_role' filter" do
1126 query = IssueQuery.new
1192 query = IssueQuery.new
1127 assert query.available_filters.keys.include?("assigned_to_role")
1193 assert query.available_filters.keys.include?("assigned_to_role")
1128 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1194 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1129
1195
1130 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1196 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1131 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1197 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1132 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1198 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1133
1199
1134 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1200 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1135 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1201 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1136 end
1202 end
1137
1203
1138 context "#statement" do
1204 context "#statement" do
1139 context "with 'member_of_group' filter" do
1205 context "with 'member_of_group' filter" do
1140 setup do
1206 setup do
1141 Group.destroy_all # No fixtures
1207 Group.destroy_all # No fixtures
1142 @user_in_group = User.generate!
1208 @user_in_group = User.generate!
1143 @second_user_in_group = User.generate!
1209 @second_user_in_group = User.generate!
1144 @user_in_group2 = User.generate!
1210 @user_in_group2 = User.generate!
1145 @user_not_in_group = User.generate!
1211 @user_not_in_group = User.generate!
1146
1212
1147 @group = Group.generate!.reload
1213 @group = Group.generate!.reload
1148 @group.users << @user_in_group
1214 @group.users << @user_in_group
1149 @group.users << @second_user_in_group
1215 @group.users << @second_user_in_group
1150
1216
1151 @group2 = Group.generate!.reload
1217 @group2 = Group.generate!.reload
1152 @group2.users << @user_in_group2
1218 @group2.users << @user_in_group2
1153
1219
1154 end
1220 end
1155
1221
1156 should "search assigned to for users in the group" do
1222 should "search assigned to for users in the group" do
1157 @query = IssueQuery.new(:name => '_')
1223 @query = IssueQuery.new(:name => '_')
1158 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1224 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1159
1225
1160 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1226 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1161 assert_find_issues_with_query_is_successful @query
1227 assert_find_issues_with_query_is_successful @query
1162 end
1228 end
1163
1229
1164 should "search not assigned to any group member (none)" do
1230 should "search not assigned to any group member (none)" do
1165 @query = IssueQuery.new(:name => '_')
1231 @query = IssueQuery.new(:name => '_')
1166 @query.add_filter('member_of_group', '!*', [''])
1232 @query.add_filter('member_of_group', '!*', [''])
1167
1233
1168 # Users not in a group
1234 # Users not in a group
1169 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1235 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1170 assert_find_issues_with_query_is_successful @query
1236 assert_find_issues_with_query_is_successful @query
1171 end
1237 end
1172
1238
1173 should "search assigned to any group member (all)" do
1239 should "search assigned to any group member (all)" do
1174 @query = IssueQuery.new(:name => '_')
1240 @query = IssueQuery.new(:name => '_')
1175 @query.add_filter('member_of_group', '*', [''])
1241 @query.add_filter('member_of_group', '*', [''])
1176
1242
1177 # Only users in a group
1243 # Only users in a group
1178 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1244 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1179 assert_find_issues_with_query_is_successful @query
1245 assert_find_issues_with_query_is_successful @query
1180 end
1246 end
1181
1247
1182 should "return an empty set with = empty group" do
1248 should "return an empty set with = empty group" do
1183 @empty_group = Group.generate!
1249 @empty_group = Group.generate!
1184 @query = IssueQuery.new(:name => '_')
1250 @query = IssueQuery.new(:name => '_')
1185 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1251 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1186
1252
1187 assert_equal [], find_issues_with_query(@query)
1253 assert_equal [], find_issues_with_query(@query)
1188 end
1254 end
1189
1255
1190 should "return issues with ! empty group" do
1256 should "return issues with ! empty group" do
1191 @empty_group = Group.generate!
1257 @empty_group = Group.generate!
1192 @query = IssueQuery.new(:name => '_')
1258 @query = IssueQuery.new(:name => '_')
1193 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1259 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1194
1260
1195 assert_find_issues_with_query_is_successful @query
1261 assert_find_issues_with_query_is_successful @query
1196 end
1262 end
1197 end
1263 end
1198
1264
1199 context "with 'assigned_to_role' filter" do
1265 context "with 'assigned_to_role' filter" do
1200 setup do
1266 setup do
1201 @manager_role = Role.find_by_name('Manager')
1267 @manager_role = Role.find_by_name('Manager')
1202 @developer_role = Role.find_by_name('Developer')
1268 @developer_role = Role.find_by_name('Developer')
1203
1269
1204 @project = Project.generate!
1270 @project = Project.generate!
1205 @manager = User.generate!
1271 @manager = User.generate!
1206 @developer = User.generate!
1272 @developer = User.generate!
1207 @boss = User.generate!
1273 @boss = User.generate!
1208 @guest = User.generate!
1274 @guest = User.generate!
1209 User.add_to_project(@manager, @project, @manager_role)
1275 User.add_to_project(@manager, @project, @manager_role)
1210 User.add_to_project(@developer, @project, @developer_role)
1276 User.add_to_project(@developer, @project, @developer_role)
1211 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1277 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1212
1278
1213 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1279 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1214 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1280 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1215 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1281 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1216 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1282 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1217 @issue5 = Issue.generate!(:project => @project)
1283 @issue5 = Issue.generate!(:project => @project)
1218 end
1284 end
1219
1285
1220 should "search assigned to for users with the Role" do
1286 should "search assigned to for users with the Role" do
1221 @query = IssueQuery.new(:name => '_', :project => @project)
1287 @query = IssueQuery.new(:name => '_', :project => @project)
1222 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1288 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1223
1289
1224 assert_query_result [@issue1, @issue3], @query
1290 assert_query_result [@issue1, @issue3], @query
1225 end
1291 end
1226
1292
1227 should "search assigned to for users with the Role on the issue project" do
1293 should "search assigned to for users with the Role on the issue project" do
1228 other_project = Project.generate!
1294 other_project = Project.generate!
1229 User.add_to_project(@developer, other_project, @manager_role)
1295 User.add_to_project(@developer, other_project, @manager_role)
1230
1296
1231 @query = IssueQuery.new(:name => '_', :project => @project)
1297 @query = IssueQuery.new(:name => '_', :project => @project)
1232 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1298 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1233
1299
1234 assert_query_result [@issue1, @issue3], @query
1300 assert_query_result [@issue1, @issue3], @query
1235 end
1301 end
1236
1302
1237 should "return an empty set with empty role" do
1303 should "return an empty set with empty role" do
1238 @empty_role = Role.generate!
1304 @empty_role = Role.generate!
1239 @query = IssueQuery.new(:name => '_', :project => @project)
1305 @query = IssueQuery.new(:name => '_', :project => @project)
1240 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1306 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1241
1307
1242 assert_query_result [], @query
1308 assert_query_result [], @query
1243 end
1309 end
1244
1310
1245 should "search assigned to for users without the Role" do
1311 should "search assigned to for users without the Role" do
1246 @query = IssueQuery.new(:name => '_', :project => @project)
1312 @query = IssueQuery.new(:name => '_', :project => @project)
1247 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1313 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1248
1314
1249 assert_query_result [@issue2, @issue4, @issue5], @query
1315 assert_query_result [@issue2, @issue4, @issue5], @query
1250 end
1316 end
1251
1317
1252 should "search assigned to for users not assigned to any Role (none)" do
1318 should "search assigned to for users not assigned to any Role (none)" do
1253 @query = IssueQuery.new(:name => '_', :project => @project)
1319 @query = IssueQuery.new(:name => '_', :project => @project)
1254 @query.add_filter('assigned_to_role', '!*', [''])
1320 @query.add_filter('assigned_to_role', '!*', [''])
1255
1321
1256 assert_query_result [@issue4, @issue5], @query
1322 assert_query_result [@issue4, @issue5], @query
1257 end
1323 end
1258
1324
1259 should "search assigned to for users assigned to any Role (all)" do
1325 should "search assigned to for users assigned to any Role (all)" do
1260 @query = IssueQuery.new(:name => '_', :project => @project)
1326 @query = IssueQuery.new(:name => '_', :project => @project)
1261 @query.add_filter('assigned_to_role', '*', [''])
1327 @query.add_filter('assigned_to_role', '*', [''])
1262
1328
1263 assert_query_result [@issue1, @issue2, @issue3], @query
1329 assert_query_result [@issue1, @issue2, @issue3], @query
1264 end
1330 end
1265
1331
1266 should "return issues with ! empty role" do
1332 should "return issues with ! empty role" do
1267 @empty_role = Role.generate!
1333 @empty_role = Role.generate!
1268 @query = IssueQuery.new(:name => '_', :project => @project)
1334 @query = IssueQuery.new(:name => '_', :project => @project)
1269 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1335 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1270
1336
1271 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1337 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1272 end
1338 end
1273 end
1339 end
1274 end
1340 end
1275 end
1341 end
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now